Skip to content

certificate assembler.1

mbenkmann edited this page Nov 20, 2015 · 12 revisions

CERTIFICATE-ASSEMBLER(1)

NAME

certificate-assembler - Low-level tool for creating X.509 certificates

SYNOPSIS

certificate-assembler [defs.asn1 …​] inputfile.json

DESCRIPTION

certificate-assembler(1) takes an input specification inputfile.json in JSON format and produces X.509 certificates and associated files (e.g. cryptographic keys). It is a low-level tool in the sense that the input specification is closely related to the data structure definitions in the relevant RFCs. Field names and other symbolic names are exactly as found in the RFCs and you have full control over data types used. In addition you can define your own variables to avoid repetition and make the input more readable. It is also possible to provide additional definition files defs.asn1…​ in ASN.1 syntax, e.g. for custom certificate extensions and the data structures and symbolic names from these can be used as if they were directly part of the RFCs. As such certificate-assembler(1) is very different from openssl(1) which uses its own syntax and names and provides high-level abstractions in some places but requires very low-level specifications in others.

The easiest way to get started with certificate-assembler(1) is to use its counterpart certificate-disassembler(1) on an existing certificate (e.g. one of those found in /usr/share/ca-certificates) and to use the output as basis.

INPUT FILE

The input file format is JSON, i.e. JavaScript Object Notation. As an extension to JSON, certificate-assembler(1) accepts lines that start with the character "#". These lines are ignored and can be used to embed comments in the file. The input file must describe a single object, i.e. after stripping comments the first non-whitespace character has to be "{" and the last "}". The following is an example of an acceptable input file:

#!/usr/bin/certificate-assembler
# Assuming that the program /usr/bin/certificate-assembler
# is available, this file can be made executable and be run
# as a script. It will write "Hello World!\n" to stdout.
{
  "message": "Hello World!\n",
  "program": "$message '/dev/stdout' write()"
}

Neither field names nor structure of the input object have any predefined semantics. certificate-assembler(1) parses the JSON object from the input file and scans it recursively for strings that start with "$". These special strings are interpreted as described in the section PROGRAMS below. Everything else contained in the JSON object may serve as data for PROGRAMS but is otherwise ignored by certificate-assembler(1). Even though the following example contains a complex structure of nested objects and arrays, the only thing it actually does when passed to certificate-assembler(1) is to print "Hello World!\n" to stdout. This is caused by the program "$foobar '/dev/stdout' write()" which uses the field "foobar" to provide the message. Everything else contained in the JSON has no effect.

{
  "this field does": "nothing",
  "whatever": {
      "stuff": [1, true, ["$foobar '/dev/stdout' write()"], false, 999],
      "more": "stuff"
  },
  "foobar": "Hello World!\n"
}

PROGRAMS

Overview
  • All strings whose first character is "$" are executed as programs.

  • A program is a sequence that may contain literals, field references, ASN.1 type names, ASN.1 value names and function calls.

  • The program sequence is executed left to right.

  • Literals, field references and ASN.1 value names cause the respective value to be pushed onto the program stack. The value thus pushed may be a complex structure such as an array or a JSON object.

  • An ASN.1 type name causes the top value on the program stack to be converted to that type.

  • A function call executes the respective function which will remove one or more elements from the top of the stack, use them as parameters, do something, and push a result onto the stack.

  • In order to be correct, a program must end with exactly 1 element on the stack.

Literals
  • A string is enclosed in single-quotes '…​'. To embed a single-quote character in a string literal, double it. E.g. 'That''s easy!'

  • An integer sequence is a series of at least 3 non-negative integers separated by ".", e.g. "1.2.3". Integer sequence literals are used to write OBJECT IDENTIFIERS and IP addresses.

  • Integer literals are written base-10 and may have any size. It’s important to note that outside of programs, i.e. at the JSON level, the valid range for numbers is severely limited. If in doubt, wrap numbers in a program e.g. write "$1234567892345678923456789" instead of naked 1234567892345678923456789.

Field references

A program may reference other fields from the JSON object it is part of and from surrounding JSON objects if any. A field is referenced simply by writing its name. Fields whose names contain spaces or that look like other program elements (e.g. a field called "10" which looks like an integer or a field called "UTF8String" which looks like the ASN.1 type of that name) cannot be referenced. If there are multiple fields that with the same name in nested objects, the one closest to the referring program takes precedence. The referenced field values may be complex structures such as arrays or JSON objects. Look at the following example:

{
  "key": "$'my.key' key()",

  "sigAlg": {
      "algorithm": "$sha256WithRSAEncryption",
      "parameters": null
  },

  "tbsCertificate": {
      "signature": "$sigAlg",
      ...
  },

  "signature": "$tbsCertificate TBSCertificate encode(DER) key sigAlg sign()"
}

The above example has 2 programs called "signature". One in the outer-most JSON object and one in the enclosed "tbsCertificate" object. Both fields reference the field "sigAlg" which for the "signature" within "tbsCertificate" is a field that belongs to an enclosing JSON object. The "sigAlg" field’s value is a JSON object that will be pushed onto the stack as a whole.

Note that the "$" character is only used to mark a string as a program. It is not part of a reference as it would be in the case of a shell script. So while "$sigAlg" looks like a variable reference in a shell script, the 2nd "signature" program in the above example illustrates that the actual reference to the sigAlg field does not use a "$".

Functions
  • encode(DER): Takes one argument that is either a key generated by keygen() or read by key(); or an instance of any ASN.1 type (either basic or custom). The argument is converted to a byte-array containing its DER-encoding.

  • encode(PEM): Takes one argument, encodes it like encode(DER) and then converts the result to PEM format.

  • decode(hex): Takes a string argument containing an even number of hex digits, optionally prefixed by "0x", and produces a byte-array with one byte per 2 hex digits.

  • write(): Takes two arguments where one has to be of type string and the other may be either a string or a byte-array. If both arguments are strings, the stack top is the file name. If only one argument is of type string, this is the file name. The function writes the raw bytes from the other argument to the specified file.

  • write(if-missing): Like write() but if the file already exists, it will not be overwritten. The following coding pattern may be used to use an existing key file if one exists or to create a new one if there is no existing key.

"keyfile": "secret.key",
"genkey": "$secp256r1 keygen()",
  # start with underscore to make sure "_save" is executed before "key"
"_save": "$genkey encode(PEM) keyfile write(if-missing)",
"key": "keyfile key()"
  • key(): Takes a file name string as argument and reads a private key from that file. All of the usual private key formats are supported. The key() function takes care not to read more bytes than necessary, so it is possible to read multiple keys from /dev/stdin. The key() function skips over leading garbage without producing an error.

  • keygen(): Takes one argument that is either an integer or an OBJECT IDENTIFIER. If the argument is an integer it specifies the number of bits and keygen() generates an RSA key with that bit length. If the argument is an OBJECT IDENTIFIER it has to identify an elliptic curve and keygen() will generate an EC key using that curve.

  • subjectPublicKeyInfo(): Takes one argument that is a private key as generated by keygen() or read by key(). The corresponding public key is extracted and converted to a JSON object that is compatible with ASN.1 type SubjectPublicKeyInfo.

  • sign(): Takes 3 arguments: a byte-array (usually produced by encode(DER)), a key produced by keygen() or key(), and a JSON object compatible with ASN.1 type AlgorithmIdentifier. The output of sign() is a byte-array containing the digital signature of the input byte-array when signed with the given key according to the algorithm specified in the AlgorithmIdentifier.

EXECUTION ORDER

When certificate-assembler(1) processes a JSON object it sorts its field names lexicographically and processes them in ascending order. If a field’s value is a program and that program references one or more fields that have not yet been processed, certificate-assembler(1) makes an exception to the lexicographic ordering and processes the referenced fields before the program that references them. This means you do not have to worry about dependencies of programs on other fields.

However, some ordering requirements do not manifest as field dependencies and cannot be handled automatically by certificate-assembler(1). A common case is when one program writes a cryptographic key to a file and another program reads the key from the file. Because the program doing the reading does not reference the field containing the program that writes the file, certificate-assembler(1) cannot determine the proper ordering automatically and will apply the default lexicographic ordering. This means that ordering requirements such as this have to be handled explicitly by using field names that sort properly.

It is important to always keep in mind that the ordering of fields within the input file is irrelevant. Both of the following input files produce the same output.

{
  "10": "$'Hello ' '/dev/stdout' write()",
  "20": "$'World!\n' '/dev/stdout' write()"
}
{
  "20": "$'World!\n' '/dev/stdout' write()",
  "10": "$'Hello ' '/dev/stdout' write()"
}

AUTHOR

Matthias S. Benkmann, <[email protected]>

SEE ALSO

certificate-disassembler(1), openssl(1), RFC 5280, RFC 5480, RFC 4055