ACME Protocol (RFC 8555) and EAB

Background (so I don't get mobbed..). I am a developer and working on implementing / writing an ACME client (very isolated purpose) for a couple of environments where software written in-house is preferred or audited code. I am actually trying to get EAB to work with another CA, but using documentation and reverse-engineered code from other clients and documentation from Let's Encrypt. I work for the UT-Austin, and are not allowed to use Let's Encrypt for production systems. We are required to use Sectigo (the official CA for UT) for production.

Since I have a lot of success utilizing REST APIs, Python, and such to request and retrieve certificates, as those only require that you have the appropriate credentials for the REST API calls, and permission behind the scenes to request certs for specific domains. I can submit a CSRs and get certificates, no problem.

Since ACME has a little more granular control over domains and such within the management tool for our CA, all that I can manage (without needing to get our ISO involved) I I thought I would try my hand at ACME. It has proven to be quite a challenge!

I am making progress, but stuck on how to implement a External Account Binding. Lots of clients/tools out there, but not many of them support EAB. I have my CA's EAB provided HMAC, KeyID, URL and such. I have successfully obtained a Nonce, and fetched the ACME directories.

In RFC8555; section 7.3.4. External Account Binding.. It has the following structure:

   {
     "protected": base64url({
       "alg": "ES256",
       "jwk": /* account key */,
       "nonce": "K60BWPrMQG9SDxBDS_xtSw",
       "url": "https://example.com/acme/new-account"
     }),
     "payload": base64url({
       "contact": [
         "mailto:cert-admin@example.org",
         "mailto:admin@example.org"
       ],
       "termsOfServiceAgreed": true,

       "externalAccountBinding": {
         "protected": base64url({
           "alg": "HS256",
           "kid": /* key identifier from CA */,
           "url": "https://example.com/acme/new-account"
         }),
         "payload": base64url(/* same as in "jwk" above */),
         "signature": /* MAC using MAC key from CA */
       }
     }),
     "signature": "5TWiqIYQfIDfALQv...x9C2mg8JGPxl5bI4"
   }

This is where major confusion sets in, as there are multiple references to "signature", "jwk", "MAC", and "kid". I have yet to find any concrete documentation either from our CA or other clients (reviewing code) or documentation from other CAs.

In the example above, does anyone have idea what those values need to be for EAB? Has anyone attempted to create and submit EAB to a CA? What was the structure you used? ..and sheepishly ask .. maybe have any experience with Sectigo EAB and ACME?

I have code for generating private/pub keys, and encrypting the JWK header, the comment about "same as in 'jwk' above" -- what does that mean? JWK when encrypted are SHA256 hashed base64 encoded json structures.. got that. So is that called an account key? So much terminology so little explanations.

These are both standard JWS objects. The inner JWS is computed with the HS256 key from the CA, and the outer one uses the account key.

It sounds like you're trying to implement JWS from scratch. If so make sure you understand rfc7515 first, 8555 alone isn't enough.

4 Likes

That does depend on the CA; if they have some external verification method then yes, you don't need to do any challenges. That is not universally true.

4 Likes

So from scratch .. kinda.. going back to the requirement of in-house developed code. I am using a lot of examples.. so I am trying to piece this together. Thanks for the RFC7515 note, it looks familiar.. as I was lead down that path. Back then .. it did not make much sense, but now that I have been playing around with code now for a while.. it looks much more understandable. Thanks for the tip!

1 Like

From what I have read, if I already have the domains allowed under a specific ACME account (specified by HMAC and KeyID) -- which has to be updated to allow or remove domains.. then yes, no additional challenges, which is what I am striving for.. getting results like I do with REST API.

What language is your system written in? I don't know what your audit process is but would auditing an existing open source client be quicker and more cost-effective that writing your own?

4 Likes

What examples are you using?

It is worth looking at acme-tiny (GitHub - diafygi/acme-tiny: A tiny script to issue and renew TLS certs from Let's Encrypt), which is written in Python and very short. This is not a full-featured client, but the stripped-down features mean the code is easy to read and follow. I referenced it a lot when building my first client.

I open-sourced our current client a few years back. Two of the files may be helpful to you:

  • acme_v2.py implements the ACME actions and includes extensive samples and docs from the ACME spec

  • cert_utils.py implements all the cryptographic functions in pure-python, with fallbacks to the same routines as openssl shell commands.

3 Likes

Thank you. Yes, acme-tiny was the best one I found so far, a little compressed and had to expand it a bit to understand what was going on.. but it did not support EAB. I have been in touch with the author, and we may work together to add EAB.

A lot of my challenges is that our CA (Sectigo) has supposedly taken a lot of the work of issuance with ACME.. but their documentation is lacking significantly.. just pointing to pre-fab clients (which does work for 80-90% of the people) and RFC8555 for their documentation. Does not help me much.

This is also a learning experience as well. Since we are trying to automate everything about certificates with all of our platforms (many are appliances, some blackbox, some switches, java applications, some IBM mainframes, etc..)

I have conquered the REST API, just need to conquer the ACME protocol.

I appreciate any assistance. I think I have the process down, just need to figure out the structure for EAB, I think I have the encryption down. So I am not trying to re-invent the wheel, literally just the submit CSR and get certificate. No need to revocation, or other functions.

1 Like

EAB is just a tiny part of the ACME protocol and is usually only relevant at account registration.

4 Likes

Not really a language barrier. There are plans for code in Python (plenty of examples), PowerShell, bash (good examples), possibly Tcl, Natural (mainframe), etc.. whatever is needed to automate the process for anyone on campus doing it manually for whatever platform, appliance, etc..

So my challenge has been EAB. Just need to figure that out. I think I have the order of steps correct, just need to decipher and build the appropriate JSON structure, encrypt/encode the correct parts and send it to the CAs ACME endpoints.

Although LetsEncrypt doesn't support it on Boulder, it is implemented in Pebble for client development. Have you looked through these issues and commits?
Search · eab · GitHub

3 Likes

I have looked through it. I do not understand Go very well.. but still digging through it. I heard it does support EAB.. so need to figure out how they do it, and what they expect.. might help me. However, just how similar is that going to be compared to Sectigo... do not know, but it might get me closer to understand the structure.. and what needs to go into the fields..

Honestly, you don't need to understand Go at all - just how to install and run Pebble.

Once it's running, you'll have a spec compliant test server at your disposal and can just iterate your client's EAB code based on trial and error against the pebble endpoint.

In terms of compliance with Sectigo – you should be mostly compliant. The ACME spec is very clear about mandating certain elements, but leaves others open to interpretation. Pebble tries to interpret those elements differently than Boulder (LetsEncrypt's production system) when it can. Because of these divergences, it's possible to have a spec-compliant client that is only compatible with Sectigo, Boulder or Pebble – but after developing a client against Pebble you should be able to quickly adjust it to support any known ACME server.

Examples:

4 Likes

The kid value in the EAB protected header should be the literal Key ID string provided by Sectigo. You've got the alg value correct assuming Sectigo isn't supporting different HMAC types like HS384 or HS512. The url value is whatever gets returned by the newAccount field from the directory endpoint. Your payload pseudo-value also looks correct.

It's unclear from your original post, but the EAB signature for that internal EAB JWS should be based on the compact form of the protected header and payload. So like:

signature = HMACSHA256( base64url(<protected>) . base64url(<payload>) )

Plenty of clients out there work with EABs because all but two of the available free CAs require using them. I haven't directly touched Sectigo's ACME implementation, but some of my (Posh-ACME) users have.

5 Likes

@rmbolger My apologies.. that JSON structure is directly from the RFC8555 documentation (unedited). It is not mine.. I am trying to come up with my own. Thank you for Posh-ACME.. great coding.. but you do everything ACME (still digging through it).. so a lot of code.. and I am trying to simplify that (only need a couple of functions). If others are supporting EAB.. I can not find it in their code.

Sectigo documentation is spare at best regarding this. It just says refer to RFC8555. So I am assuming ES256 is a valid algorithm to use (will find out soon).

So the SHA256 usually requires a "key" to encrypt the value. I looked around in google and could not find a way of using OpenSSL to encrypt a value without specifying a private key file. Unless you are signing with the HMAC .. and then there is an -HMAC key parameter in OpenSSL.

I know the HMAC, KeyID and ACME URL from Sectigo.


ACME URL https://acme.sectigo.com/v2/InCommonRSAOV
External Account Binding

  • Key ID yimZYG...oA0Dw (22 characters)
  • HMAC Key v55Q8z-z.... 8T873k4w (86 characters)

  • inner "signature" says it is the MAC field from CA.. so that make sense .. substitute in the HMAC from above. √
  • outer "signature" -- Is that what you are saying from above? I read about including a public key from a generated private/public key pair. So, Sectigo does not give us access to any private key.. and no where in the Sectigo EAB configuration (web based) does it mention a public or private key.
    SO MY thought here is ... I am generating a pub/private key pair in my script. I can sign a JSON structure (as I have seen in acmetiny, and others) using SHA256 and a private key.. So is that what is needed here? I mean .. in the documentation somewhere.. I read that the ACME server stores the public key that it uses to decode future messages? Sounds right. Thoughts?

In the OUTER "protected" section...

  • alg = ES256
  • url = "https://acme.sectigo.com/v2/InCommonRSAOV/newAccount"
  • nonce <-- not sure why this is here, as just above it in §7.3.4 of RFC8555, it says "The 'nonce' field MUST NOT be present"
  • JWK <-- it says account key. There is no clarification of what an "account key" is? A computed value (maybe an actually JWK structure that is encrypted or encoded?)

In the INNER "protected" section..

Following that .. there is a "payload" ..

  • It has "same as in "jwk" above" in the comments? So repeat the above JWK in the outer "protected"?

As the author of Posh-ACME .. can you refer me to section of your code ..where you are do all the above in PowerShell? Files and line numbers? I can go look at that. I can also go look on my own as well...

1 Like

Sorry, I misunderstood and thought you already had a grasp on constructing ACME messages and only needed help with the EAB specific bits. It might be helpful to focus on getting basic non-EAB account creation working with either Pebble or the Let's Encrypt staging environment before trying to add the EAB bits. The HMAC signature process is unique to EAB and uses that HMAC Key value provided by Sectigo. Normal ACME signatures are based on the ACME account's RSA or ECDSA private key which the client usually generates when creating a new account.

The bulk of the new account process code in Posh-ACME resides in New-PAAccount.ps1 and Invoke-ACME.ps1 both of which rely on New-Jws.ps1 to construct the inner EAB JWS and the outer ACME JWS.

Yes, the key pair you generate becomes effectively your authentication method for all future communication with the ACME server. During the new account registration process, you're basically sending the public key to the CA for storage so that when you send later messages signed by the private key, they can verify the message is authentic. The EAB process takes it a step further and more or less pairs your generated key with your existing non-ACME account at the CA. Once the account is registered, the EAB key info can be discarded (though some CAs technically allow you to re-use them for additional ACME accounts).

The hashing algorithm you use on the outer JWS is dependent on the type of key pair you generated and also corresponds with the value of alg. All RSA keys and P-256 ECDSA keys use SHA256, but P-384 keys use SHA384 and P-521 keys (if supported) use SHA512.

The nonce is always required in the outer JWS. 7.3.4 is referring to the EAB inner JWS. The JWK is just JSON as specified in RFC 7517 representing the public key of the keypair you generated . See ConvertTo-Jwk.ps1 for Posh-ACME's implementation.

The alg here would usually be HS256, not ES256 because you're signing it with the HMAC key rather than your main ACME asymmetric key pair.

Yes, exactly.

3 Likes

All.. I think with the help from everyone -- I may have figured it out. Unfortunately, I have to work on it next week - personal hiatus for a couple of days. I also need to remove kludgy calls to command-line execution of openssh to generate keys, perform hashing and encryption. I have found BouncyCastle (used by Posh-ACME and others) so I am implementing that.

I will give y'all an update soon! I really appreciate all the help!

1 Like

I have to say, I'm kind of confused about what kinds of auditing/security requirements would allow you use BouncyCastle but not a more-comprehensive library that implements ACME for you?

4 Likes

I had exchanged a few messages with the OP and wanted to share something for future readers who have similar problems: the EAB feature in the RFC is essentially a variant of the more-widely implemented and documented "Account Key Rollover" feature. If someone is having problems with understanding/implementing EAB – they can reference rollover examples which are incredibly similar. There are many differences due to different nesting of the payloads and different fields, but they both prove ownership of two keys via cryptography.

My guess is they subscribe to a service that audits popular libraries for security concerns and BouncyCastle is in there, but no ACME clients that implement EAB are. In my experience, organizational policies are a bit stricter on libraries that make public internet calls as well (e.g. you might be able to use an unaudited library that generates ACME payloads but not one that transmits them).

5 Likes

I apologize. I have every intention in responding and answering some of the questions. Poking this so it does not close down. I will get back with you all very soon.

1 Like