WFE: Add support for certificate profiles (#7373)
- Parse and validate the `profile` field in `newOrder` requests. - Pass the `profile` field from `newOrder` calls to the resulting `RA.NewOrder` call. - When the client requests a specific profile, ensure that the profile field is populated in the order returned. Fixes #7332 Part of #7309
This commit is contained in:
parent
206c35f099
commit
c6b50558e6
|
|
@ -168,6 +168,12 @@ type Config struct {
|
|||
// CP/CPS, under "DV-SSL Subscriber Certificate". The value must match
|
||||
// the CA and RA configurations.
|
||||
MaxNames int `validate:"min=0,max=100"`
|
||||
|
||||
// CertificateProfileNames is the list of acceptable certificate profile
|
||||
// names for newOrder requests. Requests with a profile name not in this
|
||||
// list will be rejected. This field is optional; if unset, no profile
|
||||
// names are accepted.
|
||||
CertificateProfileNames []string `validate:"omitempty,dive,alphanum,min=1,max=32"`
|
||||
}
|
||||
|
||||
Syslog cmd.SyslogConfig
|
||||
|
|
@ -409,6 +415,7 @@ func main() {
|
|||
limiter,
|
||||
txnBuilder,
|
||||
maxNames,
|
||||
c.WFE.CertificateProfileNames,
|
||||
)
|
||||
cmd.FailOnError(err, "Unable to create WFE")
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,10 @@
|
|||
"features": {
|
||||
"ServeRenewalInfo": true,
|
||||
"TrackReplacementCertificatesARI": true
|
||||
}
|
||||
},
|
||||
"certificateProfileNames": [
|
||||
"defaultBoulderCertificateProfile"
|
||||
]
|
||||
},
|
||||
"syslog": {
|
||||
"stdoutlevel": 4,
|
||||
|
|
|
|||
39
wfe2/wfe.go
39
wfe2/wfe.go
|
|
@ -14,6 +14,7 @@ import (
|
|||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -171,6 +172,11 @@ type WebFrontEndImpl struct {
|
|||
limiter *ratelimits.Limiter
|
||||
txnBuilder *ratelimits.TransactionBuilder
|
||||
maxNames int
|
||||
|
||||
// certificateProfileNames is a list of profile names that are allowed to be
|
||||
// passed to the newOrder endpoint. If a profile name is not in this list,
|
||||
// the request will be rejected as malformed.
|
||||
certificateProfileNames []string
|
||||
}
|
||||
|
||||
// NewWebFrontEndImpl constructs a web service for Boulder
|
||||
|
|
@ -195,6 +201,7 @@ func NewWebFrontEndImpl(
|
|||
limiter *ratelimits.Limiter,
|
||||
txnBuilder *ratelimits.TransactionBuilder,
|
||||
maxNames int,
|
||||
certificateProfileNames []string,
|
||||
) (WebFrontEndImpl, error) {
|
||||
if len(issuerCertificates) == 0 {
|
||||
return WebFrontEndImpl{}, errors.New("must provide at least one issuer certificate")
|
||||
|
|
@ -234,6 +241,7 @@ func NewWebFrontEndImpl(
|
|||
limiter: limiter,
|
||||
txnBuilder: txnBuilder,
|
||||
maxNames: maxNames,
|
||||
certificateProfileNames: certificateProfileNames,
|
||||
}
|
||||
|
||||
return wfe, nil
|
||||
|
|
@ -2002,6 +2010,7 @@ type orderJSON struct {
|
|||
Identifiers []identifier.ACMEIdentifier `json:"identifiers"`
|
||||
Authorizations []string `json:"authorizations"`
|
||||
Finalize string `json:"finalize"`
|
||||
Profile string `json:"profile,omitempty"`
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
Error *probs.ProblemDetails `json:"error,omitempty"`
|
||||
}
|
||||
|
|
@ -2253,6 +2262,19 @@ func (wfe *WebFrontEndImpl) validateReplacementOrder(ctx context.Context, acct *
|
|||
return replaces, renewalInfo.SuggestedWindow.IsWithin(wfe.clk.Now()), nil
|
||||
}
|
||||
|
||||
func (wfe *WebFrontEndImpl) validateCertificateProfileName(profile string) error {
|
||||
if profile == "" {
|
||||
// No profile name is specified.
|
||||
return nil
|
||||
}
|
||||
if !slices.Contains(wfe.certificateProfileNames, profile) {
|
||||
// The profile name is not in the list of configured profiles.
|
||||
return errors.New("not a recognized profile name")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewOrder is used by clients to create a new order object and a set of
|
||||
// authorizations to fulfill for issuance.
|
||||
func (wfe *WebFrontEndImpl) NewOrder(
|
||||
|
|
@ -2276,6 +2298,7 @@ func (wfe *WebFrontEndImpl) NewOrder(
|
|||
NotBefore string
|
||||
NotAfter string
|
||||
Replaces string
|
||||
Profile string
|
||||
}
|
||||
err := json.Unmarshal(body, &newOrderRequest)
|
||||
if err != nil {
|
||||
|
|
@ -2326,6 +2349,13 @@ func (wfe *WebFrontEndImpl) NewOrder(
|
|||
}
|
||||
}
|
||||
|
||||
err = wfe.validateCertificateProfileName(newOrderRequest.Profile)
|
||||
if err != nil {
|
||||
// TODO(#7392) Provide link to profile documentation.
|
||||
wfe.sendError(response, logEvent, probs.Malformed("Invalid certificate profile, %q: %s", newOrderRequest.Profile, err), err)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(#5545): Spending and Refunding can be async until these rate limits
|
||||
// are authoritative. This saves us from adding latency to each request.
|
||||
// Goroutines spun out below will respect a context deadline set by the
|
||||
|
|
@ -2348,10 +2378,11 @@ func (wfe *WebFrontEndImpl) NewOrder(
|
|||
}()
|
||||
|
||||
order, err := wfe.ra.NewOrder(ctx, &rapb.NewOrderRequest{
|
||||
RegistrationID: acct.ID,
|
||||
Names: names,
|
||||
ReplacesSerial: replaces,
|
||||
LimitsExempt: limitsExempt,
|
||||
RegistrationID: acct.ID,
|
||||
Names: names,
|
||||
ReplacesSerial: replaces,
|
||||
LimitsExempt: limitsExempt,
|
||||
CertificateProfileName: newOrderRequest.Profile,
|
||||
})
|
||||
// TODO(#7153): Check each value via core.IsAnyNilOrZero
|
||||
if err != nil || order == nil || order.Id == 0 || order.RegistrationID == 0 || len(order.Names) == 0 || core.IsAnyNilOrZero(order.Created, order.Expires) {
|
||||
|
|
|
|||
|
|
@ -427,6 +427,7 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock, requestSigner) {
|
|||
limiter,
|
||||
txnBuilder,
|
||||
100,
|
||||
[]string{""},
|
||||
)
|
||||
test.AssertNotError(t, err, "Unable to create WFE")
|
||||
|
||||
|
|
@ -4053,3 +4054,87 @@ func TestOrderMatchesReplacement(t *testing.T) {
|
|||
err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 1}, []string{"example.com"}, "1")
|
||||
test.AssertErrorIs(t, err, berrors.NotFound)
|
||||
}
|
||||
|
||||
type mockRA struct {
|
||||
rapb.RegistrationAuthorityClient
|
||||
expectProfileName string
|
||||
}
|
||||
|
||||
// NewOrder returns an error if the ""
|
||||
func (sa *mockRA) NewOrder(ctx context.Context, in *rapb.NewOrderRequest, opts ...grpc.CallOption) (*corepb.Order, error) {
|
||||
if in.CertificateProfileName != sa.expectProfileName {
|
||||
return nil, errors.New("not expected profile name")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
created := now.AddDate(-30, 0, 0)
|
||||
exp := now.AddDate(30, 0, 0)
|
||||
return &corepb.Order{
|
||||
Id: 123456789,
|
||||
RegistrationID: 987654321,
|
||||
Created: timestamppb.New(created),
|
||||
Expires: timestamppb.New(exp),
|
||||
Names: []string{"example.com"},
|
||||
Status: string(core.StatusValid),
|
||||
V2Authorizations: []int64{1},
|
||||
CertificateSerial: "serial",
|
||||
Error: nil,
|
||||
CertificateProfileName: in.CertificateProfileName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestNewOrderWithProfile(t *testing.T) {
|
||||
wfe, _, signer := setupWFE(t)
|
||||
expectProfileName := "test-profile"
|
||||
wfe.ra = &mockRA{expectProfileName: expectProfileName}
|
||||
mux := wfe.Handler(metrics.NoopRegisterer)
|
||||
wfe.certificateProfileNames = []string{expectProfileName}
|
||||
|
||||
// Test that the newOrder endpoint returns the proper error if an invalid
|
||||
// profile is specified.
|
||||
invalidOrderBody := `
|
||||
{
|
||||
"Identifiers": [
|
||||
{"type": "dns", "value": "example.com"}
|
||||
],
|
||||
"Profile": "bad-profile"
|
||||
}`
|
||||
|
||||
responseWriter := httptest.NewRecorder()
|
||||
r := signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, invalidOrderBody)
|
||||
mux.ServeHTTP(responseWriter, r)
|
||||
test.AssertEquals(t, responseWriter.Code, http.StatusBadRequest)
|
||||
var errorResp map[string]interface{}
|
||||
err := json.Unmarshal(responseWriter.Body.Bytes(), &errorResp)
|
||||
test.AssertNotError(t, err, "Failed to unmarshal error response")
|
||||
test.AssertEquals(t, errorResp["type"], "urn:ietf:params:acme:error:malformed")
|
||||
test.AssertEquals(t, errorResp["detail"], "Invalid certificate profile, \"bad-profile\": not a recognized profile name")
|
||||
|
||||
// Test that the newOrder endpoint returns no error if the valid profile is specified.
|
||||
validOrderBody := `
|
||||
{
|
||||
"Identifiers": [
|
||||
{"type": "dns", "value": "example.com"}
|
||||
],
|
||||
"Profile": "test-profile"
|
||||
}`
|
||||
responseWriter = httptest.NewRecorder()
|
||||
r = signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, validOrderBody)
|
||||
mux.ServeHTTP(responseWriter, r)
|
||||
test.AssertEquals(t, responseWriter.Code, http.StatusCreated)
|
||||
var errorResp1 map[string]interface{}
|
||||
err = json.Unmarshal(responseWriter.Body.Bytes(), &errorResp1)
|
||||
test.AssertNotError(t, err, "Failed to unmarshal order response")
|
||||
test.AssertEquals(t, errorResp1["status"], "valid")
|
||||
|
||||
// Set the acceptable profiles to an empty list, the WFE should no longer accept any profiles.
|
||||
wfe.certificateProfileNames = []string{}
|
||||
responseWriter = httptest.NewRecorder()
|
||||
r = signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, validOrderBody)
|
||||
mux.ServeHTTP(responseWriter, r)
|
||||
test.AssertEquals(t, responseWriter.Code, http.StatusBadRequest)
|
||||
var errorResp2 map[string]interface{}
|
||||
err = json.Unmarshal(responseWriter.Body.Bytes(), &errorResp2)
|
||||
test.AssertNotError(t, err, "Failed to unmarshal error response")
|
||||
test.AssertEquals(t, errorResp2["type"], "urn:ietf:params:acme:error:malformed")
|
||||
test.AssertEquals(t, errorResp2["detail"], "Invalid certificate profile, \"test-profile\": not a recognized profile name")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue