Golang, x.509 certificates and otherName
In the wonderful world of infrastructure, there comes a time when you will need to run commands on a remote server. Be it because you need to bootstrap your agent on some remote VM or bare metal machine, or because you need to do some post-deployment configuration, using an Ansible playbook.
For the most part, you’ll probably want to use SSH to run those remote commands.
SSH has become ubiquitous and is even available on Windows Server 2019, which means that you can use the same tooling to run your scripts on both Linux and Windows. However, SSH is not available in older versions of Windows Server. At least not without resorting to third party implementations.
The good news is, you don’t really need SSH on Windows. Windows has had remote access capabilities since Windows Server 2008 via Windows Remote Management (WinRM). This nifty little service is for all intents and purposes, the SSH equivalent for Windows. It supports various authentication mechanisms (Basic, Digest, Kerberos and Certificate), and can be used over HTTPS. Like SSH, you can use it with password-less authentication, but instead of using a public/private key-pair, WinRM requires an x509 certificate.
In this article, we’ll be looking at how to generate a valid x509 client certificate, which can be used to authenticate against WinRM, using Go.
Great! Golang has a x509 package!
Indeed it does. But here is the thing. In order to use certificate authentication with WinRM, the x509 certificate we generate must have the following:
- The client auth extended key usage
- The User Principal Name (UPN) subjectAltName extension
UPN is a subjectAltName named type that normally gets added using the otherName field. Se RFC5280 for a detailed explanation on the structure of an x509 certificate, and in particular, the SubjectAltName extension.
The problem is, that the golang x509 package does not seem to have a way to use the otherName field of the subjectAltName extension. While it does allow us to add IP addresses, DNS names, email addresses and URIs, it does not have an easy way for us to add what we need, using otherName. It’s not impossible, just not very well documented.
But why do we need the UPN named type?
The UPN named type is used by the WinRM service to map an x509 certificate to a username on the system. Think of it as placing your public key in $HOME/.ssh/authorized_keys on a linux machine. So this extension is absolutely necessary to make things work.
Okay, how do we add it?
The UPN named type is described by Microsoft as follows:
Subject Alternative Name = Other Name: Principal Name= (UPN). For example:
UPN = user1@name.com
The UPN OtherName OID is : "1.3.6.1.4.1.311.20.2.3"
The UPN OtherName value: Must be ASN1-encoded UTF8 string
So we know it is a subjectAltName named type that needs to be specified as part of otherName. To begin with, let’s have a look at all the moving parts.
RFC5280 describes subjectAtlName as a sequence of GeneralName structures:
SubjectAltName ::= GeneralNames
GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
GeneralName ::= CHOICE {
otherName [0] OtherName,
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 }
The numbers you see in brackets correspond to the asn.1 tags for each of the supported SAN extensions. As of this writing, the golang x509 package supports tags 1, 2 , 6 and 7. But we need the otherName value, which in the RFC is described as follows:
OtherName ::= SEQUENCE {
type-id OBJECT IDENTIFIER,
value [0] EXPLICIT ANY DEFINED BY type-id }
This means that otherName will be comprised of a structure with two fields:
- The OID of the named type we want to add
- A nested data structure which will hold data in a format described by the OID
In our case, we want to add a UPN, which as we have seen, has an OID of 1.3.6.1.4.1.311.20.2.3, and the value is an UTF-8 encoded string, in the form of an email address. You can add any other named type you wish. You are not restricted only to UPN.
Great! Now that we know what we need to add, time to build our structures.
Defining our structures
We know that the subjectAltName extension is a sequence of GeneralNames, each with its own tag. We only care about otherName, which has a tag of “0”. And we know that otherName is a sequence made up of an OID and a value. Finally, we know that the UPN is a UTF-8 encoded email address. That means we need to define three structures:
- One for UPN
- One for OtherName
- One for GeneralNames
So in golang all of that would look something like:
// UPN type for asn1 encoding. This will hold
// our utf-8 encoded string.
type UPN struct {
A string `asn1:"utf8"`
}
This is our UPN definition. The asn.1 tags are important here. They will dictate how these structures will be ultimately serialized. For the UPN, it’s a UTF-8 string.
Next comes the otherName structure:
// OtherName type for asn1 encoding
type OtherName struct {
OID asn1.ObjectIdentifier
Value interface{} `asn1:"tag:0"`
}
So we have an OID that tells us what named type we are adding, and the Value field is an interface because different OIDs will have different syntax and semantics. The tag of that value needs to be “0”.
Finally we have the GeneralNames sequence, which is essentially what subjectAltName is:
// GeneralNames type for asn1 encoding
type GeneralNames struct {
OtherName OtherName `asn1:"tag:0"`
}
We only add otherName here, because that is all we care about. You can add the rest if you wish, but for the remainder of this post, we’ll only focus on otherName.
Now that we have our data structures, let’s generate a certificate. We’ll create a self signed certificate, but you can generate a CSR if you wish and have it signed by the CA of your choice. The steps are almost identical.
First, we need a private key:
// Generate a private key. We'll go with ecdsa, but you can
// use RSA as well.
root, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatal(err)
}
Next, we create our subjectAltName extension:
// This is where we create the UPN data structure, and marshal
// it into an asn1 object.
upnExt, err := asn1.Marshal(GeneralNames{
OtherName: OtherName{
// init our ASN.1 object identifier
OID: asn1.ObjectIdentifier{
1, 3, 6, 1, 4, 1, 311, 20, 2, 3},
// This is the email address of the person we
// are generating the certificate for.
Value: UPN{
A: "johnDoe@example.com",
},
},
})
if err != nil {
log.Fatal(err)
}
We’re creating a new instance of a GeneralNames{} structure, with OtherName constructed to represent a proper UPN. Using the golang asn1 package, we’re marshaling that structure to asn.1.
Then we construct our subjectAltName extension, and set the value to the asn1 encoded UPN we generated above:
// Finally, we create a new extension with
// the OID 2.5.29.17 (SubjectAltName), and set the
// marshaled GeneralNames structure as the Value
//
// http://oid-info.com/get/2.5.29.17
extSubjectAltName := pkix.Extension{
Id: asn1.ObjectIdentifier{2, 5, 29, 17},
Critical: false,
Value: upnExt,
}
We create our certificate template:
now := time.Now()
// 1 day ago. To account for any time skew.
notBefore := now.Add(time.Duration(-24) * time.Hour)
// 1 year
notAfter := notBefore.Add(8760 * time.Hour)
// Generate a serial number
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
log.Fatalf("failed to generate serial number: %s", err)
}
// Define the x509 template
certificateTemplate := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Example Org"},
CommonName: "John Doe",
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
// Add subjectAltName
ExtraExtensions: []pkix.Extension{extSubjectAltName},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
And finally, generate the certificate:
// generate the self signed certificate
derBytes, err := x509.CreateCertificate(
rand.Reader, &certificateTemplate,
&certificateTemplate, &root.PublicKey, root)
if err != nil {
log.Fatal(err)
}
You can dump the private key and certificate to files, and use them. You can find all this code put together in this github gist. Feel free to grab it.
Let’s test it out. Grab the code from the gist and run it. It will generate two files (key.pem and cert.pem):
gabriel@rossak:/tmp/ssc$ go run gen_cert.go
gabriel@rossak:/tmp/ssc$ ls
cert.pem gen_cert.go key.pem
Let’s check that the UPN was properly added. We’ll use openssl to parse the asn.1 structures:
gabriel@rossak:/tmp/ssc$ openssl asn1parse -i -dump < ./cert.pem
...... removed for clarity .......
307:d=4 hl=2 l= 41 cons: SEQUENCE
309:d=5 hl=2 l= 3 prim: OBJECT :X509v3 Subject Alternative Name
314:d=5 hl=2 l= 34 prim: OCTET STRING
0000 - 30 20 a0 1e 06 0a 2b 06-01 04 01 82 37 14 02 03 0 ....+.....7...
0010 - a0 10 0c 0e 67 61 62 72-69 65 6c 40 72 6f 73 73 ....johnDoe@exam
0020 - 61 6b ak
350:d=1 hl=2 l= 10 cons: SEQUENCE
352:d=2 hl=2 l= 8 prim: OBJECT :ecdsa-with-SHA256
...... removed for clarity .......
Great! We have our subjectAltName. Let’s see if the UPN is properly set. If we look carefully at the output of the above command, we see that the data structure we are interested in is writen at offset 314. So lets parse that:
gabriel@rossak:/tmp/ssc$ openssl asn1parse -i -dump -strparse 314 < ./cert.pem
0:d=0 hl=2 l= 32 cons: SEQUENCE
2:d=1 hl=2 l= 30 cons: cont [ 0 ]
4:d=2 hl=2 l= 10 prim: OBJECT :Microsoft User Principal Name
16:d=2 hl=2 l= 16 cons: cont [ 0 ]
18:d=3 hl=2 l= 14 prim: UTF8STRING :johnDoe@example.com
All is well! We should now be able to use this certificate for password-less authentication with WinRM.