RFC 8555's External Account Binding is not currently supported by x/crypto/acme while there are multiple certificate authorities that require the feature. (Sectigo, zerossl, etc)
External Account Binding (EAB) occurs in ACME's "new-account" RPC. Within acme.go this is handled by Register()
The public API for this method would not have to change. Notably, the call takes in an Account struct which is a non-wire compatible version of an ACME Account object. Since the normal wire protocol account object contains the EAB, a simple extension of the Account struct seems appropriate. This new field would contain an "EAB struct" that would be easily configurable by end users. The EAB struct can be similarly serialized into an ACME EAB JWS within the existing Register method.
As far as requirements go, the RFC states that
I have seen many client implementations assume HMAC SHA256 is used, though this is not guaranteed by the protocol. [[1](https://github.com/go-acme/lego/blob/abdd2efc72e2de7df28a9ed739fa9afba8de2ab9/acme/api/internal/secure/jws.go#L79), [2](https://github.com/certbot/certbot/blob/ae7b4a1755655b0bf87614ff6eb75531e6c454b0/acme/acme/messages.py#L300)]
The External account binding struct would need to look something like
// ExternalAccountBinding contains the data needed to form a request with
// an external account binding.
// See https://tools.ietf.org/html/rfc8555#section-7.3.4 for more details.
type ExternalAccountBinding struct {
// KID is the Key ID of the symmetric MAC key that the CA uses to
// identify an external account from ACME.
KID string
// Key is the bytes of a symmetric key that the CA uses to identify
// the account. The KID should reference the same key that the CA holds.
Key []byte
// KeyAlgorithm of the JWS. Only the HMAC algorithms are supported
// https://tools.ietf.org/html/rfc7518#section-3.1
KeyAlgorithm string
}
Adapted from @munnerz's existing PR for this functionality.
Looks good to me, thank you. I suspect Account is the right place to put it, but I am not a fan of adding private keys to a type that did not have one before and might get logged. We should probably add a String method to hide the key from formatting.
Given Client has the account key, would it be a better place for the ExternalAccountBinding field?
/cc @rolandshoemaker
Account is probably the correct place for it since the EAB is only needed during account creation and putting it in Client may suggest that the user needs to populate it for all subsequent API calls after Client.Register, which could cause them to persist the key longer than they need to.
Agreed we should probably put something in place to prevent logging the field. I realize now that it's not explicitly called out in the RFC but the best practice should be for EAB keys to be one-time use (such that a EAB key can only be used to associate an external account with a single ACME account) but I suspect there will be implementations out there that will allow an EAB key to be used multiple times, so leaking the key would be bad.
Thank you for the reviews. I agree that additional precautions should be implemented to help protect the key from being accidentally logged.
I think my only other real comment on this would be to replace KeyAlgorithm with a enum instead of a string, since we're only going to be supporting like three things, but ĀÆ_(ć)_/ĀÆ.
Assuming this proposal is in a reasonable state, I will aim to build upon the existing work by munnerz@ with the changes discussed here.
Change https://golang.org/cl/269279 mentions this issue: acme: Add External Account Binding Support
I realise the CL has just landed so unfortunately not the best timing, but Iām quite confused about why the caller should have to specify the MAC algorithm. The RFC says only that āthe CA operating the ACME server needs to provide the ACME client with a MAC key and a key identifierā with no mention of the algorithm.
It seems to me that the algorithm is entirely an internal implantation detail and should not be visible to callers. Furthermore, thereās no compelling reason (that Iām aware of) for callers to want to change this, all of the options are secure, in particular HMAC-SHA256 is a perfectly secure choice here thatās most likely to be compatible across the ecosystem. This really just seems like needless complexity and cryptographic agility that callers will have to understand and deal with (something I do know @FiloSottile has rightly championed against in the past).
The field and constants also provide no documentation about how a caller should pick the algorithm, which at the very least should be rectified. Otherwise I think most choices will be something like: HS256 is the first one so pick that or HS512 is the biggest number so it must be most secure.
Ultimately, is there any real use case where always using HS256 wouldnāt work? If so, how likely are callers to understand and correctly deal with that situation? It seems like thatās the approach used elsewhere and it would both simplify the API, the implementation and ease of use.
I wonder if it could default to HMAC-SHA256 (if field was left empty) and then someone would only change it from the default if they explicitly told to.
Most helpful comment
Assuming this proposal is in a reasonable state, I will aim to build upon the existing work by munnerz@ with the changes discussed here.