⟵ Home

ASN.1 Field Ordering in Go

October 18, 2020 ∙ 4 minute read

A few days ago I had to generate a Certificate Signing Request using Go; dealing with crypto in Go is always a breeze, but then I was notified by the Certification Authority that fields were out of order. Technically, considering the DER nature of the CSR, the order of Organizational Unit should not matter. The CSR was regenerated to no avail.

The solution was to write the CSR manually (sort of manually, see below), defining all asn1 bits. That comprised of digging through the documentation, implementation files and tests, so I could understand what was happening behind the courtains. Personally, what most intriged me was Go checking for a prefix in a type name through reflection to determine whether it was an ASN.1 sequence or set.

So, first of all, let’s determine what we will need to generate our Subject line: Object IDs for all the fields we plan on filling, a custom type so we can write UTF8String instead of PrintableString (Only Country is PrintableString), and a custom type so we can get sets instead of sequences.

package generator

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/asn1"
    "encoding/pem"

)

var (
    oidCountry            = asn1.ObjectIdentifier{2, 5, 4, 6}
    oidOrganization       = asn1.ObjectIdentifier{2, 5, 4, 10}
    oidOrganizationalUnit = asn1.ObjectIdentifier{2, 5, 4, 11}
    oidCommonName         = asn1.ObjectIdentifier{2, 5, 4, 3}
    oidLocality           = asn1.ObjectIdentifier{2, 5, 4, 7}
    oidProvince           = asn1.ObjectIdentifier{2, 5, 4, 8}
)

type ASNUTF8String struct {
    Type  asn1.ObjectIdentifier
    Value string `asn1:"utf8"`
}

// AnySET is an interface{} slice; SET is required by asn.1 to generate it
// as a set instead of a sequence.
type AnySET []interface{}

Then, to the implementation. The CSR requires a private key, which in my case was storad in a PKCS#12 container. Reading it is easy using Go’s crypto packages. One of the CSR’s Organizational Unit values and the Common Name is provided through arguments, together with two byte slices containing the private key, and its password. Error checks were elided to keep the example small.


func GenerateCSR(private, password []byte, commonName, OU string) ([]byte, error) {
    block, _ := pem.Decode(private)

    var rawBlock []byte
    var key *rsa.PrivateKey

    rawBlock, _ = x509.DecryptPEMBlock(block, password)
    key, _ = x509.ParsePKCS1PrivateKey(rawBlock)

    attributes := []AnySET{
        {ASNUTF8String{Type: oidCommonName, Value: commonName}},
        {ASNUTF8String{Type: oidOrganizationalUnit, Value: OU}},
        {ASNUTF8String{Type: oidOrganizationalUnit, Value: "Another OU Value"}},
        {ASNUTF8String{Type: oidOrganizationalUnit, Value: "Yet another OU Value"}},
        {ASNUTF8String{Type: oidOrganization, Value: "Organization Name"}},
        {ASNUTF8String{Type: oidLocality, Value: "Locality Name"}},
        {ASNUTF8String{Type: oidProvince, Value: "Province"}},
        {pkix.AttributeTypeAndValue{Type: oidCountry, Value: "Country Code"}},
    }

    attrBytes, _ := asn1.Marshal(attributes)
    template := x509.CertificateRequest{
        RawSubject: attrBytes,
    }

    csrBytes, _ := x509.CreateCertificateRequest(rand.Reader, &template, key)
    return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}), nil
}

Without this, the order was based on the Subject type field order, which was considered invalid by our CA.