From f0fd07cf84fc8fc4feb70855ceb85db98619646b Mon Sep 17 00:00:00 2001 From: Kiel C Date: Thu, 15 Oct 2026 19:13:44 +0000 Subject: [PATCH 1/6] wfe: New endpoint for serving standlone MTCs This change updates the WFE GetOrder handler to include a "merkle-certificate" key with a mtc-retrieval URI value in orders returned to ACME clients when the wfe instance is configured with an MTCB service, and the order contains critical MTC fields. The change also adds a WFE MerkleCertificate handler for the mtc-retrieval URI, which asks the MTCB to return a Standalone certificate and hands it over to the client ( PEM-encoded ). --- cmd/boulder-wfe2/main.go | 10 ++ metrics/measured_http/http.go | 7 +- test/config-next/mtcb.json | 3 +- test/config-next/wfe2.json | 10 ++ wfe2/wfe.go | 172 ++++++++++++++++++- wfe2/wfe_test.go | 306 ++++++++++++++++++++++++++++++++++ 6 files changed, 496 insertions(+), 12 deletions(-) diff --git a/cmd/boulder-wfe2/main.go b/cmd/boulder-wfe2/main.go index eb12aa5422d..8c446b8de71 100644 --- a/cmd/boulder-wfe2/main.go +++ b/cmd/boulder-wfe2/main.go @@ -22,6 +22,7 @@ import ( "github.com/letsencrypt/boulder/grpc/noncebalancer" noncebalancerv1 "github.com/letsencrypt/boulder/grpc/noncebalancerv1" "github.com/letsencrypt/boulder/issuance" + mtcbpb "github.com/letsencrypt/boulder/mtcb/proto" "github.com/letsencrypt/boulder/nonce" rapb "github.com/letsencrypt/boulder/ra/proto" "github.com/letsencrypt/boulder/ratelimits" @@ -68,6 +69,7 @@ type Config struct { RAService *cmd.GRPCClientConfig SAService *cmd.GRPCClientConfig EmailExporter *cmd.GRPCClientConfig + MTCBService *cmd.GRPCClientConfig // GetNonceService is a gRPC config which contains a single SRV name // used to lookup nonce-service instances used exclusively for nonce @@ -382,6 +384,13 @@ func main() { eec = emailpb.NewExporterClient(emailExporterConn) } + var mtcbc mtcbpb.MTCBClient + if c.WFE.MTCBService != nil { + mtcbConn, err := bgrpc.ClientSetup(c.WFE.MTCBService, tlsConfig, stats, clk) + cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to MTCB") + mtcbc = mtcbpb.NewMTCBClient(mtcbConn) + } + if c.WFE.RedeemNonceService == nil { cmd.Fail("'redeemNonceService' must be configured.") } @@ -465,6 +474,7 @@ func main() { rac, sac, eec, + mtcbc, gnc, rnc, noncePrefixKey, diff --git a/metrics/measured_http/http.go b/metrics/measured_http/http.go index 64b17e143e8..a3bb877283c 100644 --- a/metrics/measured_http/http.go +++ b/metrics/measured_http/http.go @@ -32,11 +32,12 @@ func (r *responseWriterWithStatus) Write(body []byte) (int, error) { return r.ResponseWriter.Write(body) } -// serveMux is a partial interface wrapper for the one method http.ServeMux +// serveMux is a partial interface wrapper for the methods http.ServeMux // exposes that we use. This prevents us from accidentally developing an // overly-specific reliance on that concrete type. type serveMux interface { Handler(*http.Request) (http.Handler, string) + ServeHTTP(w http.ResponseWriter, r *http.Request) } // MeasuredHandler wraps an http.Handler and records prometheus stats @@ -73,7 +74,7 @@ func (h *MeasuredHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { begin := h.clk.Now() rwws := &responseWriterWithStatus{w, 0} - subHandler, pattern := h.Handler(r) + _, pattern := h.serveMux.Handler(r) h.inFlightRequestsGauge.WithLabelValues(pattern).Inc() defer h.inFlightRequestsGauge.WithLabelValues(pattern).Dec() @@ -97,5 +98,5 @@ func (h *MeasuredHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { }).Observe(h.clk.Since(begin).Seconds()) }() - subHandler.ServeHTTP(rwws, r) + h.serveMux.ServeHTTP(rwws, r) } diff --git a/test/config-next/mtcb.json b/test/config-next/mtcb.json index 9aa43ac45df..b012026e8a1 100644 --- a/test/config-next/mtcb.json +++ b/test/config-next/mtcb.json @@ -10,7 +10,8 @@ "services": { "mtcb.MTCB": { "clientNames": [ - "ra.boulder" + "ra.boulder", + "wfe.boulder" ] }, "grpc.health.v1.Health": { diff --git a/test/config-next/wfe2.json b/test/config-next/wfe2.json index acec82e7965..d2a710d91e8 100644 --- a/test/config-next/wfe2.json +++ b/test/config-next/wfe2.json @@ -77,6 +77,16 @@ "noWaitForReady": true, "hostOverride": "nonce.boulder" }, + "mtcbService": { + "dnsAuthority": "consul.service.consul", + "srvLookup": { + "service": "mtcb", + "domain": "service.consul" + }, + "timeout": "20s", + "noWaitForReady": true, + "hostOverride": "mtcb.boulder" + }, "nonceHMACKey": { "keyFile": "test/secrets/nonce_prefix_key" }, diff --git a/wfe2/wfe.go b/wfe2/wfe.go index e16483f571f..d8177f92760 100644 --- a/wfe2/wfe.go +++ b/wfe2/wfe.go @@ -37,6 +37,7 @@ import ( "github.com/letsencrypt/boulder/issuance" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics/measured_http" + mtcbpb "github.com/letsencrypt/boulder/mtcb/proto" "github.com/letsencrypt/boulder/nonce" "github.com/letsencrypt/boulder/policy" "github.com/letsencrypt/boulder/probs" @@ -67,6 +68,7 @@ const ( finalizeOrderPath = "/acme/finalize/" certPath = "/acme/cert/" renewalInfoPath = "/acme/renewal-info/" + merkleCertPath = "/acme/merkle-cert/" // Non-ACME paths. getCertPath = "/get/cert/" @@ -90,9 +92,10 @@ var errIncompleteGRPCResponse = errors.New("incomplete gRPC response message") // plus a few other data items used in ACME. Its methods are primarily handlers // for HTTPS requests for the various ACME functions. type WebFrontEndImpl struct { - ra rapb.RegistrationAuthorityClient - sa sapb.StorageAuthorityReadOnlyClient - ee emailpb.ExporterClient + ra rapb.RegistrationAuthorityClient + sa sapb.StorageAuthorityReadOnlyClient + ee emailpb.ExporterClient + mtcb mtcbpb.MTCBClient // gnc is a nonce-service client used exclusively for the issuance of // nonces. It's configured to route requests to backends colocated with the // WFE. @@ -212,6 +215,7 @@ func NewWebFrontEndImpl( rac rapb.RegistrationAuthorityClient, sac sapb.StorageAuthorityReadOnlyClient, eec emailpb.ExporterClient, + mtcbc mtcbpb.MTCBClient, gnc nonce.Getter, rnc nonce.Redeemer, rncKey []byte, @@ -265,6 +269,7 @@ func NewWebFrontEndImpl( ra: rac, sa: sac, ee: eec, + mtcb: mtcbc, gnc: gnc, rnc: rnc, rncKey: rncKey, @@ -311,7 +316,7 @@ func (wfe *WebFrontEndImpl) HandleFunc(mux *http.ServeMux, pattern string, h web methodsMap["HEAD"] = true } methodsStr := strings.Join(methods, ", ") - handler := http.StripPrefix(pattern, web.NewTopHandler(wfe.log, + var handler http.Handler = web.NewTopHandler(wfe.log, web.WFEHandlerFunc(func(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { span := trace.SpanFromContext(ctx) span.SetName(pattern) @@ -367,7 +372,14 @@ func (wfe *WebFrontEndImpl) HandleFunc(mux *http.ServeMux, pattern string, h web h(ctx, logEvent, response, request) cancel() }), - )) + ) + // Detect the lack of at least one PAIR of curly braces as a signal that + // there is no wildcard pattern. This gives us a way to migrate handlers + // individually to use wildcard patterns, as our non-wildcard handlers need + // prefixes stripped. + if !strings.Contains(pattern, `{`) && !strings.Contains(pattern, `}`) { + handler = http.StripPrefix(pattern, handler) + } mux.Handle(pattern, handler) } @@ -467,6 +479,9 @@ func (wfe *WebFrontEndImpl) Handler(stats prometheus.Registerer, oTelHTTPOptions wfe.HandleFunc(m, certPath, wfe.Certificate, "GET", "POST") wfe.HandleFunc(m, renewalInfoPath, wfe.RenewalInfo, "GET", "POST") + // POST-as-GETable ACME endpoints + wfe.HandleFunc(m, merkleCertPath+"{acctID}/{orderID}/{logID}/{serial}", wfe.MerkleCertificate, "POST") + // Boulder specific endpoints wfe.HandleFunc(m, getCertPath, wfe.Certificate, "GET") wfe.HandleFunc(m, getCertInfoPath, wfe.CertificateInfo, "GET") @@ -1856,6 +1871,126 @@ func (wfe *WebFrontEndImpl) Certificate(ctx context.Context, logEvent *web.Reque } } +// MerkleCertificate is used by client to request a copy of their Merkle Tree Certificate. +func (wfe *WebFrontEndImpl) MerkleCertificate(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { + acct, err := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) + return + } + + // wfe instance not configured with an MTCB Client + if wfe.mtcb == nil { + wfe.sendError(response, logEvent, probs.Malformed("Not Supported"), nil) + return + } + + reqAcctID, err := parsePositiveInt(request.PathValue("acctID")) + if err != nil { + wfe.sendError(response, logEvent, probs.Malformed("Request URL does not include a valid accountID"), nil) + return + } + reqOrderID, err := parsePositiveInt(request.PathValue("orderID")) + if err != nil { + wfe.sendError(response, logEvent, probs.Malformed("Request URL does not include a valid orderID"), nil) + return + } + reqSerial, err := parsePositiveUint(request.PathValue("serial")) + if err != nil { + wfe.sendError(response, logEvent, probs.Malformed("Request URL does not include a valid serial number"), nil) + return + } + + // Verify that the request path account ID matches the POST-as-GET-validated account ID + if reqAcctID != acct.ID { + wfe.sendError(response, logEvent, probs.Unauthorized(fmt.Sprintf("Not allowed for ID %d", acct.ID)), nil) + return + } + + // MerkleCertificate does not need deployability protection via + // WFECallsRAForGetOrder, and always looks up orders via the RA. + order, err := wfe.ra.GetOrder(ctx, &rapb.GetOrderRequest{OrderID: reqOrderID}) + if err != nil { + if errors.Is(err, berrors.NotFound) { + wfe.sendError(response, logEvent, probs.NotFound(fmt.Sprintf("No order for ID %d", reqOrderID)), nil) + return + } + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, + fmt.Sprintf("Failed to retrieve order for ID %d", reqOrderID)), err) + return + } + + if core.IsAnyNilOrZero(order.Id, order.Status, order.RegistrationID, order.Identifiers, order.Created, order.Expires) { + wfe.sendError(response, logEvent, probs.ServerInternal(fmt.Sprintf("Failed to retrieve order for ID %d", reqOrderID)), errIncompleteGRPCResponse) + return + } + + // If the order is not yet valid, then we did not find what the requester + // wants. + if order.Status != string(core.StatusValid) { + wfe.sendError(response, logEvent, probs.NotFound(fmt.Sprintf("No order for ID %d", reqOrderID)), nil) + return + } + + // If request Path ID does not match order registration ID, then we did not + // find what the requester wants. + if acct.ID != order.RegistrationID { + wfe.sendError(response, logEvent, probs.NotFound(fmt.Sprintf("No order for ID %d", reqOrderID)), nil) + return + } + + // If the order does not have Merkle Certificate information, then we did + // not find what the requester wants. + if core.IsAnyNilOrZero(order.MtcLogID, order.MtcSerialNumber, order.MtcSubtreeID) { + wfe.sendError(response, logEvent, probs.NotFound(fmt.Sprintf("No merkle certificate for order ID %d", reqOrderID)), nil) + return + } + + // If the request path is looking for an MTC that doesn't match the + // retrieved order, their request is malformed. + if request.PathValue("logID") != order.MtcLogID || reqSerial != order.MtcSerialNumber { + wfe.sendError(response, logEvent, probs.Malformed("Merkle Cert URL details do not match order details"), nil) + return + } + + logEvent.Extra["RequestedMtcSerial"] = reqSerial + + // Now ask the MTCB for the standalone certificate. + standalone, err := wfe.mtcb.GetStandalone(ctx, + &mtcbpb.StandaloneRequest{ + MtcLogID: order.MtcLogID, + MtcSerialNumber: order.MtcSerialNumber, + MtcSubtreeID: order.MtcSubtreeID, + }) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Failed to retrieve Merkle Certificate"), err) + return + } + + if standalone.CertDER == nil { + wfe.sendError(response, logEvent, probs.ServerInternal("Problem getting Merkle Certificate"), errIncompleteGRPCResponse) + return + } + + // And PEM-encode the retrieved certificate for return to the client. + standalonePEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: standalone.CertDER, + }) + + // NOTE(@ezekiel): Similar to Certificate(), we must explicitly set the + // Content-Length header here. The Go HTTP library will only add this header + // if the body is below a certain size and a PEM encoded Merkle Certificate + // will likely exceed this threshold. Since we know the length we can + // reliably set it ourselves and not worry. + response.Header().Set("Content-Length", strconv.Itoa(len(standalonePEM))) + response.Header().Set("Content-Type", "application/pem-certificate-chain") + response.WriteHeader(http.StatusOK) + if _, err = response.Write(standalonePEM); err != nil { + wfe.log.Warningf("Could not write response: %s", err) + } +} + // BuildID tells the requester what build we're running. func (wfe *WebFrontEndImpl) BuildID(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "text/plain") @@ -2077,6 +2212,7 @@ type orderJSON struct { Finalize string `json:"finalize"` Profile string `json:"profile,omitempty"` Certificate string `json:"certificate,omitempty"` + MerkleCert string `json:"merkle-certificate,omitempty"` Error *probs.ProblemDetails `json:"error,omitempty"` Replaces string `json:"replaces,omitempty"` } @@ -2115,9 +2251,19 @@ func (wfe *WebFrontEndImpl) orderToOrderJSON(request *http.Request, order *corep respObj.Authorizations = append(respObj.Authorizations, endpoint) } if respObj.Status == core.StatusValid { - certURL := web.RelativeEndpoint(request, - certPath, order.CertificateSerial) - respObj.Certificate = certURL + if order.CertificateSerial != "" { + respObj.Certificate = web.RelativeEndpoint(request, + certPath, order.CertificateSerial) + } + if !core.IsAnyNilOrZero(order.MtcLogID, order.MtcSerialNumber, order.MtcSubtreeID) && wfe.mtcb != nil { + respObj.MerkleCert = web.RelativeEndpoint(request, + merkleCertPath, + fmt.Sprintf("%d", order.RegistrationID), + fmt.Sprintf("%d", order.Id), + order.MtcLogID, + fmt.Sprintf("%d", order.MtcSerialNumber), + ) + } } return respObj } @@ -2892,3 +3038,13 @@ func parsePositiveInt(v string) (int64, error) { } return parsed, nil } + +// parsePositiveUint returns the input parsed as an uint64, or a +// berrors.NotFoundError on parse failure, zero, or negative numbers. +func parsePositiveUint(v string) (uint64, error) { + parsed, err := strconv.ParseUint(v, 10, 64) + if err != nil || parsed == 0 { + return 0, berrors.NotFoundError("") + } + return parsed, nil +} diff --git a/wfe2/wfe_test.go b/wfe2/wfe_test.go index 63ef8abdb65..2a34f67e433 100644 --- a/wfe2/wfe_test.go +++ b/wfe2/wfe_test.go @@ -48,6 +48,7 @@ import ( blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/mocks" + mtcbpb "github.com/letsencrypt/boulder/mtcb/proto" "github.com/letsencrypt/boulder/must" "github.com/letsencrypt/boulder/nonce" noncepb "github.com/letsencrypt/boulder/nonce/proto" @@ -433,6 +434,7 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock, requestSigner) { &MockRegistrationAuthority{clk: fc}, mockSA, nil, + nil, gnc, rnc, rncKey, @@ -1130,6 +1132,11 @@ func TestHTTPMethods(t *testing.T) { Path: newNoncePath, Allowed: getOrPost, }, + { + Name: "merkleCert path should be POST only", + Path: merkleCertPath + "1/2/3/4", + Allowed: postOnly, + }, } // NOTE: We omit http.MethodOptions because all requests with this method are @@ -4622,3 +4629,302 @@ func TestMaxCumulativeIdentifierLength(t *testing.T) { t.Errorf("newOrder with too long identifiers: got %q, want %q", detail, expected) } } + +// mockRAWithGetOrder returns a specially-crafted order from a set of fixed +// values in response to GetOrder() +type mockRAWithGetOrder struct { + rapb.RegistrationAuthorityClient + clk clock.Clock +} + +func (ra *mockRAWithGetOrder) GetOrder(_ context.Context, req *rapb.GetOrderRequest, _ ...grpc.CallOption) (*corepb.Order, error) { + + now := ra.clk.Now() + created := now.AddDate(-30, 0, 0) + exp := now.AddDate(30, 0, 0) + validClassicOrder := &corepb.Order{ + Id: req.OrderID, + RegistrationID: 1, + Created: timestamppb.New(created), + Expires: timestamppb.New(exp), + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, + Status: string(core.StatusValid), + V2Authorizations: []int64{1, 2}, + CertificateSerial: "4", + Error: nil, + CertificateProfileName: "default", + } + validMerkleOrder := &corepb.Order{ + Id: req.OrderID, + RegistrationID: 1, + Created: timestamppb.New(created), + Expires: timestamppb.New(exp), + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, + Status: string(core.StatusValid), + V2Authorizations: []int64{1, 2}, + CertificateSerial: "4", + Error: nil, + CertificateProfileName: "default", + MtcLogID: "44947.4.1.0.3", + MtcSerialNumber: 4, + MtcSubtreeID: 5, + } + // initialize empty order + notYetValidMerkleOrder := &corepb.Order{} + // dereference and copy valid order + *notYetValidMerkleOrder = *validMerkleOrder + // modify Status and some Mtc detail + notYetValidMerkleOrder.Status = string(core.StatusProcessing) + notYetValidMerkleOrder.MtcSerialNumber = 0 + notYetValidMerkleOrder.MtcSubtreeID = 0 + + // initialize empty order + validMerkleOrderMissingStandalone := &corepb.Order{} + // dereference and copy valid order + *validMerkleOrderMissingStandalone = *validMerkleOrder + // modify MtcSerialNumber to trigger the mock MTCB to return an error + validMerkleOrderMissingStandalone.MtcSerialNumber = 99 + var orders = []*corepb.Order{ + {}, // index zero is empty + validClassicOrder, + validMerkleOrder, + notYetValidMerkleOrder, + validMerkleOrderMissingStandalone, + } + if 0 < req.OrderID && req.OrderID <= 4 { + return orders[req.OrderID], nil + } + return nil, berrors.NotFoundError("No order found for ID %d", req.OrderID) +} + +func TestMerkleGetOrder(t *testing.T) { + t.Cleanup(features.Reset) + + fc := clock.NewFake() + fc.Set(time.Now()) + // This fake clock is assigned as the RA mock clock, and this expiry + // timestamp calculation must match that mock. + orderExpiry := fc.Now().AddDate(30, 0, 0).Format(time.RFC3339Nano) + + testCases := []struct { + name string + path string + expect string + }{ + { + // testCase 0 is a special case tested before the wfe is wired-up with an MTCB + name: "Merkle Order without MTCBService", + path: "1/2", + expect: `{ "status": "valid", "expires": "` + orderExpiry + `", "identifiers": [ { "type": "dns", "value": "example.com" } ], "authorizations": [ "http://localhost/acme/authz/1/1", "http://localhost/acme/authz/1/2" ], "finalize": "http://localhost/acme/finalize/1/2", "profile": "default", "certificate": "http://localhost/acme/cert/4" }`, + }, + { + name: "Classic Order", + path: "1/1", + expect: `{ "status": "valid", "expires": "` + orderExpiry + `", "identifiers": [ { "type": "dns", "value": "example.com" } ], "authorizations": [ "http://localhost/acme/authz/1/1", "http://localhost/acme/authz/1/2" ], "finalize": "http://localhost/acme/finalize/1/1", "profile": "default", "certificate": "http://localhost/acme/cert/4" }`, + }, + { + name: "Merkle Order", + path: "1/2", + expect: `{ "status": "valid", "expires": "` + orderExpiry + `", "identifiers": [ { "type": "dns", "value": "example.com" } ], "authorizations": [ "http://localhost/acme/authz/1/1", "http://localhost/acme/authz/1/2" ], "finalize": "http://localhost/acme/finalize/1/2", "profile": "default", "certificate": "http://localhost/acme/cert/4", "merkle-certificate": "http://localhost/acme/merkle-cert/1/2/44947.4.1.0.3/4" }`, + }, + { + name: "Not Ready Merkle Order", + path: "1/3", + expect: `{ "status": "processing", "expires": "` + orderExpiry + `", "identifiers": [ { "type": "dns", "value": "example.com" } ], "authorizations": [ "http://localhost/acme/authz/1/1", "http://localhost/acme/authz/1/2" ], "finalize": "http://localhost/acme/finalize/1/3", "profile": "default" }`, + }, + { + name: "Not Found Order", + path: "1/9", + expect: `{"type": "urn:ietf:params:acme:error:malformed","detail": "No order for ID 9","status": 404}`, + }, + } + + wfe, _, signer := setupWFE(t) + + makePost := func(keyID int64, path, body string) *http.Request { + _, _, jwsBody := signer.byKeyID(keyID, nil, fmt.Sprintf("http://localhost/%s", path), body) + return makePostRequestWithPath(path, jwsBody) + } + + features.Set(features.Config{WFECallsRAForGetOrder: true}) + + mockRA := &mockRAWithGetOrder{} + mockRA.clk = fc + wfe.ra = mockRA + + // test order retrieval with no MTCBService configured + responseWriter := httptest.NewRecorder() + wfe.GetOrder(ctx, newRequestEvent(), responseWriter, makePost(1, testCases[0].path, "")) + test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), testCases[0].expect) + + // now configure wire-up the wfe with an MTCB mock + mockMTCB := &mockMTCBWithGetStandalone{} + wfe.mtcb = mockMTCB + + for tcIdx, tc := range testCases { + // skip special test-case 0 + if tcIdx == 0 { + continue + } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + responseWriter := httptest.NewRecorder() + wfe.GetOrder(ctx, newRequestEvent(), responseWriter, makePost(1, tc.path, "")) + test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tc.expect) + }) + } +} + +// mockMTCBWithGetStandalone returns a fixed value in response to +// GetStandalone() +type mockMTCBWithGetStandalone struct { + mtcbpb.MTCBClient +} + +// GetStandalone in this mock always returns the same palindromic nonsense bytes +// of primes, unless the request is for MtcSerialNumber 99, in which case an +// empty StandaloneResponse and a mock error are returned. +func (mtcb *mockMTCBWithGetStandalone) GetStandalone(_ context.Context, req *mtcbpb.StandaloneRequest, _ ...grpc.CallOption) (*mtcbpb.StandaloneResponse, error) { + if req.MtcSerialNumber == 99 { + return &mtcbpb.StandaloneResponse{}, errors.New("mock error") + } + return &mtcbpb.StandaloneResponse{CertDER: []byte{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 29, 23, 19, 17, 13, 11, 7, 5, 3, 2}}, nil +} + +func TestMerkleCertificate(t *testing.T) { + t.Parallel() + + type expectResponse struct { + isJSON bool + expect string + } + + testCases := []struct { + name string + pathAdd string + expResp expectResponse + }{ + { + name: "Request using non-existent Order ID", + pathAdd: "1/9/44947.4.1.0.3/4", + expResp: expectResponse{ + isJSON: true, + expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "No order for ID 9", "status": 404}`, + }, + }, + { + name: "Request using wrong Account ID for extant Merkle Order", + pathAdd: "9/2/44947.4.1.0.3/4", + expResp: expectResponse{ + isJSON: true, + expect: `{"type": "urn:ietf:params:acme:error:unauthorized", "detail": "Not allowed for ID 1", "status": 403}`, + }, + }, + { + name: "Request using wrong Account ID for extant Classic Order", + pathAdd: "9/1/44947.4.1.0.3/4", + expResp: expectResponse{ + isJSON: true, + expect: `{"type": "urn:ietf:params:acme:error:unauthorized", "detail": "Not allowed for ID 1", "status": 403}`, + }, + }, + { + name: "Request using wrong Log ID for extant Merkle Order", + pathAdd: "1/2/44947.4.1.0.9/4", + expResp: expectResponse{ + isJSON: true, + expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "Merkle Cert URL details do not match order details", "status": 400}`, + }, + }, + { + name: "Request using wrong Log ID for extant Classic Order", + pathAdd: "1/1/44947.4.1.0.9/4", + expResp: expectResponse{ + isJSON: true, + expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "No merkle certificate for order ID 1", "status": 404}`, + }, + }, + { + name: "Request using wrong Serial Number for extant Merkle Order", + pathAdd: "1/2/44947.4.1.0.3/9", + expResp: expectResponse{ + isJSON: true, + expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "Merkle Cert URL details do not match order details", "status": 400}`, + }, + }, + { + name: "Request using wrong Serial Number for extant Classic Order", + pathAdd: "1/1/44947.4.1.0.3/9", + expResp: expectResponse{ + isJSON: true, + expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "No merkle certificate for order ID 1", "status": 404}`, + }, + }, + { + name: "Request using trigger-mock Serial Number for error GetStandalone response", + pathAdd: "1/4/44947.4.1.0.3/99", + expResp: expectResponse{ + isJSON: true, + expect: `{"type": "urn:ietf:params:acme:error:serverInternal", "detail": "Failed to retrieve Merkle Certificate", "status": 500}`, + }, + }, + { + name: "Request for order in processing", + pathAdd: "1/3/44947.4.1.0.3/4", + expResp: expectResponse{ + isJSON: true, + expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "No order for ID 3", "status": 404}`, + }, + }, + { + name: "Request is Correct", + pathAdd: "1/2/44947.4.1.0.3/4", + expResp: expectResponse{ + isJSON: false, + expect: "-----BEGIN CERTIFICATE-----\nAgMFBwsNERMXHR8dFxMRDQsHBQMC\n-----END CERTIFICATE-----\n", + }, + }, + } + + fc := clock.NewFake() + fc.Set(time.Now()) + + wfe, _, signer := setupWFE(t) + mux := wfe.Handler(metrics.NoopRegisterer) + + makePost := func(keyID int64, path, body string) *http.Request { + _, _, jwsBody := signer.byKeyID(keyID, nil, fmt.Sprintf("http://localhost%s", path), body) + return makePostRequestWithPath(path, jwsBody) + } + + mockRA := &mockRAWithGetOrder{} + mockRA.clk = fc + wfe.ra = mockRA + + // Test that a merkle-cert path request to a WFE without an MTCB Client returns "Not Supported" + responseWriter := httptest.NewRecorder() + requestPath := merkleCertPath + "1/2/44947.4.1.0.3/4" + mux.ServeHTTP(responseWriter, makePost(1, requestPath, "")) + test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), `{ "type": "urn:ietf:params:acme:error:malformed", "detail": "Not Supported", "status": 400 }`) + + // Now wire-up a mock MTCB + mockMTCB := &mockMTCBWithGetStandalone{} + wfe.mtcb = mockMTCB + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + responseWriter := httptest.NewRecorder() + requestPath := merkleCertPath + tc.pathAdd + mux.ServeHTTP(responseWriter, makePost(1, requestPath, "")) + + if tc.expResp.isJSON { + test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tc.expResp.expect) + } else { + test.AssertEquals(t, responseWriter.Body.String(), tc.expResp.expect) + } + }) + } +} From 7cea46ae6d1f81356c688028cc8c7c214acb7076 Mon Sep 17 00:00:00 2001 From: Kiel C Date: Thu, 15 Oct 2026 19:26:17 +0000 Subject: [PATCH 2/6] Tidy up some comment text. --- wfe2/wfe_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/wfe2/wfe_test.go b/wfe2/wfe_test.go index 2a34f67e433..3265c4cbc04 100644 --- a/wfe2/wfe_test.go +++ b/wfe2/wfe_test.go @@ -4752,12 +4752,12 @@ func TestMerkleGetOrder(t *testing.T) { mockRA.clk = fc wfe.ra = mockRA - // test order retrieval with no MTCBService configured + // Test order retrieval with no MTCBService configured responseWriter := httptest.NewRecorder() wfe.GetOrder(ctx, newRequestEvent(), responseWriter, makePost(1, testCases[0].path, "")) test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), testCases[0].expect) - // now configure wire-up the wfe with an MTCB mock + // Now wire-up the wfe with an MTCB mock mockMTCB := &mockMTCBWithGetStandalone{} wfe.mtcb = mockMTCB @@ -4902,7 +4902,8 @@ func TestMerkleCertificate(t *testing.T) { mockRA.clk = fc wfe.ra = mockRA - // Test that a merkle-cert path request to a WFE without an MTCB Client returns "Not Supported" + // Test that a merkle-cert path request to a WFE without an MTCB Client + // correctly returns "Not Supported" responseWriter := httptest.NewRecorder() requestPath := merkleCertPath + "1/2/44947.4.1.0.3/4" mux.ServeHTTP(responseWriter, makePost(1, requestPath, "")) From 86c67859e898d97dce5b3942bb755573908946d2 Mon Sep 17 00:00:00 2001 From: Kiel C Date: Fri, 16 Oct 2026 16:14:18 +0000 Subject: [PATCH 3/6] Add testOrderPB helper to WFE tests RA mock. --- wfe2/wfe_test.go | 61 +++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/wfe2/wfe_test.go b/wfe2/wfe_test.go index 3265c4cbc04..53b48473682 100644 --- a/wfe2/wfe_test.go +++ b/wfe2/wfe_test.go @@ -4637,13 +4637,15 @@ type mockRAWithGetOrder struct { clk clock.Clock } -func (ra *mockRAWithGetOrder) GetOrder(_ context.Context, req *rapb.GetOrderRequest, _ ...grpc.CallOption) (*corepb.Order, error) { - +// testOrderPB is a mock-internal helper for creating order proto structures. +// Includes important dates in the returned order structure using the mock RA's +// clock. Test should match these timestamp calculations using the same clock. +func (ra *mockRAWithGetOrder) testOrderPB(id int64, typ string) *corepb.Order { now := ra.clk.Now() created := now.AddDate(-30, 0, 0) exp := now.AddDate(30, 0, 0) - validClassicOrder := &corepb.Order{ - Id: req.OrderID, + orderPB := &corepb.Order{ + Id: id, RegistrationID: 1, Created: timestamppb.New(created), Expires: timestamppb.New(exp), @@ -4654,36 +4656,31 @@ func (ra *mockRAWithGetOrder) GetOrder(_ context.Context, req *rapb.GetOrderRequ Error: nil, CertificateProfileName: "default", } - validMerkleOrder := &corepb.Order{ - Id: req.OrderID, - RegistrationID: 1, - Created: timestamppb.New(created), - Expires: timestamppb.New(exp), - Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, - Status: string(core.StatusValid), - V2Authorizations: []int64{1, 2}, - CertificateSerial: "4", - Error: nil, - CertificateProfileName: "default", - MtcLogID: "44947.4.1.0.3", - MtcSerialNumber: 4, - MtcSubtreeID: 5, - } - // initialize empty order - notYetValidMerkleOrder := &corepb.Order{} - // dereference and copy valid order - *notYetValidMerkleOrder = *validMerkleOrder - // modify Status and some Mtc detail + if typ == "merkle" { + orderPB.MtcLogID = "44947.4.1.0.3" + orderPB.MtcSerialNumber = 4 + orderPB.MtcSubtreeID = 5 + } + return orderPB +} + +// The `Id` in any order returned by a call to this mock's GetOrder is always +// the same as the input GetOrderRequest.OrderID, if within range of crafted +// test order fixtures. Otherwise, GetOrder returns a berrors.NotFoundError +func (ra *mockRAWithGetOrder) GetOrder(_ context.Context, req *rapb.GetOrderRequest, _ ...grpc.CallOption) (*corepb.Order, error) { + validClassicOrder := ra.testOrderPB(req.OrderID, "classic") + validMerkleOrder := ra.testOrderPB(req.OrderID, "merkle") + + // modify Status and some Mtc details for a not-yet-valid order + notYetValidMerkleOrder := ra.testOrderPB(req.OrderID, "merkle") notYetValidMerkleOrder.Status = string(core.StatusProcessing) notYetValidMerkleOrder.MtcSerialNumber = 0 notYetValidMerkleOrder.MtcSubtreeID = 0 - // initialize empty order - validMerkleOrderMissingStandalone := &corepb.Order{} - // dereference and copy valid order - *validMerkleOrderMissingStandalone = *validMerkleOrder // modify MtcSerialNumber to trigger the mock MTCB to return an error - validMerkleOrderMissingStandalone.MtcSerialNumber = 99 + validMerkleOrderMissingStandalone := ra.testOrderPB(req.OrderID, "merkle") + validMerkleOrderMissingStandalone.MtcSerialNumber = 666 + var orders = []*corepb.Order{ {}, // index zero is empty validClassicOrder, @@ -4783,10 +4780,10 @@ type mockMTCBWithGetStandalone struct { } // GetStandalone in this mock always returns the same palindromic nonsense bytes -// of primes, unless the request is for MtcSerialNumber 99, in which case an +// of primes, unless the request is for MtcSerialNumber 666, in which case an // empty StandaloneResponse and a mock error are returned. func (mtcb *mockMTCBWithGetStandalone) GetStandalone(_ context.Context, req *mtcbpb.StandaloneRequest, _ ...grpc.CallOption) (*mtcbpb.StandaloneResponse, error) { - if req.MtcSerialNumber == 99 { + if req.MtcSerialNumber == 666 { return &mtcbpb.StandaloneResponse{}, errors.New("mock error") } return &mtcbpb.StandaloneResponse{CertDER: []byte{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 29, 23, 19, 17, 13, 11, 7, 5, 3, 2}}, nil @@ -4863,7 +4860,7 @@ func TestMerkleCertificate(t *testing.T) { }, { name: "Request using trigger-mock Serial Number for error GetStandalone response", - pathAdd: "1/4/44947.4.1.0.3/99", + pathAdd: "1/4/44947.4.1.0.3/666", expResp: expectResponse{ isJSON: true, expect: `{"type": "urn:ietf:params:acme:error:serverInternal", "detail": "Failed to retrieve Merkle Certificate", "status": 500}`, From bec1210a6991a1fcfaee5c7c5f122ed989644455 Mon Sep 17 00:00:00 2001 From: Kiel C Date: Mon, 19 Oct 2026 15:24:13 +0000 Subject: [PATCH 4/6] wfe: mock RA orderPBs classic or merkle, not both The order information returned through the mockRA should only include one or the other of a classic certificate serial number, or merkle tree certificate info, not both. --- wfe2/wfe_test.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/wfe2/wfe_test.go b/wfe2/wfe_test.go index 53b48473682..282870c8c48 100644 --- a/wfe2/wfe_test.go +++ b/wfe2/wfe_test.go @@ -4652,14 +4652,18 @@ func (ra *mockRAWithGetOrder) testOrderPB(id int64, typ string) *corepb.Order { Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, Status: string(core.StatusValid), V2Authorizations: []int64{1, 2}, - CertificateSerial: "4", Error: nil, CertificateProfileName: "default", } - if typ == "merkle" { + switch typ { + case "classic": + orderPB.CertificateSerial = "4" + case "merkle": orderPB.MtcLogID = "44947.4.1.0.3" orderPB.MtcSerialNumber = 4 orderPB.MtcSubtreeID = 5 + default: + orderPB.Status = string(core.StatusInvalid) } return orderPB } @@ -4712,7 +4716,7 @@ func TestMerkleGetOrder(t *testing.T) { // testCase 0 is a special case tested before the wfe is wired-up with an MTCB name: "Merkle Order without MTCBService", path: "1/2", - expect: `{ "status": "valid", "expires": "` + orderExpiry + `", "identifiers": [ { "type": "dns", "value": "example.com" } ], "authorizations": [ "http://localhost/acme/authz/1/1", "http://localhost/acme/authz/1/2" ], "finalize": "http://localhost/acme/finalize/1/2", "profile": "default", "certificate": "http://localhost/acme/cert/4" }`, + expect: `{ "status": "valid", "expires": "` + orderExpiry + `", "identifiers": [ { "type": "dns", "value": "example.com" } ], "authorizations": [ "http://localhost/acme/authz/1/1", "http://localhost/acme/authz/1/2" ], "finalize": "http://localhost/acme/finalize/1/2", "profile": "default" }`, }, { name: "Classic Order", @@ -4722,7 +4726,7 @@ func TestMerkleGetOrder(t *testing.T) { { name: "Merkle Order", path: "1/2", - expect: `{ "status": "valid", "expires": "` + orderExpiry + `", "identifiers": [ { "type": "dns", "value": "example.com" } ], "authorizations": [ "http://localhost/acme/authz/1/1", "http://localhost/acme/authz/1/2" ], "finalize": "http://localhost/acme/finalize/1/2", "profile": "default", "certificate": "http://localhost/acme/cert/4", "merkle-certificate": "http://localhost/acme/merkle-cert/1/2/44947.4.1.0.3/4" }`, + expect: `{ "status": "valid", "expires": "` + orderExpiry + `", "identifiers": [ { "type": "dns", "value": "example.com" } ], "authorizations": [ "http://localhost/acme/authz/1/1", "http://localhost/acme/authz/1/2" ], "finalize": "http://localhost/acme/finalize/1/2", "profile": "default", "merkle-certificate": "http://localhost/acme/merkle-cert/1/2/44947.4.1.0.3/4" }`, }, { name: "Not Ready Merkle Order", From db55233b852565fa1b9d71906879f1bafc4d6704 Mon Sep 17 00:00:00 2001 From: Kiel C Date: Mon, 19 Oct 2026 15:26:37 +0000 Subject: [PATCH 5/6] wfe: fix otel span names following ServeMux changes. Claude was directed at failing integration test code. It traced the issue and added a helper function for WithSpanNameFormatter as the fix, I re-wrote its comment. --- metrics/measured_http/http.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/metrics/measured_http/http.go b/metrics/measured_http/http.go index a3bb877283c..ede721f9055 100644 --- a/metrics/measured_http/http.go +++ b/metrics/measured_http/http.go @@ -51,6 +51,16 @@ type MeasuredHandler struct { inFlightRequestsGauge *prometheus.GaugeVec } +// spanName is a helper used by otelhttp, as configured below, to name server +// spans according to the ServeMux pattern that matched the request, with a +// fallback to a span named according to the "operation". +func spanName(operation string, r *http.Request) string { + if r.Pattern != "" { + return r.Pattern + } + return operation +} + func New(m serveMux, clk clock.Clock, stats prometheus.Registerer, opts ...otelhttp.Option) http.Handler { responseTime := promauto.With(stats).NewHistogramVec(prometheus.HistogramOpts{ Name: "response_time", @@ -67,7 +77,7 @@ func New(m serveMux, clk clock.Clock, stats prometheus.Registerer, opts ...otelh clk: clk, stat: responseTime, inFlightRequestsGauge: inFlightRequestsGauge, - }, "server", opts...) + }, "server", append([]otelhttp.Option{otelhttp.WithSpanNameFormatter(spanName)}, opts...)...) } func (h *MeasuredHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { From fac05e29bbe45f226b9965ae6216e6fa7247afe7 Mon Sep 17 00:00:00 2001 From: Kiel C Date: Mon, 19 Oct 2026 17:33:09 +0000 Subject: [PATCH 6/6] wfe: one Certificate url field in presented order. Orders presented to clients should not have Certificate or MerkleCertificate fields, they just have Certificate, with an appropriate URL for the certificate type. This change also makes MTC / merkle tree certificate naming more consistent within the change, and does not abbreviate as "MerkleCert", etc. --- wfe2/wfe.go | 31 ++++++++++++------------ wfe2/wfe_test.go | 62 ++++++++++++++++++++++++------------------------ 2 files changed, 46 insertions(+), 47 deletions(-) diff --git a/wfe2/wfe.go b/wfe2/wfe.go index d8177f92760..bc1e98e006a 100644 --- a/wfe2/wfe.go +++ b/wfe2/wfe.go @@ -68,7 +68,7 @@ const ( finalizeOrderPath = "/acme/finalize/" certPath = "/acme/cert/" renewalInfoPath = "/acme/renewal-info/" - merkleCertPath = "/acme/merkle-cert/" + mtcPath = "/acme/mtc/" // Non-ACME paths. getCertPath = "/get/cert/" @@ -480,7 +480,7 @@ func (wfe *WebFrontEndImpl) Handler(stats prometheus.Registerer, oTelHTTPOptions wfe.HandleFunc(m, renewalInfoPath, wfe.RenewalInfo, "GET", "POST") // POST-as-GETable ACME endpoints - wfe.HandleFunc(m, merkleCertPath+"{acctID}/{orderID}/{logID}/{serial}", wfe.MerkleCertificate, "POST") + wfe.HandleFunc(m, mtcPath+"{acctID}/{orderID}/{logID}/{serial}", wfe.MerkleTreeCertificate, "POST") // Boulder specific endpoints wfe.HandleFunc(m, getCertPath, wfe.Certificate, "GET") @@ -1871,8 +1871,8 @@ func (wfe *WebFrontEndImpl) Certificate(ctx context.Context, logEvent *web.Reque } } -// MerkleCertificate is used by client to request a copy of their Merkle Tree Certificate. -func (wfe *WebFrontEndImpl) MerkleCertificate(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { +// MerkleTreeCertificate is used by client to request a copy of their merkle tree certificate. +func (wfe *WebFrontEndImpl) MerkleTreeCertificate(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { acct, err := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) @@ -1907,7 +1907,7 @@ func (wfe *WebFrontEndImpl) MerkleCertificate(ctx context.Context, logEvent *web return } - // MerkleCertificate does not need deployability protection via + // MerkleTreeCertificate does not need deployability protection via // WFECallsRAForGetOrder, and always looks up orders via the RA. order, err := wfe.ra.GetOrder(ctx, &rapb.GetOrderRequest{OrderID: reqOrderID}) if err != nil { @@ -1939,17 +1939,17 @@ func (wfe *WebFrontEndImpl) MerkleCertificate(ctx context.Context, logEvent *web return } - // If the order does not have Merkle Certificate information, then we did + // If the order does not have merkle tree certificate information, then we did // not find what the requester wants. if core.IsAnyNilOrZero(order.MtcLogID, order.MtcSerialNumber, order.MtcSubtreeID) { - wfe.sendError(response, logEvent, probs.NotFound(fmt.Sprintf("No merkle certificate for order ID %d", reqOrderID)), nil) + wfe.sendError(response, logEvent, probs.NotFound(fmt.Sprintf("No merkle tree certificate for order ID %d", reqOrderID)), nil) return } // If the request path is looking for an MTC that doesn't match the // retrieved order, their request is malformed. if request.PathValue("logID") != order.MtcLogID || reqSerial != order.MtcSerialNumber { - wfe.sendError(response, logEvent, probs.Malformed("Merkle Cert URL details do not match order details"), nil) + wfe.sendError(response, logEvent, probs.Malformed("MTC request URL details do not match order details"), nil) return } @@ -1963,12 +1963,12 @@ func (wfe *WebFrontEndImpl) MerkleCertificate(ctx context.Context, logEvent *web MtcSubtreeID: order.MtcSubtreeID, }) if err != nil { - wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Failed to retrieve Merkle Certificate"), err) + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Failed to retrieve merkle tree certificate"), err) return } if standalone.CertDER == nil { - wfe.sendError(response, logEvent, probs.ServerInternal("Problem getting Merkle Certificate"), errIncompleteGRPCResponse) + wfe.sendError(response, logEvent, probs.ServerInternal("Problem getting merkle tree certificate"), errIncompleteGRPCResponse) return } @@ -1980,9 +1980,9 @@ func (wfe *WebFrontEndImpl) MerkleCertificate(ctx context.Context, logEvent *web // NOTE(@ezekiel): Similar to Certificate(), we must explicitly set the // Content-Length header here. The Go HTTP library will only add this header - // if the body is below a certain size and a PEM encoded Merkle Certificate - // will likely exceed this threshold. Since we know the length we can - // reliably set it ourselves and not worry. + // if the body is below a certain size and a PEM encoded merkle tree + // certificate will likely exceed this threshold. Since we know the length + // we can reliably set it ourselves and not worry. response.Header().Set("Content-Length", strconv.Itoa(len(standalonePEM))) response.Header().Set("Content-Type", "application/pem-certificate-chain") response.WriteHeader(http.StatusOK) @@ -2212,7 +2212,6 @@ type orderJSON struct { Finalize string `json:"finalize"` Profile string `json:"profile,omitempty"` Certificate string `json:"certificate,omitempty"` - MerkleCert string `json:"merkle-certificate,omitempty"` Error *probs.ProblemDetails `json:"error,omitempty"` Replaces string `json:"replaces,omitempty"` } @@ -2256,8 +2255,8 @@ func (wfe *WebFrontEndImpl) orderToOrderJSON(request *http.Request, order *corep certPath, order.CertificateSerial) } if !core.IsAnyNilOrZero(order.MtcLogID, order.MtcSerialNumber, order.MtcSubtreeID) && wfe.mtcb != nil { - respObj.MerkleCert = web.RelativeEndpoint(request, - merkleCertPath, + respObj.Certificate = web.RelativeEndpoint(request, + mtcPath, fmt.Sprintf("%d", order.RegistrationID), fmt.Sprintf("%d", order.Id), order.MtcLogID, diff --git a/wfe2/wfe_test.go b/wfe2/wfe_test.go index 282870c8c48..df8b0a9b89c 100644 --- a/wfe2/wfe_test.go +++ b/wfe2/wfe_test.go @@ -1133,8 +1133,8 @@ func TestHTTPMethods(t *testing.T) { Allowed: getOrPost, }, { - Name: "merkleCert path should be POST only", - Path: merkleCertPath + "1/2/3/4", + Name: "MTC path should be POST only", + Path: mtcPath + "1/2/3/4", Allowed: postOnly, }, } @@ -4658,7 +4658,7 @@ func (ra *mockRAWithGetOrder) testOrderPB(id int64, typ string) *corepb.Order { switch typ { case "classic": orderPB.CertificateSerial = "4" - case "merkle": + case "mtc": orderPB.MtcLogID = "44947.4.1.0.3" orderPB.MtcSerialNumber = 4 orderPB.MtcSubtreeID = 5 @@ -4673,24 +4673,24 @@ func (ra *mockRAWithGetOrder) testOrderPB(id int64, typ string) *corepb.Order { // test order fixtures. Otherwise, GetOrder returns a berrors.NotFoundError func (ra *mockRAWithGetOrder) GetOrder(_ context.Context, req *rapb.GetOrderRequest, _ ...grpc.CallOption) (*corepb.Order, error) { validClassicOrder := ra.testOrderPB(req.OrderID, "classic") - validMerkleOrder := ra.testOrderPB(req.OrderID, "merkle") + validMTCOrder := ra.testOrderPB(req.OrderID, "mtc") // modify Status and some Mtc details for a not-yet-valid order - notYetValidMerkleOrder := ra.testOrderPB(req.OrderID, "merkle") - notYetValidMerkleOrder.Status = string(core.StatusProcessing) - notYetValidMerkleOrder.MtcSerialNumber = 0 - notYetValidMerkleOrder.MtcSubtreeID = 0 + notYetValidMTCOrder := ra.testOrderPB(req.OrderID, "mtc") + notYetValidMTCOrder.Status = string(core.StatusProcessing) + notYetValidMTCOrder.MtcSerialNumber = 0 + notYetValidMTCOrder.MtcSubtreeID = 0 // modify MtcSerialNumber to trigger the mock MTCB to return an error - validMerkleOrderMissingStandalone := ra.testOrderPB(req.OrderID, "merkle") - validMerkleOrderMissingStandalone.MtcSerialNumber = 666 + validMTCOrderMissingStandalone := ra.testOrderPB(req.OrderID, "mtc") + validMTCOrderMissingStandalone.MtcSerialNumber = 666 var orders = []*corepb.Order{ {}, // index zero is empty validClassicOrder, - validMerkleOrder, - notYetValidMerkleOrder, - validMerkleOrderMissingStandalone, + validMTCOrder, + notYetValidMTCOrder, + validMTCOrderMissingStandalone, } if 0 < req.OrderID && req.OrderID <= 4 { return orders[req.OrderID], nil @@ -4698,7 +4698,7 @@ func (ra *mockRAWithGetOrder) GetOrder(_ context.Context, req *rapb.GetOrderRequ return nil, berrors.NotFoundError("No order found for ID %d", req.OrderID) } -func TestMerkleGetOrder(t *testing.T) { +func TestMTCGetOrder(t *testing.T) { t.Cleanup(features.Reset) fc := clock.NewFake() @@ -4714,7 +4714,7 @@ func TestMerkleGetOrder(t *testing.T) { }{ { // testCase 0 is a special case tested before the wfe is wired-up with an MTCB - name: "Merkle Order without MTCBService", + name: "MTC Order without MTCBService", path: "1/2", expect: `{ "status": "valid", "expires": "` + orderExpiry + `", "identifiers": [ { "type": "dns", "value": "example.com" } ], "authorizations": [ "http://localhost/acme/authz/1/1", "http://localhost/acme/authz/1/2" ], "finalize": "http://localhost/acme/finalize/1/2", "profile": "default" }`, }, @@ -4724,12 +4724,12 @@ func TestMerkleGetOrder(t *testing.T) { expect: `{ "status": "valid", "expires": "` + orderExpiry + `", "identifiers": [ { "type": "dns", "value": "example.com" } ], "authorizations": [ "http://localhost/acme/authz/1/1", "http://localhost/acme/authz/1/2" ], "finalize": "http://localhost/acme/finalize/1/1", "profile": "default", "certificate": "http://localhost/acme/cert/4" }`, }, { - name: "Merkle Order", + name: "MTC Order", path: "1/2", - expect: `{ "status": "valid", "expires": "` + orderExpiry + `", "identifiers": [ { "type": "dns", "value": "example.com" } ], "authorizations": [ "http://localhost/acme/authz/1/1", "http://localhost/acme/authz/1/2" ], "finalize": "http://localhost/acme/finalize/1/2", "profile": "default", "merkle-certificate": "http://localhost/acme/merkle-cert/1/2/44947.4.1.0.3/4" }`, + expect: `{ "status": "valid", "expires": "` + orderExpiry + `", "identifiers": [ { "type": "dns", "value": "example.com" } ], "authorizations": [ "http://localhost/acme/authz/1/1", "http://localhost/acme/authz/1/2" ], "finalize": "http://localhost/acme/finalize/1/2", "profile": "default", "certificate": "http://localhost/acme/mtc/1/2/44947.4.1.0.3/4" }`, }, { - name: "Not Ready Merkle Order", + name: "Not Ready MTC Order", path: "1/3", expect: `{ "status": "processing", "expires": "` + orderExpiry + `", "identifiers": [ { "type": "dns", "value": "example.com" } ], "authorizations": [ "http://localhost/acme/authz/1/1", "http://localhost/acme/authz/1/2" ], "finalize": "http://localhost/acme/finalize/1/3", "profile": "default" }`, }, @@ -4793,7 +4793,7 @@ func (mtcb *mockMTCBWithGetStandalone) GetStandalone(_ context.Context, req *mtc return &mtcbpb.StandaloneResponse{CertDER: []byte{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 29, 23, 19, 17, 13, 11, 7, 5, 3, 2}}, nil } -func TestMerkleCertificate(t *testing.T) { +func TestMerkleTreeCertificate(t *testing.T) { t.Parallel() type expectResponse struct { @@ -4815,7 +4815,7 @@ func TestMerkleCertificate(t *testing.T) { }, }, { - name: "Request using wrong Account ID for extant Merkle Order", + name: "Request using wrong Account ID for extant MTC Order", pathAdd: "9/2/44947.4.1.0.3/4", expResp: expectResponse{ isJSON: true, @@ -4831,11 +4831,11 @@ func TestMerkleCertificate(t *testing.T) { }, }, { - name: "Request using wrong Log ID for extant Merkle Order", + name: "Request using wrong Log ID for extant MTC Order", pathAdd: "1/2/44947.4.1.0.9/4", expResp: expectResponse{ isJSON: true, - expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "Merkle Cert URL details do not match order details", "status": 400}`, + expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "MTC request URL details do not match order details", "status": 400}`, }, }, { @@ -4843,15 +4843,15 @@ func TestMerkleCertificate(t *testing.T) { pathAdd: "1/1/44947.4.1.0.9/4", expResp: expectResponse{ isJSON: true, - expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "No merkle certificate for order ID 1", "status": 404}`, + expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "No merkle tree certificate for order ID 1", "status": 404}`, }, }, { - name: "Request using wrong Serial Number for extant Merkle Order", + name: "Request using wrong Serial Number for extant MTC Order", pathAdd: "1/2/44947.4.1.0.3/9", expResp: expectResponse{ isJSON: true, - expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "Merkle Cert URL details do not match order details", "status": 400}`, + expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "MTC request URL details do not match order details", "status": 400}`, }, }, { @@ -4859,7 +4859,7 @@ func TestMerkleCertificate(t *testing.T) { pathAdd: "1/1/44947.4.1.0.3/9", expResp: expectResponse{ isJSON: true, - expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "No merkle certificate for order ID 1", "status": 404}`, + expect: `{"type": "urn:ietf:params:acme:error:malformed", "detail": "No merkle tree certificate for order ID 1", "status": 404}`, }, }, { @@ -4867,7 +4867,7 @@ func TestMerkleCertificate(t *testing.T) { pathAdd: "1/4/44947.4.1.0.3/666", expResp: expectResponse{ isJSON: true, - expect: `{"type": "urn:ietf:params:acme:error:serverInternal", "detail": "Failed to retrieve Merkle Certificate", "status": 500}`, + expect: `{"type": "urn:ietf:params:acme:error:serverInternal", "detail": "Failed to retrieve merkle tree certificate", "status": 500}`, }, }, { @@ -4903,10 +4903,10 @@ func TestMerkleCertificate(t *testing.T) { mockRA.clk = fc wfe.ra = mockRA - // Test that a merkle-cert path request to a WFE without an MTCB Client - // correctly returns "Not Supported" + // Test that an MTC path request to a WFE without an MTCB Client correctly + // returns "Not Supported" responseWriter := httptest.NewRecorder() - requestPath := merkleCertPath + "1/2/44947.4.1.0.3/4" + requestPath := mtcPath + "1/2/44947.4.1.0.3/4" mux.ServeHTTP(responseWriter, makePost(1, requestPath, "")) test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), `{ "type": "urn:ietf:params:acme:error:malformed", "detail": "Not Supported", "status": 400 }`) @@ -4919,7 +4919,7 @@ func TestMerkleCertificate(t *testing.T) { t.Parallel() responseWriter := httptest.NewRecorder() - requestPath := merkleCertPath + tc.pathAdd + requestPath := mtcPath + tc.pathAdd mux.ServeHTTP(responseWriter, makePost(1, requestPath, "")) if tc.expResp.isJSON {