-
-
Notifications
You must be signed in to change notification settings - Fork 593
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ACME v2 Finalize order support #3169
Conversation
This commit implements order finalization for the ACME v2 API. In broad strokes this means: * Removing the CSR from order objects & the new-order flow * Adding identifiers to the order object & new-order * Providing a finalization URL as part of orders returned by new-order * Adding support to the WFE's Order endpoint to receive finalization POST requests with a CSR * Updating the RA to accept finalization requests and to ensure orders are fully validated before issuance can proceed * Updating the SA to allow finding order authorizations & updating orders. * Updating the CA to accept an Order ID to log when issuing a certificate corresponding to an order object
ra/ra.go
Outdated
// unexpired authorizations for all of its associated names an error is | ||
// returned. Similarly we vet that all of the names in the order are acceptable | ||
// based on current policy and return an error if the order can't be fulfilled. | ||
func (ra *RegistrationAuthorityImpl) FinalizeOrder(ctx context.Context, req *rapb.FinalizeOrderRequest) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self: I forgot to check that the order isn't expired here. Will address shortly with a fix & a test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up doing this in the WFE.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Phew, big branch! Mostly looks in order though.
*pun not intended but definitely intended once I noticed it
sa/proto/sa.proto
Outdated
@@ -39,7 +39,9 @@ service StorageAuthority { | |||
rpc DeactivateRegistration(RegistrationID) returns (core.Empty) {} | |||
rpc DeactivateAuthorization(AuthorizationID) returns (core.Empty) {} | |||
rpc NewOrder(core.Order) returns (core.Order) {} | |||
rpc UpdateOrder(core.Order) returns (core.Order) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to try and scope our RPCs more narrowly to limit the amount of harm that a breach of any given component can do. For instance, can we change this to "FinalizeOrder", and only allow it to modify the fields involved in finalizing? And reject requests to finalize already-finalized orders?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. That makes sense. AFAICT there shouldn't be a need to change an order after creation except for the status & certificate serial.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jsha So something like
rpc FinalizeOrder(core.Order) returns (core.Order) {}
Where the SA's implementation only pulled out the CertificateSerial
field of the order provided as an argument & then created a model that had that field + status(final)
and used it for an update?
Or would you rather something like:
rpc FinalizeOrder(orderID, certificateSerial) returns (core.Order) {}
to make that limitation more explicit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I implemented the former to keep this PR moving.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rpc FinalizeOrder(core.Order) returns (core.Order) {}
Where the SA's implementation only pulled out the CertificateSerial field of the order provided as an argument & then created a model that had that field + status(final) and used it for an update?
This is fine, though I'd recommend an explicit query UPDATE orders SET status = ? WHERE id = ? AND status = "pending"
rather than using the whole Order model in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I'll make that change shortly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 9496ef5
sa/sa.go
Outdated
ID: *req.Id, | ||
RegistrationID: *req.RegistrationID, | ||
Expires: time.Unix(0, *req.Expires), | ||
Status: core.AcmeStatus(*req.Status), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we switch to a FinalizeOrder
RPC, we can do something like SET status = "final" WHERE status = "pending"
, which will produce the "no order updated" error if the order is not pending.
ra/ra.go
Outdated
func (ra *RegistrationAuthorityImpl) issueCertificate( | ||
ctx context.Context, | ||
req core.CertificateRequest, | ||
acctID int64, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes me pretty nervous that we have acctID and orderID here distinguished only by their order; it's very easy to make a mistake. Maybe we could define internal types, like type accountID int
and type orderID int
, and use those types for these parameters? Then the call site would have to cast appropriately, like accountID(account.ID)
. And if you saw accountID(order.ID)
at a call site it would be more obviously broken.
Also, we should have a consistent ordering for orderID vs accountID in structs and parameters. I think in most places, accountID (regID) is first, with orderID second. Probably we should adopt that idiom here too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both good ideas 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, we should have a consistent ordering for orderID vs accountID in structs and parameters. I think in most places, accountID (regID) is first, with orderID second. Probably we should adopt that idiom here too.
I'm slightly confused by this comment with closer examination. That's the order being used now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checkOrderAuthorizations
had the acct ID / order ID flipped, maybe that's what you meant? (Fixed).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I suspect that's what I meant. Thanks!
wfe2/wfe.go
Outdated
// URL and the ceritificate URL as appropriate. | ||
func (wfe *WebFrontEndImpl) orderToOrderJSON(request *http.Request, order *corepb.Order) orderJSON { | ||
// Convert the corepb.Order.Names back into identifiers for display to the | ||
// user. We do this rather than send back newOrderRequest.Identifiers because |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this sentence may be left over from a previous iteration? There's no newOrderRequest
in this scope, and we use this utility function in both NewOrder and getOrder.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup! You're right. This is some spilled copy pasta 🍝
wfe2/wfe.go
Outdated
} | ||
for i, authzID := range order.Authorizations { | ||
respObj.Authorizations[i] = wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", authzPath, authzID)) | ||
// Convert the corepb.Order.Names back into identifiers for display to the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is taken care of by orderToOrderJSON and can be deleted from this function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I missed this when I made the orderToOrderJSON
function to replace the duplicated code. Thanks!
wfe2/wfe.go
Outdated
// If there are less than 2 fields there can't be both an account ID and an | ||
// order ID so the path is invalid | ||
if len(fields) < 2 { | ||
wfe.sendError(response, logEvent, probs.Malformed("Invalid request path"), nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this and the Malformed below should both be 404's.
wfe2/wfe.go
Outdated
return | ||
} | ||
|
||
// If there is already a certificate serial for this order then it has been |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment doesn't seem to match the check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops. This was leftover from before I remembered the Order status field existed and was determining if an order was finalized or not strictly based on the presence of the certificate serial.
wfe2/wfe.go
Outdated
} | ||
// Assuming a properly formatted CSR there should be two four byte SEQUENCE |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For API v2 we can drop this special handling, since there are no legacy clients that will do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea 👍
This commit normalizes the functions in the RA that accept both an order ID and an accountID such that the accountID is always provided first. To help prevent accidental usage of an orderID as an accountID (or vice versa) dedicated types are introduced that callers must cast to explicitly.
This commit replaces the broader `UpdateOrder` SA RPC with a `FinalizeOrder` RPC. Now only the Order ID and CertificateSerial provided in the request are used for the update. The final status is hardcoded as part of the update. This helps reduces the impact of an RA compromise.
@jsha Ready for another 🔍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First pass, generally looks good, a few comments but nothing major.
One architectural question: Why do WFE -> SA then WFE -> RA for the finalization (i.e. sa.GetOrder then ra.FinalizeOrder) instead of say WFE -> RA -> SA. Is it simply so we can do more error checking at the WFE before dispatching anything to the RA? (This isn't necessarily a bad thing, just wondering)
@@ -187,12 +187,19 @@ func (ras *RegistrationAuthorityClientWrapper) NewOrder(ctx context.Context, req | |||
if err != nil { | |||
return nil, err | |||
} | |||
if resp == nil || resp.RegistrationID == nil || resp.Expires == nil || resp.Csr == nil || resp.Authorizations == nil || resp.Id == nil || resp.Status == nil { | |||
if resp == nil || !orderValid(resp) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think order.CertificateSerial
will be set here so it'll always fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're right, good catch.
@@ -472,7 +472,18 @@ func (sas StorageAuthorityClientWrapper) NewOrder(ctx context.Context, request * | |||
if err != nil { | |||
return nil, err | |||
} | |||
if resp == nil || resp.Id == nil || resp.RegistrationID == nil || resp.Expires == nil || resp.Csr == nil || resp.Authorizations == nil || resp.Status == nil { | |||
if resp == nil || !orderValid(resp) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
order.CertificateSerial
won't be set here either I think?
@@ -954,13 +990,25 @@ func (sas StorageAuthorityServerWrapper) DeactivateAuthorization(ctx context.Con | |||
} | |||
|
|||
func (sas StorageAuthorityServerWrapper) NewOrder(ctx context.Context, request *corepb.Order) (*corepb.Order, error) { | |||
if request == nil || request.RegistrationID == nil || request.Expires == nil || request.Csr == nil || request.Authorizations == nil || request.Status == nil { | |||
if request == nil || !orderValid(request) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
order.CertificateSerial
won't be set here either I think?
if err != nil { | ||
logEvent.Error = err.Error() | ||
return emptyCert, err | ||
} | ||
|
||
// Verify the CSR | ||
csr := req.CSR | ||
if err := csrlib.VerifyCSR(csr, ra.maxNames, &ra.keyPolicy, ra.PA, ra.forceCNFromSAN, regID); err != nil { | ||
if err := csrlib.VerifyCSR(csr, ra.maxNames, &ra.keyPolicy, ra.PA, ra.forceCNFromSAN, int64(acctID)); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This appears to get called twice now? (Once in ra.FinalizeOrder
and once in ra. issueCertificate
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch - if it's already being done in ra.issueCertificate
I'll remove it from ra.FinalizeOrder
.
ra/ra.go
Outdated
// at the time of order creation, but we check again in case the policy has | ||
// changed in the time since the order was accepted. | ||
id := core.AcmeIdentifier{Value: name, Type: core.IdentifierDNS} | ||
if err := ra.PA.WillingToIssue(id); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already checked in VerifyCSR
isn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, it is. I'll remove from here 👍
sa/sa.go
Outdated
err = tx.Insert(reqdName) | ||
if err != nil { | ||
err = Rollback(tx, err) | ||
return nil, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the above assignment and a few below aren't necessary, can just be return nil, Rollback(tx, err)
. Not really required but a general style thing.
if err != nil { | ||
return nil, err | ||
} | ||
// The requested names are stored reversed to improve indexing performance. We |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: there isn't an index on the name column, and we don't (at least in this PR) do a select using that column. Are we planning on doing this in the future at some point? Or can we just store these in their normal form?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I stored the names in reversed order after @jsha suggested it for consistency with issuedNames
.
At the time he said: "I can't think of a strong reason to argue in favor of forward names, and reversedNames gives slightly nicer index locality for subdomains of the same domain."
I think I'd lean towards keeping them reversed to make it easier to exploit an index on the column down the road if we require it. It's tough to land DB migrations in prod so the future-proof approach might be better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I think future proofing makes sense, lack of indexes has definitely bit us down the road in the past. That said if that is why we're doing it we should probably just add the index now, I want to say it'll be faster to remove an index we don't need than add one we do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That said if that is why we're doing it we should probably just add the index now, I want to say it'll be faster to remove an index we don't need than add one we do.
I like that argument. I'm 👍 to adding the index now.
wfe2/wfe.go
Outdated
// bytes on the wire, and (b) the CA logs all rejections as audit events, but | ||
// a bad key from the client is just a malformed request and doesn't need to | ||
// be audited. | ||
if err := wfe.keyPolicy.GoodKey(certificateRequest.CSR.PublicKey); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also checked in the RA via VerifyCSR
.
wfe2/wfe.go
Outdated
idents[i] = core.AcmeIdentifier{Type: core.IdentifierDNS, Value: name} | ||
} | ||
|
||
respObj := wfe.orderToOrderJSON(request, order) | ||
err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, respObj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this not be missing the certificate serial and still have a pending status since ra.FinalizeOrder
isn't mutating order
or am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's correct. The spec PR doesn't say that the finalize POST should return the order with the certificate serial populated. The client is meant to poll the order after POSTing the finalize endpoint so that the Order can be issued asynchronously from the finalize request.
I think we should probably be mutating the order state to "Processing" but this will require an adjustment to the SA's Order RPCs since there isn't a way to update just the status field of an order with the current Order RPCs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe since we are issuing synchronously anyway we should just skip the processing state and change the RA to return the certificate serial and the order in the valid state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will want to switch to async issuance pretty soon as part of the SCT embedding work, so we should be planning ahead for that. However, I think it would probably make sense for that to be its own RPC: SetOrderProcessing or similar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I'll have the RA set the order to status processing with a dedicated RPC and the WFE can return the order without the CertificateSerial but with Status: Pending at the end of a successful finalize order request.
wfe2/wfe.go
Outdated
return | ||
} | ||
// Convert the corepb.Order.Names into identifiers for display to the user. | ||
idents := make([]core.AcmeIdentifier, len(order.Names)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These don't appear to be used anywhere (and this code is duplicated in orderToOrderJSON
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doh, I thought I caught the last of the leftover orderToOrderJSON
duplication.
@rolandshoemaker I think this was a byproduct of the way I approached making the finalization endpoint a subpath of the Order endpoint. The existing There's not a lot of deep thought behind the decision. I'm open to adjusting if that's preferred. |
I think there's probably some performance gain to wrapping all of the round trips (i.e. one round trip from the WFE to RA) but tbh I doubt it's that big of an issue. Given it'd end up being a relatively big change to make at this point I think it's probably fine to leave as is. |
The NewOrder usage of `orderValid` means that `CertificateSerial` will be nil but the order _is_ valid, just not finalized. This commit updates the `grpc.orderValid` function to allow the nil `CertificateSerial` field. The wrappers for the SA's `finalizeOrder` RPCs are updated to additionally check that `orderValid` is true *and* `CertificateSerial` is not nil.
@rolandshoemaker @jsha Another round of feedback applied. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally looks good! Just a couple things:
grpc/pb-marshalling.go
Outdated
@@ -406,8 +406,12 @@ func registrationValid(reg *corepb.Registration) bool { | |||
return !(reg.Id == nil || reg.Key == nil || reg.Agreement == nil || reg.InitialIP == nil || reg.CreatedAt == nil || reg.Status == nil || reg.ContactsPresent == nil) | |||
} | |||
|
|||
// orderValid checks that a corepb.Order is valid. It allows | |||
// `order.CertificateStatus` to be nil such that it can be used in places where | |||
// the order has not been finalized yet. Callers must additional ensure the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
*additionally
sa/sa.go
Outdated
err = tx.Commit() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
req.Id = &order.ID | ||
// Set the status to Processing in the request ob we return since this is the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/ob//
Also, do we definitely need to return the order? It seems like just returning an error or nil error would be clearer, since we wouldn't be claiming to fill out the order. I feel like we've run into issues elsewhere where we pass around partially-filled objects (like an authz with just a RegistrationID passed to the VA).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, do we definitely need to return the order? It seems like just returning an error or nil error would be clearer, since we wouldn't be claiming to fill out the order.
We don't - happy to rejig the two SA RPCs to not return the Order. Might as well save some bytes on the wire :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed with b74f159
@rolandshoemaker @jsha This should be ready for another 🔍 pass when you folks have a chance. |
This commit implements ietf-wg-acme/acme#342 - replacing proactive issuance and CSR as part of new-order with an explicit order finalization step that delivers the CSR. This is largely a port of the work done to add order finalization to the WIP ACMEv2 support in Boulder: letsencrypt/boulder#3169 I haven't tested this end-to-end yet - There are likely bugs lurking :-)
This commit implements ietf-wg-acme/acme#342 - replacing proactive issuance and CSR as part of new-order with an explicit order finalization step that delivers the CSR. This is largely a port of the work done to add order finalization to the WIP ACMEv2 support in Boulder: letsencrypt/boulder#3169 I haven't tested this end-to-end yet - There are likely bugs lurking :-)
This PR implements order finalization for the ACME v2 API.
In broad strokes this means:
Resolves #3123