certificates
certificates copied to clipboard
External account binding
What would you like to be added
ACME External Account Binding (EAB) support. RFC 8555 describes how the ACME server should support it:
When a CA receives a newAccount request containing an "externalAccountBinding" field, it decides whether or not to verify the binding. If the CA does not verify the binding, then it MUST NOT reflect the "externalAccountBinding" field in the resulting account object (if any). To verify the account binding, the CA MUST take the following steps:
Verify that the value of the field is a well-formed JWS
Verify that the JWS protected field meets the above criteria
Retrieve the MAC key corresponding to the key identifier in the "kid" field
Verify that the MAC on the JWS verifies using that MAC key
Verify that the payload of the JWS represents the same key as was used to verify the outer JWS (i.e., the "jwk" field of the outer JWS)
If all of these checks pass and the CA creates a new account, then the CA may consider the new account associated with the external account corresponding to the MAC key. The account object the CA returns MUST include an "externalAccountBinding" field with the same value as the field in the request. If any of these checks fail, then the CA MUST reject the newAccount request.
I believe a user-configurable hook or interface will be needed too, so that we can verify that the EAB key originated from our service, and not somewhere else.
Why this is needed
It allows ACME servers to bind or associate ACME accounts with accounts on an external system.
In my case, I'm building a system that has user accounts, and I want to forbid anyone but registered users from using this ACME server. So I would require clients to provide EAB -- and I'd want to ensure they are credentials we provided to them, so we know which account in our own DB they are associated with.
Happy to talk about this in more detail when you get around to it. Thanks again!
I want to support EAB. The difficult question seems to be: how are the EAB symmetric keys managed? Are they managed by step-ca or by some external service?
I wish EAB was spec'd more loosely to use a bearer-token for binding instead of signing over the pubkey, as it'd be easier to tie the binding back to something like an OAuth OIDC or SAML identity provider or cloud instance identity documents without having new credentials to manage (the binding could be achieved by presenting an audience-addressed signed assertion issued from an IdP). It's also sort of lame that EAB keys must be symmetric keys. But I digress.
I can think of two ways to achieve what you're describing:
The easy way:
- In ACME provisioner configuration, you can enable external account binding and specify an EAB verification webhook to enable EAB (
step-cawould then send"externalAccountRequired": trueduring account creation) - While handling a
newAccountrequest,step-cawould verify the outer JWS as usual. It would POST the inner JWS (the EAB assertion) to the EAB verification webhook and rely on the webhook response for verification - Assuming verification succeeds,
step-cawould compare the JWK form the inner & outer JWS to ensure that they match (as specified). If they do, an account would be created and the ACME database would reflect an EAB association (i.e., it would record thekidof the EAB assertion or would record the full assertion).
This would effectively outsource all of the key management to an external service. This would be easier for us, but users would need to build their own custom infrastructure for it to be useful.
There are some variants on this "easy way":
- Instead of a webhook, we could use a plugin architecture
- Instead of POSTing the EAB assertion and expecting a verification response, we could request the symmetric key by
kidand verify the JWS ourselves (this would simplify the webhook, but we'd end up shunting symmetric keys around which is generally bad practice and would require the CA to authenticate to the webhook somehow)
The harder way:
- In ACME provisioner configuration, you can enable external account binding and specify an EAB enrollment mechanism along with any parameters required to configure the specified mechanism. As a strawman, let's assume the only enrollment mechanism is OAuth OIDC. More generally, most of our "provisioners" could also be used for "EAB enrollment".
- A proprietary API would be made available for clients to authenticate using the specified mechanism and obtain EAB credentials (a symmetric key for signing an EAB assertion).
step-cawould need to store these credentials in its database, and would associate each key with an authenticated identity. - Additional proprietary APIs would be made available for EAB credential enumeration and revocation. An authenticated user should be allowed to enumerate & revoke their own credentials. An administrator should be allowed to enumerate & revoke credentials for any user.
- During a
newAccountflow,step-cawould challenge for an external account binding and could verify the signed EAB assertion itself using its internal credential database.
This design has step-ca doing more work. It's less flexible, but is more turnkey for what I assume are the most common use cases. It also has the advantage of exposing user identity, and potentially other metadata, to step-ca. That metadata could be useful down the road to make more granular authorization decisions. Of course, this same metadata could be made available via another webhook if we implement "the easy way" described above.
To start, it sounds like you're just interested in establishing a "perimeter" around an ACME server: authentic users can obtain certificates, but random anonymous users can't. Do you anticipate more granular authorizations down the road?
Any thoughts on which of these approaches would work best for you (or do you have any other ideas)? I'm also interested in any prior art in this space. I know Sectigo has an ACME implementation with external account binding. I wonder how theirs works (a quick Google didn't turn up any docs).
@mmalone I definitely agree on the sentiment about EAB. But it is what it is I guess.
To start, it sounds like you're just interested in establishing a "perimeter" around an ACME server: authentic users can obtain certificates, but random anonymous users can't. Do you anticipate more granular authorizations down the road?
Since my call with your team earlier this week I think I have actually determined a way to do what I need to do without EAB or any modifications to the current ACME server! (Assuming my assumptions hold, which I think they will. I'll know better after I write the code.) This should be great news. :smiley: It should mean, if my design is correct, that no changes are needed in your libraries!
Here's how it'll work in my specific case: Assuming that the ACME account is validated at every request for a certificate, all I really need to do is ensure that accounts cannot be created except by authorized users. I should be able to do this by gatekeeping the new-account endpoint. This can be done with a outer middleware handler or even a reverse proxy. Since users will sign up on our website, our site can generate their account key pair purely client-side in the browser (using webcrypto APIs), and then make the new-account request from the browser. I can authenticate that request since it comes from our own site and IP address, and allow it. Then I'll show the user their private key and tell them to save it. After that, they can configure their ACME clients with that key and they won't have to make a new account. The clients will use ACME's account lookup endpoint to find the account based on the key.
This approach doesn't require EAB and still allows me to control who can use my ACME server. All it requires is a thin reverse proxy or outer middleware handler to guard the new-account endpoint. And it should be trustworthy since the account key is generated client-side by (a partial implementation of) an ACME client. The server only ever sees public keys. I will begin implementing this next week and let you know if I hit any bumps with the ACME server.
Do you happen to know: can an ACME client craft a request using an account key created with one ACME server, on another ACME server? I imagine an ACME server will reject account keys it does not recognize, right?
However I still think EAB will be an important feature for Smallstep going forward, so I'm happy to collaborate with the design discussion.
Any thoughts on which of these approaches would work best for you (or do you have any other ideas)?
Appreciate your well-thought-out ideas! So far, I'm leaning toward the "easy" way with the webhook. I think it's reasonable for integrators of EAB to be able to verify the EAB themselves, without having to worry about details of the ACME protocol. This seems like a good way to go, and should be very flexible, I'd imagine. However, my needs are pretty simple as I don't foresee needing myself to integrate with OIDC or SAML or anything crazy. In my head, any systems I'd personally build if they need EAB would more or less do a database lookup.
I'm also interested in any prior art in this space. I know Sectigo has an ACME implementation with external account binding. I wonder how theirs works (a quick Google didn't turn up any docs).
I will point their devs to this thread, I think it'd be neat to see what they came up with, if they're willing to share.
Seconding the webhook implementation!
It'd be great to integrating other systems with relatively low overhead.
That said, I have been thinking about this recently.
Looking at what I want as a user, it's restricting the creation of certs by restricting who can create them and/or which certs are created.
As mholt described, the front half of this can be done with a minimal proxy around step-ca. This however doesn't resolve the fundamental issue of being able to filter which certs are created (and it's kind of janky as a misconfiguration could result in security issues).
Looking through RFC 8555, I noticed that the Pre-Authorization endpoint ('/new-authz') isn't spec'd as tightly, just that it:
[it] might have a completely external, non-ACME process for authorizing a client to issue certificates for an identifier.
This makes it pretty easy to have step-ca validate against some allowlist defined by the provider whether it be an accounts contact string, an IP allowlist, or possibly just an authorization header (eg. 'X-STEP-AUTH: foo'). The idea of a provider specific lists where already floated in issue #123.
Maybe I'm misinterpreting the spec or there is some glaring flaw, but it seems to work and fall within the ACME spec.