1

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

1 Answers1

1

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.

Zeke Lu
  • 6,349
  • 1
  • 17
  • 23
  • 1
    It works as you described. – Hyeonsoo David Lee May 19 '23 at 02:49
  • But there is still am issue, and it might be a bug in the implementation of Go. Specifically, the generated certificate's AuthorityKeyIdentifier's DirName is missing the "emailAddress" field compared to the Names field of your CA (Issuer). Issuer's Subject ``` C=KR, ST=Busan, L=Busan, O=Innomic, OU=DevOps, CN=innomic.ml/emailAddress=devops@innomic.ml ``` AuthorityKeyIdentifier.DirName (generated) ``` C=KR, ST=Busan, L=Busan, O=Innomic, OU=DevOps, CN=innomic.ml ``` – Hyeonsoo David Lee May 19 '23 at 03:03
  • 1
    @HyeonsooDavidLee I have updated the answer to add a new `gen` func that includes the email address in the extension. But it seems that this oid was deprecated. – Zeke Lu May 19 '23 at 04:48