WFE: Count NewOrders which indicate replacement (#7416)

Add support for counting new orders which indicate replacement according
to draft-ietf-acme-ari.

Fixes #7405
This commit is contained in:
Samantha 2024-04-08 12:32:45 -04:00 committed by GitHub
parent 70d1887eea
commit a88bd68ead
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 116 additions and 0 deletions

View File

@ -20,6 +20,11 @@ type wfe2Stats struct {
// nonceNoMatchingBackendCount counts the number of times we've received a nonce
// with a prefix that doesn't match a known backend.
nonceNoMatchingBackendCount prometheus.Counter
// ariReplacementOrders counts the number of new order requests that replace
// an existing order, labeled by:
// - isReplacement=[true|false]
// - limitsExempt=[true|false]
ariReplacementOrders *prometheus.CounterVec
}
func initStats(stats prometheus.Registerer) wfe2Stats {
@ -64,11 +69,21 @@ func initStats(stats prometheus.Registerer) wfe2Stats {
)
stats.MustRegister(nonceNoBackendCount)
ariReplacementOrders := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "ari_replacements",
Help: "Number of new order requests that replace an existing order, labeled isReplacement=[true|false], limitsExempt=[true|false]",
},
[]string{"isReplacement", "limitsExempt"},
)
stats.MustRegister(ariReplacementOrders)
return wfe2Stats{
httpErrorCount: httpErrorCount,
joseErrorCount: joseErrorCount,
csrSignatureAlgs: csrSignatureAlgs,
improperECFieldLengths: improperECFieldLengths,
nonceNoMatchingBackendCount: nonceNoBackendCount,
ariReplacementOrders: ariReplacementOrders,
}
}

View File

@ -2372,6 +2372,13 @@ func (wfe *WebFrontEndImpl) NewOrder(
var newOrderSuccessful bool
var errIsRateLimit bool
defer func() {
if features.Get().TrackReplacementCertificatesARI {
wfe.stats.ariReplacementOrders.With(prometheus.Labels{
"isReplacement": fmt.Sprintf("%t", replaces != ""),
"limitsExempt": fmt.Sprintf("%t", limitsExempt),
}).Inc()
}
if !newOrderSuccessful && !errIsRateLimit {
// This can be a little racy, but we're not going to worry about it
// for now. If the check hasn't completed yet, we can pretty safely

View File

@ -4012,6 +4012,22 @@ func (sa *mockSA) GetCertificate(ctx context.Context, req *sapb.Serial, _ ...grp
return nil, berrors.NotFoundError("certificate with serial %q not found", req.Serial)
}
func (sa *mockSA) ReplacementOrderExists(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.Exists, error) {
if in.Serial == sa.cert.Serial {
return &sapb.Exists{Exists: false}, nil
}
return &sapb.Exists{Exists: true}, nil
}
func (sa *mockSA) IncidentsForSerial(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.Incidents, error) {
return &sapb.Incidents{}, nil
}
func (sa *mockSA) GetCertificateStatus(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*corepb.CertificateStatus, error) {
return &corepb.CertificateStatus{Serial: in.Serial, Status: string(core.OCSPStatusGood)}, nil
}
func TestOrderMatchesReplacement(t *testing.T) {
wfe, _, _ := setupWFE(t)
@ -4138,3 +4154,81 @@ func TestNewOrderWithProfile(t *testing.T) {
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")
}
func makeARICertID(leaf *x509.Certificate) (string, error) {
if leaf == nil {
return "", errors.New("leaf certificate is nil")
}
// Marshal the Serial Number into DER.
der, err := asn1.Marshal(leaf.SerialNumber)
if err != nil {
return "", err
}
// Check if the DER encoded bytes are sufficient (at least 3 bytes: tag,
// length, and value).
if len(der) < 3 {
return "", errors.New("invalid DER encoding of serial number")
}
// Extract only the integer bytes from the DER encoded Serial Number
// Skipping the first 2 bytes (tag and length). The result is base64url
// encoded without padding.
serial := base64.RawURLEncoding.EncodeToString(der[2:])
// Convert the Authority Key Identifier to base64url encoding without
// padding.
aki := base64.RawURLEncoding.EncodeToString(leaf.AuthorityKeyId)
// Construct the final identifier by concatenating AKI and Serial Number.
return fmt.Sprintf("%s.%s", aki, serial), nil
}
func TestCountNewOrderWithReplaces(t *testing.T) {
wfe, _, signer := setupWFE(t)
features.Set(features.Config{TrackReplacementCertificatesARI: true})
expectExpiry := time.Now().AddDate(0, 0, 1)
var expectAKID []byte
for _, v := range wfe.issuerCertificates {
expectAKID = v.SubjectKeyId
break
}
testKey, _ := rsa.GenerateKey(rand.Reader, 1024)
expectSerial := big.NewInt(1337)
expectCert := &x509.Certificate{
NotAfter: expectExpiry,
DNSNames: []string{"example.com"},
SerialNumber: expectSerial,
AuthorityKeyId: expectAKID,
}
expectCertId, err := makeARICertID(expectCert)
test.AssertNotError(t, err, "failed to create test cert id")
expectDer, err := x509.CreateCertificate(rand.Reader, expectCert, expectCert, &testKey.PublicKey, testKey)
test.AssertNotError(t, err, "failed to create test certificate")
// MockSA that returns the certificate with the expected serial.
wfe.sa = &mockSA{
cert: &corepb.Certificate{
RegistrationID: 1,
Serial: core.SerialToString(expectSerial),
Der: expectDer,
},
}
mux := wfe.Handler(metrics.NoopRegisterer)
responseWriter := httptest.NewRecorder()
body := fmt.Sprintf(`
{
"Identifiers": [
{"type": "dns", "value": "example.com"}
],
"Replaces": %q
}`, expectCertId)
r := signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, body)
mux.ServeHTTP(responseWriter, r)
test.AssertEquals(t, responseWriter.Code, http.StatusCreated)
test.AssertMetricWithLabelsEquals(t, wfe.stats.ariReplacementOrders, prometheus.Labels{"isReplacement": "true", "limitsExempt": "true"}, 1)
}