Skip to content
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

Merged
merged 27 commits into from
Nov 1, 2017
Merged

Conversation

cpu
Copy link
Contributor

@cpu cpu commented Oct 12, 2017

This PR 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

Resolves #3123

Daniel added 5 commits October 12, 2017 14:44
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
@cpu cpu self-assigned this Oct 12, 2017
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 {
Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@jsha jsha left a 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

@@ -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) {}
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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),
Copy link
Contributor

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,
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both good ideas 👍

Copy link
Contributor Author

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

Copy link
Contributor Author

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).

Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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)
Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea 👍

Daniel added 8 commits October 16, 2017 15:51
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.
@cpu
Copy link
Contributor Author

cpu commented Oct 17, 2017

@jsha Ready for another 🔍

Copy link
Contributor

@rolandshoemaker rolandshoemaker left a 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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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) {
Copy link
Contributor

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) {
Copy link
Contributor

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 {
Copy link
Contributor

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 )

Copy link
Contributor Author

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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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 {
Copy link
Contributor

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)
Copy link
Contributor

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?

Copy link
Contributor Author

@cpu cpu Oct 19, 2017

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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))
Copy link
Contributor

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).

Copy link
Contributor Author

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.

@cpu
Copy link
Contributor Author

cpu commented Oct 19, 2017

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)

@rolandshoemaker I think this was a byproduct of the way I approached making the finalization endpoint a subpath of the Order endpoint. The existing Order() handler was already going WFE->SA to get the order in order to return it. It seemed like it could save an RPC for the easier error cases if the WFE passed that order to the RA instead of asking the RA to load it itself.

There's not a lot of deep thought behind the decision. I'm open to adjusting if that's preferred.

@rolandshoemaker
Copy link
Contributor

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.

Daniel added 4 commits October 20, 2017 13:09
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.
@cpu
Copy link
Contributor Author

cpu commented Oct 30, 2017

@rolandshoemaker @jsha Another round of feedback applied.

Copy link
Contributor

@jsha jsha left a 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:

@@ -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
Copy link
Contributor

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
Copy link
Contributor

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).

Copy link
Contributor Author

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 :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with b74f159

@cpu
Copy link
Contributor Author

cpu commented Nov 1, 2017

@rolandshoemaker @jsha This should be ready for another 🔍 pass when you folks have a chance.

@rolandshoemaker rolandshoemaker merged commit 2f263f8 into master Nov 1, 2017
@cpu cpu deleted the cpu-v2-finalize-order branch November 1, 2017 19:51
cpu pushed a commit to letsencrypt/pebble that referenced this pull request Nov 6, 2017
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 :-)
jsha pushed a commit to letsencrypt/pebble that referenced this pull request Dec 5, 2017
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 :-)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants