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.
The options.ext file used by OpenSSL contains the following extensions:
basicConstraints=CA:FALSE
authorityKeyIdentifier=keyid,issuer
subjectKeyIdentifier=hash
keyUsage=digitalSignature
extendedKeyUsage=clientAuth
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
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?
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
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:
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.