ARI: How do you get the DER-encoded CertID ASN.1 sequence from x509.Certificate in Go?

Hi everyone,

First, congrats to Let's Encrypt for deploying a new feature (ARI) into production. Let's give it a shot!

I'm adding ARI support to ACMEz, which is used by CertMagic and Caddy. The draft spec says:

   The full request URL is computed by concatenating the renewalInfo URL
   from the server's directory with a forward slash and the base64url-
   encoded [RFC4648] bytes of a DER-encoded CertID ASN.1 sequence
   [RFC6960].

My current API design takes in a decoded *x509.Certificate. I assume the SerialNumber field (big.Int) is the "CertID" I'm looking for (the post I link to below also refers to code in Boulder that calls it "Serial")?

However, we're apparently supposed to submit the ASN.1 bytes -- in that case should I use the Raw field? How do I extract only the relevant bytes? :face_with_spiral_eyes:

I found this post today:

which was helpful in showing me what decoded structure I'm looking for, but I'm hoping someone with experience in Go can shine a light on things to make sure I get it right.

I probably should actually learn the ins and outs of ASN.1 at some point (I know LE has a great explainer on their blog). :slight_smile:

Thanks!

4 Likes

Maybe not that useful for your case but here's how we do it in Certify The Web for our ARI checks: certify/CertUtils.cs at development · webprofusion/certify · GitHub

This leads to bc-csharp/CertificateID.cs at 830d9b8c7bdfcec511bff0a6cf4a0e8ed568e7c1 · bcgit/bc-csharp · GitHub

1 Like

Hang on a sec, I just noticed the reference to RFC 6960 in the quoted spec. When I search in the doc for "CertID" I find a grammar:

CertID ::= SEQUENCE {
   hashAlgorithm           AlgorithmIdentifier,
   issuerNameHash          OCTET STRING, -- Hash of issuer's DN
   issuerKeyHash           OCTET STRING, -- Hash of issuer's public key
   serialNumber            CertificateSerialNumber }

where this is used in an OCSP request. Huh... CertMagic already staples OCSP so we have code that makes a request. What does it look like, I wonder?

So I go to our code that does it, but we call a third-party package function, ocsp.CreateRequest(), which takes in an *x509.Certificate and returns []byte.

Huh. So what does that function do?

It returns a structure that looks very familiar now:

	req := &Request{
		HashAlgorithm:  hashFunc,
		IssuerNameHash: issuerNameHash,
		IssuerKeyHash:  issuerKeyHash,
		SerialNumber:   cert.SerialNumber,
	}
	return req.Marshal()

Soooo... am I basically just using an OCSP request?

Edit: Just saw the reply above while typing this. Thanks for the links, I'll check out that C# code and see what I can learn from it.

2 Likes

Yes, but then base64-encoded. :slight_smile:

5 Likes

You can think of ASN.1 as like JSON in a world where people cared a ton about formal specifications and not having different serializations that are canonically equivalent to each other. It is perhaps a world in which people actively disagreed with Jon Postel about "be liberal what you accept", and wanted interoperability problems and type confusions to be noticed early and cause up-front failures. In this world, ever trying to write your own JSON by hand (or view JSON in a text editor) is considered crazy because your human subjectivity might cause you to misinterpret a data type somewhere. The specifiers have also provided further insurance against reading or writing serialized data by hand by making the serialization format genuinely hard for humans to deal with.

Therefore, you would have a habit of always encoding and decoding using a serialization library, and basically never trying to view or understand serialized data any other way¹. However, because of the formal specification thing, the serialization library's data types may or may not match directly with your programming language's data types, so it may be required in some cases to provide some kind of explicit type indications so that people using a different programming language will be sure of what was meant.

¹ although if you do look at a lot of it, you may start to notice patterns anyway, like that AQAB in PEM-encoded RSA public keys...

6 Likes

Thanks, that's great to know.

Lovely, the ocsp package does not export just the certID struct that I need to marshal... :grimacing: Guess I'll be copying a LOT of code...

For anyone following along at home, it's this little bugger:

https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.7.0:ocsp/ocsp.go;l=330-337;drc=018c28f8a114a06529db797a334e535135fa601d

:upside_down_face:

Got it!

With a lot of duplicated code from x/crypto/ocsp. :frowning:

Issue opened upstream with Go to export some things:

PS. The spec says:

   The ACME Server MAY restrict the hash algorithms which it accepts
   (for example, only allowing SHA256 to limit the number of potential
   cache keys); if it receives a request whose embedded hashAlgorithm
   field contains an unacceptable OID, it SHOULD respond with HTTP
   status code 400 (Bad Request).

I see that Let's Encrypt supports SHA-256. Is there a way to know which hashes are supported?

I like "trial and error" [but I'm sure there's a faster way(s)].
Thou, it can sometimes be more complete/correct than what's been advertised/expected/defined.


Just for giggles:
What's a manual?
Back in my day... everything was done manually.
We had to use chisels on stone tablets to code - LOL

4 Likes

I just haven't found it documented. I guessed SHA-256 was supported based on the spec. I just am wondering how to tell users of my code which hash to use.

2 Likes

A real answer is coming...

3 Likes

If this might be helpful, this is how the boulder tests generate an ARI request:

Basically:

  1. Generate a real OCSP request, and re-parse the DER returned by the stdlib
  2. Extract the IssuerNameHash and IssuerKeyHash from that object
  3. Put those, plus the Serial and the AlgorithmIdentifier, into a custom CertID object
  4. Marshal it to DER, and base64 encode that

This would definitely be easier if the crypto package exposed CertID directly, but it's good enough for our purposes for now.

5 Likes

Let's Encrypt only supports SHA256 for ARI:

The spec does not include a general way to query for what hashes are supported, but neither does OCSP / RFC6960.

5 Likes

Cool, thanks. I ended up just extracting the relevant code from the ocsp package... but I'll keep this as reference since this approach may be useful too!

2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.