gox509certificateasn.1authoritykeyidentifier

How to add DirName and serial to X509v3 Authority Key Identifier


I'm trying to generate a client certificate using OpenSSL and Go code. I have an OpenSSL script that generates the certificate with the required extensions, and I want to achieve the same result using Go code.

With OpenSSL

options.ext

The options.ext file used by OpenSSL contains the following extensions:

basicConstraints=CA:FALSE
authorityKeyIdentifier=keyid,issuer
subjectKeyIdentifier=hash
keyUsage=digitalSignature
extendedKeyUsage=clientAuth

generate-client-cert.sh

The OpenSSL script I currently have is as follows:

openssl req \
  -newkey rsa:2048 \
  -keyout cert.crt \
  -out cert.csr \
  -nodes \
  -sha256

openssl x509 \
  -req \
  -CA ca.crt \
  -CAkey ca.key \
  -in cert.csr \
  -out cert.crt \
  -days 365 \
  -CAcreateserial \
  -extfile options.ext \
  -sha256

After generating the certificate, I can use the following command to view its details:

openssl x509 -in cert.crt -text -noout

The resulting certificate has the following structure:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            xx:xx:xx:xx:xx:xx:xx:xx
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=xxx
        Validity
            Not Before: Jan 1 00:00:00 2023 GMT
            Not After : Jan 1 00:00:00 2024 GMT
        Subject: CN=xxx
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    ...
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: 
                CA:FALSE
            X509v3 Authority Key Identifier: 
                DirName:CN=xxx
                serial:xx:xx:xx:xx:xx:xx:xx:xx

            X509v3 Subject Key Identifier: 
                ...
            X509v3 Key Usage: 
                Digital Signature
            X509v3 Extended Key Usage: 
                TLS Web Client Authentication
    Signature Algorithm: sha256WithRSAEncryption

it should look like this:

X509v3 Authority Key Identifier: 
    DirName:CN=xxx
    serial:xx:xx:xx:xx:xx:xx:xx:xx

Go code

In my Go code, I'm using the x509 package to generate the certificate. However, I'm unsure how to set the X509v3 Authority Key Identifier extension. Here's the relevant part of my Go code:

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha1"
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/asn1"
    "os"
    "time"
)

...

var caCertificate *x509.Certificate
var caPrivateKey *rsa.PrivateKey

var authorityKeyIdentifierValue []byte // how to write this?

template := &x509.Certificate{
    Subject: pkix.Name{
        CommonName: "xxx",
    },
    ExtraExtensions: []pkix.Extension{
        {
            Id:    asn1.ObjectIdentifier{2, 5, 29, 35},
            Value: authorityKeyIdentifierValue,
        },
    },
    KeyUsage:              x509.KeyUsageDigitalSignature,
    ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
    NotBefore:             time.Now(),
    NotAfter:              time.Now().AddDate(0, 0, 365),
    IsCA:                  false,
    BasicConstraintsValid: true,
}

privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
    // err
}

certificateBytes, err := x509.CreateCertificate(rand.Reader, template, caCertificate, &privateKey.PublicKey, caPrivateKey)
if err != nil {
    // err
}

// out

How to add DirName and serial to X509v3 Authority Key Identifier?

Related

When I tried this:

var caPublicKeyBytes []byte
publicKeyHash := (sha1.Sum(caPublicKeyBytes))[:]

var dirName string

authorityKeyIdentifierValue := []byte{0x30, len(publicKeyHash)}
authorityKeyIdentifierValue = append(authorityKeyIdentifierValue, publicKeyHash...)
authorityKeyIdentifierValue = append(authorityKeyIdentifierValue, 0x80, len(dirName))
authorityKeyIdentifierValue = append(authorityKeyIdentifierValue, []byte(dirName)...)
...

The result was:

X509v3 Authority Key Identifier:
    0....0...<....).!.r[..F.....".hCN=xxx.....$...D

Solution

  • The authorityKeyIdentifierValue can be generated with asn1.Marshal. The demo below defines the struct authKeyId according to RFC 5280 and generates the value using this struct:

    package main
    
    import (
        "crypto/x509"
        "crypto/x509/pkix"
        "encoding/asn1"
        "encoding/hex"
        "encoding/pem"
        "fmt"
        "math/big"
    )
    
    // RFC 5280, A.2. Implicitly Tagged Module, 1988 Syntax
    //
    //  AuthorityKeyIdentifier ::= SEQUENCE {
    //      keyIdentifier             [0] KeyIdentifier            OPTIONAL,
    //      authorityCertIssuer       [1] GeneralNames             OPTIONAL,
    //      authorityCertSerialNumber [2] CertificateSerialNumber  OPTIONAL }
    //      -- authorityCertIssuer and authorityCertSerialNumber MUST both
    //      -- be present or both be absent
    type authKeyId struct {
        KeyIdentifier             []byte       `asn1:"optional,tag:0"`
        AuthorityCertIssuer       generalNames `asn1:"optional,tag:1"`
        AuthorityCertSerialNumber *big.Int     `asn1:"optional,tag:2"`
    }
    
    // RFC 5280, A.2. Implicitly Tagged Module, 1988 Syntax
    //
    //  GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
    //
    //  GeneralName ::= CHOICE {
    //       otherName                 [0]  AnotherName,
    //       rfc822Name                [1]  IA5String,
    //       dNSName                   [2]  IA5String,
    //       x400Address               [3]  ORAddress,
    //       directoryName             [4]  Name,
    //       ediPartyName              [5]  EDIPartyName,
    //       uniformResourceIdentifier [6]  IA5String,
    //       iPAddress                 [7]  OCTET STRING,
    //       registeredID              [8]  OBJECT IDENTIFIER }
    type generalNames struct {
        Name []pkix.RDNSequence `asn1:"tag:4"`
    }
    
    func gen(issuer *x509.Certificate) ([]byte, error) {
        return asn1.Marshal(authKeyId{
            KeyIdentifier:             issuer.SubjectKeyId,
            AuthorityCertIssuer:       generalNames{Name: []pkix.RDNSequence{issuer.Issuer.ToRDNSequence()}},
            AuthorityCertSerialNumber: issuer.SerialNumber,
        })
    }
    
    func main() {
        caCert := `-----BEGIN CERTIFICATE-----
    MIIBoTCCAUegAwIBAgIQGoCjDJN1Y6rGWEbXW8V8MDAKBggqhkjOPQQDAjAmMQ8w
    DQYDVQQKEwZNeSBPcmcxEzARBgNVBAMTCk15IFJvb3QgQ0EwHhcNMjMwNTE2MTQy
    NTUwWhcNMjMwNTE3MTUyNTUwWjAmMQ8wDQYDVQQKEwZNeSBPcmcxEzARBgNVBAMT
    Ck15IFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARZQz2Ka7Fi6w9/
    32SJHTAjrkE+VqYx7hFNmtX1INPBAJNfvONF2SIlh5nQmS50JpNVGIvEhTbFL0A0
    dcuruFHno1cwVTAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEw
    DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU5Y48DJ96LQWVh3S/aNJ/6SGy/j4w
    CgYIKoZIzj0EAwIDSAAwRQIhANQDh6SGZ014wVFdH0ZHbEGhdb2TqXZUJxA7YMo3
    80UnAiApZp4wlzqlB+J4fIPnep+Txru01JgFaKsml2yHv3mEWg==
    -----END CERTIFICATE-----`
        b, _ := pem.Decode([]byte(caCert))
        if b == nil {
            panic("couldn't decode test certificate")
        }
        issuer, err := x509.ParseCertificate(b.Bytes)
        if err != nil {
            panic(err)
        }
    
        authorityKeyIdentifierValue, err := gen(issuer)
        if err != nil {
            panic(err)
        }
        fmt.Println(hex.EncodeToString(authorityKeyIdentifierValue))
    }
    

    The hex encoded value is:

    30548014e58e3c0c9f7a2d05958774bf68d27fe921b2fe3ea12aa4283026310f300d060355040a13064d79204f7267311330110603550403130a4d7920526f6f7420434182101a80a30c937563aac65846d75bc57c30
    

    The hex string can be decoded with tools such as ASN.1 JavaScript decoder:

    enter image description here


    The following C# demo gives the same result:

    using System.Security.Cryptography.X509Certificates;
    using System.Text;
    
    internal class Program
    {
        private static void Main(string[] args)
        {
            var certBytes = Encoding.ASCII.GetBytes(@"-----BEGIN CERTIFICATE-----
    MIIBoTCCAUegAwIBAgIQGoCjDJN1Y6rGWEbXW8V8MDAKBggqhkjOPQQDAjAmMQ8w
    DQYDVQQKEwZNeSBPcmcxEzARBgNVBAMTCk15IFJvb3QgQ0EwHhcNMjMwNTE2MTQy
    NTUwWhcNMjMwNTE3MTUyNTUwWjAmMQ8wDQYDVQQKEwZNeSBPcmcxEzARBgNVBAMT
    Ck15IFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARZQz2Ka7Fi6w9/
    32SJHTAjrkE+VqYx7hFNmtX1INPBAJNfvONF2SIlh5nQmS50JpNVGIvEhTbFL0A0
    dcuruFHno1cwVTAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEw
    DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU5Y48DJ96LQWVh3S/aNJ/6SGy/j4w
    CgYIKoZIzj0EAwIDSAAwRQIhANQDh6SGZ014wVFdH0ZHbEGhdb2TqXZUJxA7YMo3
    80UnAiApZp4wlzqlB+J4fIPnep+Txru01JgFaKsml2yHv3mEWg==
    -----END CERTIFICATE-----");
    
            using var issuer = new X509Certificate2(certBytes);
            var e = X509AuthorityKeyIdentifierExtension.CreateFromCertificate(issuer, true, true);
            Console.WriteLine(ByteArrayToHex(e.RawData));
        }
    
        private static string ByteArrayToHex(byte[] bytes)
        {
            var builder = new StringBuilder(bytes.Length * 2);
    
            for (int i = 0; i < bytes.Length; i++)
            {
                builder.Append($"{bytes[i]:x2}");
            }
    
            return builder.ToString();
        }
    }
    

    Update:

    Here is the updated version of gen to include email address:

    func gen(issuer *x509.Certificate) ([]byte, error) {
        rdnSequence := issuer.Issuer.ToRDNSequence()
        if len(issuer.EmailAddresses) > 0 {
            oidEmail := asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}
            emails := make([]pkix.AttributeTypeAndValue, len(issuer.EmailAddresses))
            for i, value := range issuer.EmailAddresses {
                emails[i].Type = oidEmail
                emails[i].Value = value
            }
            rdnSequence = append(rdnSequence, emails)
        }
    
        return asn1.Marshal(authKeyId{
            KeyIdentifier:             issuer.SubjectKeyId,
            AuthorityCertIssuer:       generalNames{Name: []pkix.RDNSequence{rdnSequence}},
            AuthorityCertSerialNumber: issuer.SerialNumber,
        })
    }
    

    Please note that the OID is deprecated (see http://oid-info.com/get/1.2.840.113549.1.9.1). And .NET does not include it too.