x509 certificate templates with step ca

Rather that having your step ca issue certificates that only reflect what is in the CSR, you can use certificate templates in order to dynamically add content to the x509 certificates being issued.

CA Configuration

In your /usr/local/etc/step/ca/config/ca.json configuration file you need to add some options to your provisioner. Let’s assume you have an existing ACME provisioner that looks as follows:

                        {
                                "type": "ACME",
                                "name": "24h",
                                "claims": {
                                        "maxTLSCertDuration": "24h0m0s",
                                        "defaultTLSCertDuration": "24h0m0s"
                                }
                        },

After the claims section you need to insert an options block. As usual, don’t forget the comma after the curly braces that end the claims section.

                        {
                                "type": "ACME",
                                "name": "24h",
                                "claims": {
                                        "maxTLSCertDuration": "24h0m0s",
                                        "defaultTLSCertDuration": "24h0m0s"
                                },
                                "options": {
                                        "x509": {
                                                "templateFile": "/usr/local/etc/step/ca/templates/certs/x509/acme.tpl",
                                                "templateData": {
                                                        "TDCountry": "DE",
                                                        "TDStateOrProvince": "Saxony",
                                                        "TDLocality": "Dresden",
                                                        "TDStreetAddress": "Musterstrasse 1, 01234 Dresden, Germany",
                                                        "TDOrganization": "Tinkivity",
                                                        "TDOrganizationalUnit": "web server team"
                                                }
                                        }
                                }
                        },

Line 10 points to a template file that we will look at in just a few seconds. Line 11 introduces a data section that we use to inject some dynamic data. The data items (line 12 until line 17) make more sense when looking at the actual acme.tpl template file as referenced in line 11.

Template file

Below shows our acme.tpl template file. I will not explain the complete template but just some of the most important aspects.

{
    "subject": {
    {{- if .Insecure.CR.Subject.CommonName }}
        "commonName": "{{ .Insecure.CR.Subject.CommonName }}",
    {{- else }}
        "commonName": "{{ (index .SANs 0).Value }}",
    {{- end }}
        "country": "{{ .TDCountry }}",
        "province": "{{ .TDStateOrProvince }}",
        "locality": "{{ .TDLocality }}",
        "streetAddress": "{{ .TDStreetAddress }}",
        "organization": "{{ .TDOrganization }}",
        "organizationalUnit": "{{ .TDOrganizationalUnit }}"
    },
    "sans": {{ toJson .SANs }},
{{- if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }}
    "keyUsage": ["keyEncipherment", "digitalSignature"],
{{- else }}
    "keyUsage": ["digitalSignature"],
{{- end }}
    "extKeyUsage": ["serverAuth", "clientAuth"]
}

Lines 3 until 7 check if the CSR contains a common name in the subject. If the common name exists it will be applied, otherwise the first subject alternative name is used for the common name in the certificate subject. The reason for that logic is that certbot in my case seems to sometimes omit the common name ;-(

Lines 8 until 13 contain a reference to the injected template data and set the according subject fields for the certificate that is being issued.

Certificate issued with applied template

Let’s check how a certificate obtained via certbot looks like after the template is applied.

andreas@testserver ➜  ~ sudo openssl x509 -noout -text -in /usr/local/etc/letsencrypt/live/testserver/fullchain.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            9f:95:31:ed:1f:0b:b8:99:39:e1:64:02:73:89:d1:db
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = DE, ST = Saxony, O = Tinkivity, OU = Tinkivity Intermediate Certificate Authority, CN = Smallstep Intermediate CA, emailAddress = xxx@xxx.com
        Validity
            Not Before: Dec  7 18:18:07 2020 GMT
            Not After : Dec  8 18:19:07 2020 GMT
        Subject: C = DE, ST = Saxony, L = Dresden, street = "Musterstrasse 1, 01234 Dresden, Germany", O = Tinkivity, OU = web server team, CN = testserver
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:c4:74:01:28:bb:26:20:2f:a1:6b:30:44:9e:9b:
                    ...
                    << REDACTED >>
                    ...
                    04:57
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Subject Key Identifier: 
                81:7C:BB:71:7F:02:52:06:71:CF:E2:87:3B:CA:8A:09:6C:81:65:46
            X509v3 Authority Key Identifier: 
                keyid:87:32:28:49:63:29:06:79:96:13:DE:47:14:9F:EF:C0:DD:EC:4D:C3

            X509v3 Subject Alternative Name: 
                DNS:testserver, DNS:testserver.local
            1.3.6.1.4.1.37476.9000.64.1: 
                0
.....24h..
    Signature Algorithm: sha256WithRSAEncryption
         b7:74:16:a4:a1:5a:fb:df:a0:ea:42:5c:cd:70:fc:16:2d:8b:
         ...
         << REDACTED >>
         ...
         c9:9e:db:00:40:56:61:a1