package ra import ( "bytes" "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/hex" "encoding/json" "encoding/pem" "errors" "fmt" "math" "math/big" mrand "math/rand/v2" "net/netip" "regexp" "strconv" "strings" "sync" "testing" "time" "github.com/go-jose/go-jose/v4" "github.com/jmhodges/clock" "github.com/prometheus/client_golang/prometheus" "golang.org/x/crypto/ocsp" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" akamaipb "github.com/letsencrypt/boulder/akamai/proto" "github.com/letsencrypt/boulder/allowlist" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/ctpolicy" "github.com/letsencrypt/boulder/ctpolicy/loglist" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/goodkey" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/issuance" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/mocks" "github.com/letsencrypt/boulder/policy" pubpb "github.com/letsencrypt/boulder/publisher/proto" rapb "github.com/letsencrypt/boulder/ra/proto" "github.com/letsencrypt/boulder/ratelimits" "github.com/letsencrypt/boulder/sa" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/test" isa "github.com/letsencrypt/boulder/test/inmem/sa" "github.com/letsencrypt/boulder/test/vars" "github.com/letsencrypt/boulder/va" vapb "github.com/letsencrypt/boulder/va/proto" ) // randomDomain creates a random domain name for testing. // // panics if crypto/rand.Rand.Read fails. func randomDomain() string { var bytes [4]byte _, err := rand.Read(bytes[:]) if err != nil { panic(err) } return fmt.Sprintf("%x.example.com", bytes[:]) } // randomIPv6 creates a random IPv6 netip.Addr for testing. It uses a real IPv6 // address range, not a test/documentation range. // // panics if crypto/rand.Rand.Read or netip.AddrFromSlice fails. func randomIPv6() netip.Addr { var ipBytes [10]byte _, err := rand.Read(ipBytes[:]) if err != nil { panic(err) } ipPrefix, err := hex.DecodeString("2602080a600f") if err != nil { panic(err) } ip, ok := netip.AddrFromSlice(bytes.Join([][]byte{ipPrefix, ipBytes[:]}, nil)) if !ok { panic("Couldn't parse random IP to netip.Addr") } return ip } func createPendingAuthorization(t *testing.T, sa sapb.StorageAuthorityClient, ident identifier.ACMEIdentifier, exp time.Time) *corepb.Authorization { t.Helper() res, err := sa.NewOrderAndAuthzs( context.Background(), &sapb.NewOrderAndAuthzsRequest{ NewOrder: &sapb.NewOrderRequest{ RegistrationID: Registration.Id, Expires: timestamppb.New(exp), Identifiers: []*corepb.Identifier{ident.ToProto()}, }, NewAuthzs: []*sapb.NewAuthzRequest{ { Identifier: ident.ToProto(), RegistrationID: Registration.Id, Expires: timestamppb.New(exp), ChallengeTypes: []string{ string(core.ChallengeTypeHTTP01), string(core.ChallengeTypeDNS01), string(core.ChallengeTypeTLSALPN01)}, Token: core.NewToken(), }, }, }, ) test.AssertNotError(t, err, "sa.NewOrderAndAuthzs failed") return getAuthorization(t, fmt.Sprint(res.V2Authorizations[0]), sa) } func createFinalizedAuthorization(t *testing.T, sa sapb.StorageAuthorityClient, ident identifier.ACMEIdentifier, exp time.Time, chall core.AcmeChallenge, attemptedAt time.Time) int64 { t.Helper() pending := createPendingAuthorization(t, sa, ident, exp) pendingID, err := strconv.ParseInt(pending.Id, 10, 64) test.AssertNotError(t, err, "strconv.ParseInt failed") _, err = sa.FinalizeAuthorization2(context.Background(), &sapb.FinalizeAuthorizationRequest{ Id: pendingID, Status: "valid", Expires: timestamppb.New(exp), Attempted: string(chall), AttemptedAt: timestamppb.New(attemptedAt), }) test.AssertNotError(t, err, "sa.FinalizeAuthorizations2 failed") return pendingID } func getAuthorization(t *testing.T, id string, sa sapb.StorageAuthorityClient) *corepb.Authorization { t.Helper() idInt, err := strconv.ParseInt(id, 10, 64) test.AssertNotError(t, err, "strconv.ParseInt failed") dbAuthz, err := sa.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: idInt}) test.AssertNotError(t, err, "Could not fetch authorization from database") return dbAuthz } func dnsChallIdx(t *testing.T, challenges []*corepb.Challenge) int64 { t.Helper() var challIdx int64 var set bool for i, ch := range challenges { if core.AcmeChallenge(ch.Type) == core.ChallengeTypeDNS01 { challIdx = int64(i) set = true break } } if !set { t.Errorf("dnsChallIdx didn't find challenge of type DNS-01") } return challIdx } func numAuthorizations(o *corepb.Order) int { return len(o.V2Authorizations) } // def is a test-only helper that returns the default validation profile // and is guaranteed to succeed because the validationProfile constructor // ensures that the default name has a corresponding profile. func (vp *validationProfiles) def() *validationProfile { return vp.byName[vp.defaultName] } type DummyValidationAuthority struct { doDCVRequest chan *vapb.PerformValidationRequest doDCVError error doDCVResult *vapb.ValidationResult doCAARequest chan *vapb.IsCAAValidRequest doCAAError error doCAAResponse *vapb.IsCAAValidResponse } func (dva *DummyValidationAuthority) PerformValidation(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { dcvRes, err := dva.DoDCV(ctx, req) if err != nil { return nil, err } if dcvRes.Problem != nil { return dcvRes, nil } caaResp, err := dva.DoCAA(ctx, &vapb.IsCAAValidRequest{ Identifier: req.Identifier, ValidationMethod: req.Challenge.Type, AccountURIID: req.Authz.RegID, AuthzID: req.Authz.Id, }) if err != nil { return nil, err } return &vapb.ValidationResult{ Records: dcvRes.Records, Problem: caaResp.Problem, }, nil } func (dva *DummyValidationAuthority) IsCAAValid(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { return nil, status.Error(codes.Unimplemented, "IsCAAValid not implemented") } func (dva *DummyValidationAuthority) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { dva.doDCVRequest <- req return dva.doDCVResult, dva.doDCVError } func (dva *DummyValidationAuthority) DoCAA(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { dva.doCAARequest <- req return dva.doCAAResponse, dva.doCAAError } var ( // These values we simulate from the client AccountKeyJSONA = []byte(`{ "kty":"RSA", "n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", "e":"AQAB" }`) AccountKeyA = jose.JSONWebKey{} AccountKeyJSONB = []byte(`{ "kty":"RSA", "n":"z8bp-jPtHt4lKBqepeKF28g_QAEOuEsCIou6sZ9ndsQsEjxEOQxQ0xNOQezsKa63eogw8YS3vzjUcPP5BJuVzfPfGd5NVUdT-vSSwxk3wvk_jtNqhrpcoG0elRPQfMVsQWmxCAXCVRz3xbcFI8GTe-syynG3l-g1IzYIIZVNI6jdljCZML1HOMTTW4f7uJJ8mM-08oQCeHbr5ejK7O2yMSSYxW03zY-Tj1iVEebROeMv6IEEJNFSS4yM-hLpNAqVuQxFGetwtwjDMC1Drs1dTWrPuUAAjKGrP151z1_dE74M5evpAhZUmpKv1hY-x85DC6N0hFPgowsanmTNNiV75w", "e":"AQAB" }`) AccountKeyB = jose.JSONWebKey{} AccountKeyJSONC = []byte(`{ "kty":"RSA", "n":"rFH5kUBZrlPj73epjJjyCxzVzZuV--JjKgapoqm9pOuOt20BUTdHqVfC2oDclqM7HFhkkX9OSJMTHgZ7WaVqZv9u1X2yjdx9oVmMLuspX7EytW_ZKDZSzL-sCOFCuQAuYKkLbsdcA3eHBK_lwc4zwdeHFMKIulNvLqckkqYB9s8GpgNXBDIQ8GjR5HuJke_WUNjYHSd8jY1LU9swKWsLQe2YoQUz_ekQvBvBCoaFEtrtRaSJKNLIVDObXFr2TLIiFiM0Em90kK01-eQ7ZiruZTKomll64bRFPoNo4_uwubddg3xTqur2vdF3NyhTrYdvAgTem4uC0PFjEQ1bK_djBQ", "e":"AQAB" }`) AccountKeyC = jose.JSONWebKey{} // These values we simulate from the client AccountPrivateKeyJSON = []byte(`{ "kty":"RSA", "n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", "e":"AQAB", "d":"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q", "p":"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs", "q":"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk", "dp":"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0", "dq":"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk", "qi":"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU" }`) AccountPrivateKey = jose.JSONWebKey{} ShortKeyJSON = []byte(`{ "e": "AQAB", "kty": "RSA", "n": "tSwgy3ORGvc7YJI9B2qqkelZRUC6F1S5NwXFvM4w5-M0TsxbFsH5UH6adigV0jzsDJ5imAechcSoOhAh9POceCbPN1sTNwLpNbOLiQQ7RD5mY_" }`) ShortKey = jose.JSONWebKey{} ResponseIndex = 0 ExampleCSR = &x509.CertificateRequest{} Registration = &corepb.Registration{Id: 1} Identifier = "not-example.com" log = blog.UseMock() ) var ctx = context.Background() func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAuthorityClient, *RegistrationAuthorityImpl, ratelimits.Source, clock.FakeClock, func()) { err := json.Unmarshal(AccountKeyJSONA, &AccountKeyA) test.AssertNotError(t, err, "Failed to unmarshal public JWK") err = json.Unmarshal(AccountKeyJSONB, &AccountKeyB) test.AssertNotError(t, err, "Failed to unmarshal public JWK") err = json.Unmarshal(AccountKeyJSONC, &AccountKeyC) test.AssertNotError(t, err, "Failed to unmarshal public JWK") err = json.Unmarshal(AccountPrivateKeyJSON, &AccountPrivateKey) test.AssertNotError(t, err, "Failed to unmarshal private JWK") err = json.Unmarshal(ShortKeyJSON, &ShortKey) test.AssertNotError(t, err, "Failed to unmarshal JWK") fc := clock.NewFake() // Set to some non-zero time. fc.Set(time.Date(2020, 3, 4, 5, 0, 0, 0, time.UTC)) dbMap, err := sa.DBMapForTest(vars.DBConnSA) if err != nil { t.Fatalf("Failed to create dbMap: %s", err) } ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, 1, 0, fc, log, metrics.NoopRegisterer) if err != nil { t.Fatalf("Failed to create SA: %s", err) } sa := &isa.SA{Impl: ssa} saDBCleanUp := test.ResetBoulderTestDatabase(t) dummyVA := &DummyValidationAuthority{ doDCVRequest: make(chan *vapb.PerformValidationRequest, 1), doCAARequest: make(chan *vapb.IsCAAValidRequest, 1), } va := va.RemoteClients{VAClient: dummyVA, CAAClient: dummyVA} pa, err := policy.New( map[identifier.IdentifierType]bool{ identifier.TypeDNS: true, identifier.TypeIP: true, }, map[core.AcmeChallenge]bool{ core.ChallengeTypeHTTP01: true, core.ChallengeTypeDNS01: true, }, blog.NewMock()) test.AssertNotError(t, err, "Couldn't create PA") err = pa.LoadHostnamePolicyFile("../test/hostname-policy.yaml") test.AssertNotError(t, err, "Couldn't set hostname policy") stats := metrics.NoopRegisterer ca := &mocks.MockCA{ PEM: eeCertPEM, } cleanUp := func() { saDBCleanUp() } block, _ := pem.Decode(CSRPEM) ExampleCSR, _ = x509.ParseCertificateRequest(block.Bytes) test.AssertNotError(t, err, "Couldn't create initial IP") Registration, _ = ssa.NewRegistration(ctx, &corepb.Registration{ Key: AccountKeyJSONA, Status: string(core.StatusValid), }) ctp := ctpolicy.New(&mocks.PublisherClient{}, loglist.List{ {Name: "LogA1", Operator: "OperA", Url: "UrlA1", Key: []byte("KeyA1")}, {Name: "LogB1", Operator: "OperB", Url: "UrlB1", Key: []byte("KeyB1")}, }, nil, nil, 0, log, metrics.NoopRegisterer) rlSource := ratelimits.NewInmemSource() limiter, err := ratelimits.NewLimiter(fc, rlSource, stats) test.AssertNotError(t, err, "making limiter") txnBuilder, err := ratelimits.NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "") test.AssertNotError(t, err, "making transaction composer") testKeyPolicy, err := goodkey.NewPolicy(nil, nil) test.AssertNotError(t, err, "making keypolicy") profiles := &validationProfiles{ defaultName: "test", byName: map[string]*validationProfile{"test": { pendingAuthzLifetime: 7 * 24 * time.Hour, validAuthzLifetime: 300 * 24 * time.Hour, orderLifetime: 7 * 24 * time.Hour, maxNames: 100, identifierTypes: []identifier.IdentifierType{identifier.TypeDNS}, }}, } ra := NewRegistrationAuthorityImpl( fc, log, stats, 1, testKeyPolicy, limiter, txnBuilder, 100, profiles, nil, 5*time.Minute, ctp, nil, nil) ra.SA = sa ra.VA = va ra.CA = ca ra.OCSP = &mocks.MockOCSPGenerator{} ra.PA = pa return dummyVA, sa, ra, rlSource, fc, cleanUp } func TestValidateContacts(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ansible := "ansible:earth.sol.milkyway.laniakea/letsencrypt" validEmail := "mailto:admin@email.com" otherValidEmail := "mailto:other-admin@email.com" malformedEmail := "mailto:admin.com" nonASCII := "mailto:seƱor@email.com" unparsable := "mailto:a@email.com, b@email.com" forbidden := "mailto:a@example.org" err := ra.validateContacts([]string{}) test.AssertNotError(t, err, "No Contacts") err = ra.validateContacts([]string{validEmail, otherValidEmail}) test.AssertError(t, err, "Too Many Contacts") err = ra.validateContacts([]string{validEmail}) test.AssertNotError(t, err, "Valid Email") err = ra.validateContacts([]string{malformedEmail}) test.AssertError(t, err, "Malformed Email") err = ra.validateContacts([]string{ansible}) test.AssertError(t, err, "Unknown scheme") err = ra.validateContacts([]string{""}) test.AssertError(t, err, "Empty URL") err = ra.validateContacts([]string{nonASCII}) test.AssertError(t, err, "Non ASCII email") err = ra.validateContacts([]string{unparsable}) test.AssertError(t, err, "Unparsable email") err = ra.validateContacts([]string{forbidden}) test.AssertError(t, err, "Forbidden email") err = ra.validateContacts([]string{"mailto:admin@localhost"}) test.AssertError(t, err, "Forbidden email") err = ra.validateContacts([]string{"mailto:admin@example.not.a.iana.suffix"}) test.AssertError(t, err, "Forbidden email") err = ra.validateContacts([]string{"mailto:admin@1.2.3.4"}) test.AssertError(t, err, "Forbidden email") err = ra.validateContacts([]string{"mailto:admin@[1.2.3.4]"}) test.AssertError(t, err, "Forbidden email") err = ra.validateContacts([]string{"mailto:admin@a.com?no-reminder-emails"}) test.AssertError(t, err, "No hfields in email") err = ra.validateContacts([]string{"mailto:example@a.com?"}) test.AssertError(t, err, "No hfields in email") err = ra.validateContacts([]string{"mailto:example@a.com#"}) test.AssertError(t, err, "No fragment") err = ra.validateContacts([]string{"mailto:example@a.com#optional"}) test.AssertError(t, err, "No fragment") // The registrations.contact field is VARCHAR(191). 175 'a' characters plus // the prefix "mailto:" and the suffix "@a.com" makes exactly 191 bytes of // encoded JSON. The correct size to hit our maximum DB field length. var longStringBuf strings.Builder longStringBuf.WriteString("mailto:") for range 175 { longStringBuf.WriteRune('a') } longStringBuf.WriteString("@a.com") err = ra.validateContacts([]string{longStringBuf.String()}) test.AssertError(t, err, "Too long contacts") } func TestNewRegistration(t *testing.T) { _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() mailto := "mailto:foo@letsencrypt.org" acctKeyB, err := AccountKeyB.MarshalJSON() test.AssertNotError(t, err, "failed to marshal account key") input := &corepb.Registration{ Contact: []string{mailto}, Key: acctKeyB, } result, err := ra.NewRegistration(ctx, input) if err != nil { t.Fatalf("could not create new registration: %s", err) } test.AssertByteEquals(t, result.Key, acctKeyB) test.Assert(t, len(result.Contact) == 0, "Wrong number of contacts") test.Assert(t, result.Agreement == "", "Agreement didn't default empty") reg, err := sa.GetRegistration(ctx, &sapb.RegistrationID{Id: result.Id}) test.AssertNotError(t, err, "Failed to retrieve registration") test.AssertByteEquals(t, reg.Key, acctKeyB) } type mockSAFailsNewRegistration struct { sapb.StorageAuthorityClient } func (sa *mockSAFailsNewRegistration) NewRegistration(_ context.Context, _ *corepb.Registration, _ ...grpc.CallOption) (*corepb.Registration, error) { return &corepb.Registration{}, fmt.Errorf("too bad") } func TestNewRegistrationSAFailure(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ra.SA = &mockSAFailsNewRegistration{} acctKeyB, err := AccountKeyB.MarshalJSON() test.AssertNotError(t, err, "failed to marshal account key") input := corepb.Registration{ Contact: []string{"mailto:test@example.com"}, Key: acctKeyB, } result, err := ra.NewRegistration(ctx, &input) if err == nil { t.Fatalf("NewRegistration should have failed when SA.NewRegistration failed %#v", result.Key) } } func TestNewRegistrationNoFieldOverwrite(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() mailto := "mailto:foo@letsencrypt.org" acctKeyC, err := AccountKeyC.MarshalJSON() test.AssertNotError(t, err, "failed to marshal account key") input := &corepb.Registration{ Id: 23, Key: acctKeyC, Contact: []string{mailto}, Agreement: "I agreed", } result, err := ra.NewRegistration(ctx, input) test.AssertNotError(t, err, "Could not create new registration") test.Assert(t, result.Id != 23, "ID shouldn't be set by user") // TODO: Enable this test case once we validate terms agreement. //test.Assert(t, result.Agreement != "I agreed", "Agreement shouldn't be set with invalid URL") } func TestNewRegistrationBadKey(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() mailto := "mailto:foo@letsencrypt.org" shortKey, err := ShortKey.MarshalJSON() test.AssertNotError(t, err, "failed to marshal account key") input := &corepb.Registration{ Contact: []string{mailto}, Key: shortKey, } _, err = ra.NewRegistration(ctx, input) test.AssertError(t, err, "Should have rejected authorization with short key") } func TestPerformValidationExpired(t *testing.T) { _, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() authz := createPendingAuthorization(t, sa, identifier.NewDNS("example.com"), fc.Now().Add(-2*time.Hour)) _, err := ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ Authz: authz, ChallengeIndex: int64(ResponseIndex), }) test.AssertError(t, err, "Updated expired authorization") } func TestPerformValidationAlreadyValid(t *testing.T) { va, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() // Create a finalized authorization exp := ra.clk.Now().Add(365 * 24 * time.Hour) authz := core.Authorization{ ID: "1337", Identifier: identifier.NewDNS("not-example.com"), RegistrationID: 1, Status: "valid", Expires: &exp, Challenges: []core.Challenge{ { Token: core.NewToken(), Type: core.ChallengeTypeHTTP01, Status: core.StatusPending, }, }, } authzPB, err := bgrpc.AuthzToPB(authz) test.AssertNotError(t, err, "bgrpc.AuthzToPB failed") va.doDCVResult = &vapb.ValidationResult{ Records: []*corepb.ValidationRecord{ { AddressUsed: []byte("192.168.0.1"), Hostname: "example.com", Port: "8080", Url: "http://example.com/", }, }, Problem: nil, } va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil} // A subsequent call to perform validation should return nil due // to being short-circuited because of valid authz reuse. val, err := ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ Authz: authzPB, ChallengeIndex: int64(ResponseIndex), }) test.Assert(t, core.AcmeStatus(val.Status) == core.StatusValid, "Validation should have been valid") test.AssertNotError(t, err, "Error was not nil, but should have been nil") } func TestPerformValidationSuccess(t *testing.T) { va, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() idents := identifier.ACMEIdentifiers{ identifier.NewDNS("example.com"), identifier.NewIP(netip.MustParseAddr("192.168.0.1")), } for _, ident := range idents { // We know this is OK because of TestNewAuthorization authzPB := createPendingAuthorization(t, sa, ident, fc.Now().Add(12*time.Hour)) va.doDCVResult = &vapb.ValidationResult{ Records: []*corepb.ValidationRecord{ { AddressUsed: []byte("192.168.0.1"), Hostname: "example.com", Port: "8080", Url: "http://example.com/", ResolverAddrs: []string{"rebound"}, }, }, Problem: nil, } va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil} now := fc.Now() challIdx := dnsChallIdx(t, authzPB.Challenges) authzPB, err := ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ Authz: authzPB, ChallengeIndex: challIdx, }) test.AssertNotError(t, err, "PerformValidation failed") var vaRequest *vapb.PerformValidationRequest select { case r := <-va.doDCVRequest: vaRequest = r case <-time.After(time.Second): t.Fatal("Timed out waiting for DummyValidationAuthority.PerformValidation to complete") } // Verify that the VA got the request, and it's the same as the others test.AssertEquals(t, authzPB.Challenges[challIdx].Type, vaRequest.Challenge.Type) test.AssertEquals(t, authzPB.Challenges[challIdx].Token, vaRequest.Challenge.Token) // Sleep so the RA has a chance to write to the SA time.Sleep(100 * time.Millisecond) dbAuthzPB := getAuthorization(t, authzPB.Id, sa) t.Log("dbAuthz:", dbAuthzPB) // Verify that the responses are reflected challIdx = dnsChallIdx(t, dbAuthzPB.Challenges) challenge, err := bgrpc.PBToChallenge(dbAuthzPB.Challenges[challIdx]) test.AssertNotError(t, err, "Failed to marshall corepb.Challenge to core.Challenge.") test.AssertNotNil(t, vaRequest.Challenge, "Request passed to VA has no challenge") test.Assert(t, challenge.Status == core.StatusValid, "challenge was not marked as valid") // The DB authz's expiry should be equal to the current time plus the // configured authorization lifetime test.AssertEquals(t, dbAuthzPB.Expires.AsTime(), now.Add(ra.profiles.def().validAuthzLifetime)) // Check that validated timestamp was recorded, stored, and retrieved expectedValidated := fc.Now() test.Assert(t, *challenge.Validated == expectedValidated, "Validated timestamp incorrect or missing") } } // mockSAWithSyncPause is a mock sapb.StorageAuthorityClient that forwards all // method calls to an inner SA, but also performs a blocking write to a channel // when PauseIdentifiers is called to allow the tests to synchronize. type mockSAWithSyncPause struct { sapb.StorageAuthorityClient out chan<- *sapb.PauseRequest } func (msa mockSAWithSyncPause) PauseIdentifiers(ctx context.Context, req *sapb.PauseRequest, _ ...grpc.CallOption) (*sapb.PauseIdentifiersResponse, error) { res, err := msa.StorageAuthorityClient.PauseIdentifiers(ctx, req) msa.out <- req return res, err } func TestPerformValidation_FailedValidationsTriggerPauseIdentifiersRatelimit(t *testing.T) { va, sa, ra, rl, fc, cleanUp := initAuthorities(t) defer cleanUp() features.Set(features.Config{AutomaticallyPauseZombieClients: true}) defer features.Reset() // Replace the SA with one that will block when PauseIdentifiers is called. pauseChan := make(chan *sapb.PauseRequest) defer close(pauseChan) ra.SA = mockSAWithSyncPause{ StorageAuthorityClient: ra.SA, out: pauseChan, } // Set the default ratelimits to only allow one failed validation per 24 // hours before pausing. txnBuilder, err := ratelimits.NewTransactionBuilder(ratelimits.LimitConfigs{ ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount.String(): &ratelimits.LimitConfig{ Burst: 1, Count: 1, Period: config.Duration{Duration: time.Hour * 24}}, }) test.AssertNotError(t, err, "making transaction composer") ra.txnBuilder = txnBuilder // Set up a fake domain, authz, and bucket key to care about. domain := randomDomain() ident := identifier.NewDNS(domain) authzPB := createPendingAuthorization(t, sa, ident, fc.Now().Add(12*time.Hour)) bucketKey := ratelimits.NewRegIdIdentValueBucketKey(ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount, authzPB.RegistrationID, ident) // Set the stored TAT to indicate that this bucket has exhausted its quota. err = rl.BatchSet(context.Background(), map[string]time.Time{ bucketKey: fc.Now().Add(25 * time.Hour), }) test.AssertNotError(t, err, "updating rate limit bucket") // Now a failed validation should result in the identifier being paused // due to the strict ratelimit. va.doDCVResult = &vapb.ValidationResult{ Records: []*corepb.ValidationRecord{ { AddressUsed: []byte("192.168.0.1"), Hostname: domain, Port: "8080", Url: fmt.Sprintf("http://%s/", domain), ResolverAddrs: []string{"rebound"}, }, }, Problem: nil, } va.doCAAResponse = &vapb.IsCAAValidResponse{ Problem: &corepb.ProblemDetails{ Detail: fmt.Sprintf("CAA invalid for %s", domain), }, } _, err = ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ Authz: authzPB, ChallengeIndex: dnsChallIdx(t, authzPB.Challenges), }) test.AssertNotError(t, err, "PerformValidation failed") // Wait for the RA to finish processing the validation, and ensure that the paused // account+identifier is what we expect. paused := <-pauseChan test.AssertEquals(t, len(paused.Identifiers), 1) test.AssertEquals(t, paused.Identifiers[0].Value, domain) } // mockRLSourceWithSyncDelete is a mock ratelimits.Source that forwards all // method calls to an inner Source, but also performs a blocking write to a // channel when Delete is called to allow the tests to synchronize. type mockRLSourceWithSyncDelete struct { ratelimits.Source out chan<- string } func (rl mockRLSourceWithSyncDelete) Delete(ctx context.Context, bucketKey string) error { err := rl.Source.Delete(ctx, bucketKey) rl.out <- bucketKey return err } func TestPerformValidation_FailedThenSuccessfulValidationResetsPauseIdentifiersRatelimit(t *testing.T) { va, sa, ra, rl, fc, cleanUp := initAuthorities(t) defer cleanUp() features.Set(features.Config{AutomaticallyPauseZombieClients: true}) defer features.Reset() // Replace the rate limit source with one that will block when Delete is called. keyChan := make(chan string) defer close(keyChan) limiter, err := ratelimits.NewLimiter(fc, mockRLSourceWithSyncDelete{ Source: rl, out: keyChan, }, metrics.NoopRegisterer) test.AssertNotError(t, err, "creating mock limiter") ra.limiter = limiter // Set up a fake domain, authz, and bucket key to care about. domain := randomDomain() ident := identifier.NewDNS(domain) authzPB := createPendingAuthorization(t, sa, ident, fc.Now().Add(12*time.Hour)) bucketKey := ratelimits.NewRegIdIdentValueBucketKey(ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount, authzPB.RegistrationID, ident) // Set a stored TAT so that we can tell when it's been reset. err = rl.BatchSet(context.Background(), map[string]time.Time{ bucketKey: fc.Now().Add(25 * time.Hour), }) test.AssertNotError(t, err, "updating rate limit bucket") va.doDCVResult = &vapb.ValidationResult{ Records: []*corepb.ValidationRecord{ { AddressUsed: []byte("192.168.0.1"), Hostname: domain, Port: "8080", Url: fmt.Sprintf("http://%s/", domain), ResolverAddrs: []string{"rebound"}, }, }, Problem: nil, } va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil} _, err = ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ Authz: authzPB, ChallengeIndex: dnsChallIdx(t, authzPB.Challenges), }) test.AssertNotError(t, err, "PerformValidation failed") // Wait for the RA to finish processesing the validation, and ensure that // the reset bucket key is what we expect. reset := <-keyChan test.AssertEquals(t, reset, bucketKey) // Verify that the bucket no longer exists (because the limiter reset has // deleted it). This indicates the accountID:identifier bucket has regained // capacity avoiding being inadvertently paused. _, err = rl.Get(ctx, bucketKey) test.AssertErrorIs(t, err, ratelimits.ErrBucketNotFound) } func TestPerformValidationVAError(t *testing.T) { va, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() authzPB := createPendingAuthorization(t, sa, identifier.NewDNS("example.com"), fc.Now().Add(12*time.Hour)) va.doDCVError = fmt.Errorf("Something went wrong") challIdx := dnsChallIdx(t, authzPB.Challenges) authzPB, err := ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ Authz: authzPB, ChallengeIndex: challIdx, }) test.AssertNotError(t, err, "PerformValidation completely failed") var vaRequest *vapb.PerformValidationRequest select { case r := <-va.doDCVRequest: vaRequest = r case <-time.After(time.Second): t.Fatal("Timed out waiting for DummyValidationAuthority.PerformValidation to complete") } // Verify that the VA got the request, and it's the same as the others test.AssertEquals(t, authzPB.Challenges[challIdx].Type, vaRequest.Challenge.Type) test.AssertEquals(t, authzPB.Challenges[challIdx].Token, vaRequest.Challenge.Token) // Sleep so the RA has a chance to write to the SA time.Sleep(100 * time.Millisecond) dbAuthzPB := getAuthorization(t, authzPB.Id, sa) t.Log("dbAuthz:", dbAuthzPB) // Verify that the responses are reflected challIdx = dnsChallIdx(t, dbAuthzPB.Challenges) challenge, err := bgrpc.PBToChallenge(dbAuthzPB.Challenges[challIdx]) test.AssertNotError(t, err, "Failed to marshall corepb.Challenge to core.Challenge.") test.Assert(t, challenge.Status == core.StatusInvalid, "challenge was not marked as invalid") test.AssertContains(t, challenge.Error.String(), "Could not communicate with VA") test.Assert(t, challenge.ValidationRecord == nil, "challenge had a ValidationRecord") // Check that validated timestamp was recorded, stored, and retrieved expectedValidated := fc.Now() test.Assert(t, *challenge.Validated == expectedValidated, "Validated timestamp incorrect or missing") } func TestCertificateKeyNotEqualAccountKey(t *testing.T) { _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() exp := ra.clk.Now().Add(365 * 24 * time.Hour) authzID := createFinalizedAuthorization(t, sa, identifier.NewDNS("www.example.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) order, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ NewOrder: &sapb.NewOrderRequest{ RegistrationID: Registration.Id, Expires: timestamppb.New(exp), Identifiers: []*corepb.Identifier{identifier.NewDNS("www.example.com").ToProto()}, V2Authorizations: []int64{authzID}, }, }) test.AssertNotError(t, err, "Could not add test order with finalized authz IDs, ready status") csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ // Registration has key == AccountKeyA PublicKey: AccountKeyA.Key, SignatureAlgorithm: x509.SHA256WithRSA, DNSNames: []string{"www.example.com"}, }, AccountPrivateKey.Key) test.AssertNotError(t, err, "Failed to sign CSR") _, err = ra.FinalizeOrder(ctx, &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Status: string(core.StatusReady), Identifiers: []*corepb.Identifier{identifier.NewDNS("www.example.com").ToProto()}, Id: order.Id, RegistrationID: Registration.Id, }, Csr: csrBytes, }) test.AssertError(t, err, "Should have rejected cert with key = account key") test.AssertEquals(t, err.Error(), "certificate public key must be different than account key") } func TestDeactivateAuthorization(t *testing.T) { _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() exp := ra.clk.Now().Add(365 * 24 * time.Hour) authzID := createFinalizedAuthorization(t, sa, identifier.NewDNS("not-example.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) dbAuthzPB := getAuthorization(t, fmt.Sprint(authzID), sa) _, err := ra.DeactivateAuthorization(ctx, dbAuthzPB) test.AssertNotError(t, err, "Could not deactivate authorization") deact, err := sa.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: authzID}) test.AssertNotError(t, err, "Could not get deactivated authorization with ID "+dbAuthzPB.Id) test.AssertEquals(t, deact.Status, string(core.StatusDeactivated)) } type mockSARecordingPauses struct { sapb.StorageAuthorityClient recv *sapb.PauseRequest } func (sa *mockSARecordingPauses) PauseIdentifiers(ctx context.Context, req *sapb.PauseRequest, _ ...grpc.CallOption) (*sapb.PauseIdentifiersResponse, error) { sa.recv = req return &sapb.PauseIdentifiersResponse{Paused: int64(len(req.Identifiers))}, nil } func (sa *mockSARecordingPauses) DeactivateAuthorization2(_ context.Context, _ *sapb.AuthorizationID2, _ ...grpc.CallOption) (*emptypb.Empty, error) { return nil, nil } func TestDeactivateAuthorization_Pausing(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() if ra.limiter == nil { t.Skip("no redis limiter configured") } msa := mockSARecordingPauses{} ra.SA = &msa features.Set(features.Config{AutomaticallyPauseZombieClients: true}) defer features.Reset() // Set the default ratelimits to only allow one failed validation per 24 // hours before pausing. txnBuilder, err := ratelimits.NewTransactionBuilder(ratelimits.LimitConfigs{ ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount.String(): &ratelimits.LimitConfig{ Burst: 1, Count: 1, Period: config.Duration{Duration: time.Hour * 24}}, }) test.AssertNotError(t, err, "making transaction composer") ra.txnBuilder = txnBuilder // The first deactivation of a pending authz should work and nothing should // get paused. _, err = ra.DeactivateAuthorization(ctx, &corepb.Authorization{ Id: "1", RegistrationID: 1, Identifier: identifier.NewDNS("example.com").ToProto(), Status: string(core.StatusPending), }) test.AssertNotError(t, err, "mock deactivation should work") test.AssertBoxedNil(t, msa.recv, "shouldn't be a pause request yet") // Deactivating a valid authz shouldn't increment any limits or pause anything. _, err = ra.DeactivateAuthorization(ctx, &corepb.Authorization{ Id: "2", RegistrationID: 1, Identifier: identifier.NewDNS("example.com").ToProto(), Status: string(core.StatusValid), }) test.AssertNotError(t, err, "mock deactivation should work") test.AssertBoxedNil(t, msa.recv, "deactivating valid authz should never pause") // Deactivating a second pending authz should surpass the limit and result // in a pause request. _, err = ra.DeactivateAuthorization(ctx, &corepb.Authorization{ Id: "3", RegistrationID: 1, Identifier: identifier.NewDNS("example.com").ToProto(), Status: string(core.StatusPending), }) test.AssertNotError(t, err, "mock deactivation should work") test.AssertNotNil(t, msa.recv, "should have recorded a pause request") test.AssertEquals(t, msa.recv.RegistrationID, int64(1)) test.AssertEquals(t, msa.recv.Identifiers[0].Value, "example.com") } func TestDeactivateRegistration(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() // Deactivate failure because incomplete registration provided _, err := ra.DeactivateRegistration(context.Background(), &rapb.DeactivateRegistrationRequest{}) test.AssertDeepEquals(t, err, fmt.Errorf("incomplete gRPC request message")) // Deactivate success with valid registration got, err := ra.DeactivateRegistration(context.Background(), &rapb.DeactivateRegistrationRequest{RegistrationID: 1}) test.AssertNotError(t, err, "DeactivateRegistration failed") test.AssertEquals(t, got.Status, string(core.StatusDeactivated)) // Check db to make sure account is deactivated dbReg, err := ra.SA.GetRegistration(context.Background(), &sapb.RegistrationID{Id: 1}) test.AssertNotError(t, err, "GetRegistration failed") test.AssertEquals(t, dbReg.Status, string(core.StatusDeactivated)) } // noopCAA implements vapb.CAAClient, always returning nil type noopCAA struct{} func (cr noopCAA) IsCAAValid( ctx context.Context, in *vapb.IsCAAValidRequest, opts ...grpc.CallOption, ) (*vapb.IsCAAValidResponse, error) { return &vapb.IsCAAValidResponse{}, nil } func (cr noopCAA) DoCAA( ctx context.Context, in *vapb.IsCAAValidRequest, opts ...grpc.CallOption, ) (*vapb.IsCAAValidResponse, error) { return &vapb.IsCAAValidResponse{}, nil } // caaRecorder implements vapb.CAAClient, always returning nil, but recording // the names it was called for. type caaRecorder struct { sync.Mutex names map[string]bool } func (cr *caaRecorder) IsCAAValid( ctx context.Context, in *vapb.IsCAAValidRequest, opts ...grpc.CallOption, ) (*vapb.IsCAAValidResponse, error) { cr.Lock() defer cr.Unlock() cr.names[in.Identifier.Value] = true return &vapb.IsCAAValidResponse{}, nil } func (cr *caaRecorder) DoCAA( ctx context.Context, in *vapb.IsCAAValidRequest, opts ...grpc.CallOption, ) (*vapb.IsCAAValidResponse, error) { cr.Lock() defer cr.Unlock() cr.names[in.Identifier.Value] = true return &vapb.IsCAAValidResponse{}, nil } // Test that the right set of domain names have their CAA rechecked, based on // their `Validated` (attemptedAt in the database) timestamp. func TestRecheckCAADates(t *testing.T) { _, _, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() recorder := &caaRecorder{names: make(map[string]bool)} ra.VA = va.RemoteClients{CAAClient: recorder} ra.profiles.def().validAuthzLifetime = 15 * time.Hour recentValidated := fc.Now().Add(-1 * time.Hour) recentExpires := fc.Now().Add(15 * time.Hour) olderValidated := fc.Now().Add(-8 * time.Hour) olderExpires := fc.Now().Add(5 * time.Hour) authzs := map[identifier.ACMEIdentifier]*core.Authorization{ identifier.NewDNS("recent.com"): { Identifier: identifier.NewDNS("recent.com"), Expires: &recentExpires, Challenges: []core.Challenge{ { Status: core.StatusValid, Type: core.ChallengeTypeHTTP01, Token: "exampleToken", Validated: &recentValidated, }, }, }, identifier.NewDNS("older.com"): { Identifier: identifier.NewDNS("older.com"), Expires: &olderExpires, Challenges: []core.Challenge{ { Status: core.StatusValid, Type: core.ChallengeTypeHTTP01, Token: "exampleToken", Validated: &olderValidated, }, }, }, identifier.NewDNS("older2.com"): { Identifier: identifier.NewDNS("older2.com"), Expires: &olderExpires, Challenges: []core.Challenge{ { Status: core.StatusValid, Type: core.ChallengeTypeHTTP01, Token: "exampleToken", Validated: &olderValidated, }, }, }, identifier.NewDNS("wildcard.com"): { Identifier: identifier.NewDNS("wildcard.com"), Expires: &olderExpires, Challenges: []core.Challenge{ { Status: core.StatusValid, Type: core.ChallengeTypeHTTP01, Token: "exampleToken", Validated: &olderValidated, }, }, }, identifier.NewDNS("*.wildcard.com"): { Identifier: identifier.NewDNS("*.wildcard.com"), Expires: &olderExpires, Challenges: []core.Challenge{ { Status: core.StatusValid, Type: core.ChallengeTypeHTTP01, Token: "exampleToken", Validated: &olderValidated, }, }, }, } twoChallenges := map[identifier.ACMEIdentifier]*core.Authorization{ identifier.NewDNS("twochallenges.com"): { ID: "twochal", Identifier: identifier.NewDNS("twochallenges.com"), Expires: &recentExpires, Challenges: []core.Challenge{ { Status: core.StatusValid, Type: core.ChallengeTypeHTTP01, Token: "exampleToken", Validated: &olderValidated, }, { Status: core.StatusValid, Type: core.ChallengeTypeDNS01, Token: "exampleToken", Validated: &olderValidated, }, }, }, } noChallenges := map[identifier.ACMEIdentifier]*core.Authorization{ identifier.NewDNS("nochallenges.com"): { ID: "nochal", Identifier: identifier.NewDNS("nochallenges.com"), Expires: &recentExpires, Challenges: []core.Challenge{}, }, } noValidationTime := map[identifier.ACMEIdentifier]*core.Authorization{ identifier.NewDNS("novalidationtime.com"): { ID: "noval", Identifier: identifier.NewDNS("novalidationtime.com"), Expires: &recentExpires, Challenges: []core.Challenge{ { Status: core.StatusValid, Type: core.ChallengeTypeHTTP01, Token: "exampleToken", Validated: nil, }, }, }, } // NOTE: The names provided here correspond to authorizations in the // `mockSAWithRecentAndOlder` err := ra.checkAuthorizationsCAA(context.Background(), Registration.Id, authzs, fc.Now()) // We expect that there is no error rechecking authorizations for these names if err != nil { t.Errorf("expected nil err, got %s", err) } // Should error if a authorization has `!= 1` challenge err = ra.checkAuthorizationsCAA(context.Background(), Registration.Id, twoChallenges, fc.Now()) test.AssertEquals(t, err.Error(), "authorization has incorrect number of challenges. 1 expected, 2 found for: id twochal") // Should error if a authorization has `!= 1` challenge err = ra.checkAuthorizationsCAA(context.Background(), Registration.Id, noChallenges, fc.Now()) test.AssertEquals(t, err.Error(), "authorization has incorrect number of challenges. 1 expected, 0 found for: id nochal") // Should error if authorization's challenge has no validated timestamp err = ra.checkAuthorizationsCAA(context.Background(), Registration.Id, noValidationTime, fc.Now()) test.AssertEquals(t, err.Error(), "authorization's challenge has no validated timestamp for: id noval") // We expect that "recent.com" is not checked because its mock authorization // isn't expired if _, present := recorder.names["recent.com"]; present { t.Errorf("Rechecked CAA unnecessarily for recent.com") } // We expect that "older.com" is checked if _, present := recorder.names["older.com"]; !present { t.Errorf("Failed to recheck CAA for older.com") } // We expect that "older2.com" is checked if _, present := recorder.names["older2.com"]; !present { t.Errorf("Failed to recheck CAA for older2.com") } // We expect that the "wildcard.com" domain (without the `*.` prefix) is checked. if _, present := recorder.names["wildcard.com"]; !present { t.Errorf("Failed to recheck CAA for wildcard.com") } // We expect that "*.wildcard.com" is checked (with the `*.` prefix, because // it is stripped at a lower layer than we are testing) if _, present := recorder.names["*.wildcard.com"]; !present { t.Errorf("Failed to recheck CAA for *.wildcard.com") } } type caaFailer struct{} func (cf *caaFailer) IsCAAValid( ctx context.Context, in *vapb.IsCAAValidRequest, opts ...grpc.CallOption, ) (*vapb.IsCAAValidResponse, error) { cvrpb := &vapb.IsCAAValidResponse{} switch in.Identifier.Value { case "a.com": cvrpb.Problem = &corepb.ProblemDetails{ Detail: "CAA invalid for a.com", } case "b.com": case "c.com": cvrpb.Problem = &corepb.ProblemDetails{ Detail: "CAA invalid for c.com", } case "d.com": return nil, fmt.Errorf("Error checking CAA for d.com") default: return nil, fmt.Errorf("Unexpected test case") } return cvrpb, nil } func (cf *caaFailer) DoCAA( ctx context.Context, in *vapb.IsCAAValidRequest, opts ...grpc.CallOption, ) (*vapb.IsCAAValidResponse, error) { cvrpb := &vapb.IsCAAValidResponse{} switch in.Identifier.Value { case "a.com": cvrpb.Problem = &corepb.ProblemDetails{ Detail: "CAA invalid for a.com", } case "b.com": case "c.com": cvrpb.Problem = &corepb.ProblemDetails{ Detail: "CAA invalid for c.com", } case "d.com": return nil, fmt.Errorf("Error checking CAA for d.com") default: return nil, fmt.Errorf("Unexpected test case") } return cvrpb, nil } func TestRecheckCAAEmpty(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() err := ra.recheckCAA(context.Background(), nil) test.AssertNotError(t, err, "expected nil") } func makeHTTP01Authorization(ident identifier.ACMEIdentifier) *core.Authorization { return &core.Authorization{ Identifier: ident, Challenges: []core.Challenge{{Status: core.StatusValid, Type: core.ChallengeTypeHTTP01}}, } } func TestRecheckCAASuccess(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ra.VA = va.RemoteClients{CAAClient: &noopCAA{}} authzs := []*core.Authorization{ makeHTTP01Authorization(identifier.NewDNS("a.com")), makeHTTP01Authorization(identifier.NewDNS("b.com")), makeHTTP01Authorization(identifier.NewDNS("c.com")), } err := ra.recheckCAA(context.Background(), authzs) test.AssertNotError(t, err, "expected nil") } func TestRecheckCAAFail(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ra.VA = va.RemoteClients{CAAClient: &caaFailer{}} authzs := []*core.Authorization{ makeHTTP01Authorization(identifier.NewDNS("a.com")), makeHTTP01Authorization(identifier.NewDNS("b.com")), makeHTTP01Authorization(identifier.NewDNS("c.com")), } err := ra.recheckCAA(context.Background(), authzs) test.AssertError(t, err, "expected err, got nil") var berr *berrors.BoulderError test.AssertErrorWraps(t, err, &berr) test.AssertErrorIs(t, berr, berrors.CAA) test.AssertEquals(t, len(berr.SubErrors), 2) // We don't know whether the asynchronous a.com or c.com CAA recheck will fail // first. Whichever does will be mentioned in the top level problem detail. expectedDetailRegex := regexp.MustCompile( `Rechecking CAA for "(?:a\.com|c\.com)" and 1 more identifiers failed. Refer to sub-problems for more information`, ) if !expectedDetailRegex.MatchString(berr.Detail) { t.Errorf("expected suberror detail to match expected regex, got %q", err) } // There should be a sub error for both a.com and c.com with the correct type subErrMap := make(map[string]berrors.SubBoulderError, len(berr.SubErrors)) for _, subErr := range berr.SubErrors { subErrMap[subErr.Identifier.Value] = subErr } subErrA, foundA := subErrMap["a.com"] subErrB, foundB := subErrMap["c.com"] test.AssertEquals(t, foundA, true) test.AssertEquals(t, foundB, true) test.AssertEquals(t, subErrA.Type, berrors.CAA) test.AssertEquals(t, subErrB.Type, berrors.CAA) // Recheck CAA with just one bad authz authzs = []*core.Authorization{ makeHTTP01Authorization(identifier.NewDNS("a.com")), } err = ra.recheckCAA(context.Background(), authzs) // It should error test.AssertError(t, err, "expected err from recheckCAA") // It should be a berror test.AssertErrorWraps(t, err, &berr) // There should be *no* suberrors because there was only one overall error test.AssertEquals(t, len(berr.SubErrors), 0) } func TestRecheckCAAInternalServerError(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ra.VA = va.RemoteClients{CAAClient: &caaFailer{}} authzs := []*core.Authorization{ makeHTTP01Authorization(identifier.NewDNS("a.com")), makeHTTP01Authorization(identifier.NewDNS("b.com")), makeHTTP01Authorization(identifier.NewDNS("d.com")), } err := ra.recheckCAA(context.Background(), authzs) test.AssertError(t, err, "expected err, got nil") test.AssertErrorIs(t, err, berrors.InternalServer) } func TestRecheckSkipIPAddress(t *testing.T) { _, _, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() ra.VA = va.RemoteClients{CAAClient: &caaFailer{}} ident := identifier.NewIP(netip.MustParseAddr("127.0.0.1")) olderValidated := fc.Now().Add(-8 * time.Hour) olderExpires := fc.Now().Add(5 * time.Hour) authzs := map[identifier.ACMEIdentifier]*core.Authorization{ ident: { Identifier: ident, Expires: &olderExpires, Challenges: []core.Challenge{ { Status: core.StatusValid, Type: core.ChallengeTypeHTTP01, Token: "exampleToken", Validated: &olderValidated, }, }, }, } err := ra.checkAuthorizationsCAA(context.Background(), 1, authzs, fc.Now()) test.AssertNotError(t, err, "rechecking CAA for IP address, should have skipped") } func TestRecheckInvalidIdentifierType(t *testing.T) { _, _, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() ident := identifier.ACMEIdentifier{ Type: "fnord", Value: "well this certainly shouldn't have happened", } olderValidated := fc.Now().Add(-8 * time.Hour) olderExpires := fc.Now().Add(5 * time.Hour) authzs := map[identifier.ACMEIdentifier]*core.Authorization{ ident: { Identifier: ident, Expires: &olderExpires, Challenges: []core.Challenge{ { Status: core.StatusValid, Type: core.ChallengeTypeHTTP01, Token: "exampleToken", Validated: &olderValidated, }, }, }, } err := ra.checkAuthorizationsCAA(context.Background(), 1, authzs, fc.Now()) test.AssertError(t, err, "expected err, got nil") test.AssertErrorIs(t, err, berrors.Malformed) test.AssertContains(t, err.Error(), "invalid identifier type") } func TestNewOrder(t *testing.T) { _, _, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() now := fc.Now() orderA, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, CertificateProfileName: "test", Identifiers: []*corepb.Identifier{ identifier.NewDNS("b.com").ToProto(), identifier.NewDNS("a.com").ToProto(), identifier.NewDNS("a.com").ToProto(), identifier.NewDNS("C.COM").ToProto(), }, }) test.AssertNotError(t, err, "ra.NewOrder failed") test.AssertEquals(t, orderA.RegistrationID, int64(1)) test.AssertEquals(t, orderA.Expires.AsTime(), now.Add(ra.profiles.def().orderLifetime)) test.AssertEquals(t, len(orderA.Identifiers), 3) test.AssertEquals(t, orderA.CertificateProfileName, "test") // We expect the order's identifier values to have been sorted, // deduplicated, and lowercased. test.AssertDeepEquals(t, orderA.Identifiers, []*corepb.Identifier{ identifier.NewDNS("a.com").ToProto(), identifier.NewDNS("b.com").ToProto(), identifier.NewDNS("c.com").ToProto(), }) test.AssertEquals(t, orderA.Id, int64(1)) test.AssertEquals(t, numAuthorizations(orderA), 3) _, err = ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{identifier.NewDNS("a").ToProto()}, }) test.AssertError(t, err, "NewOrder with invalid names did not error") test.AssertEquals(t, err.Error(), "Cannot issue for \"a\": Domain name needs at least one dot") } // TestNewOrder_OrderReuse tests that subsequent requests by an ACME account to create // an identical order results in only one order being created & subsequently // reused. func TestNewOrder_OrderReuse(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() // Create an initial order with regA and names idents := identifier.ACMEIdentifiers{ identifier.NewDNS("zombo.com"), identifier.NewDNS("welcome.to.zombo.com"), } orderReq := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: idents.ToProtoSlice(), CertificateProfileName: "test", } firstOrder, err := ra.NewOrder(context.Background(), orderReq) test.AssertNotError(t, err, "Adding an initial order for regA failed") // Create a second registration to reference acctKeyB, err := AccountKeyB.MarshalJSON() test.AssertNotError(t, err, "failed to marshal account key") input := &corepb.Registration{Key: acctKeyB} secondReg, err := ra.NewRegistration(context.Background(), input) test.AssertNotError(t, err, "Error creating a second test registration") // Insert a second (albeit identical) profile to reference ra.profiles.byName["different"] = ra.profiles.def() testCases := []struct { Name string RegistrationID int64 Identifiers identifier.ACMEIdentifiers Profile string ExpectReuse bool }{ { Name: "Duplicate order, same regID", RegistrationID: Registration.Id, Identifiers: idents, Profile: "test", // We expect reuse since the order matches firstOrder ExpectReuse: true, }, { Name: "Subset of order names, same regID", RegistrationID: Registration.Id, Identifiers: idents[:1], Profile: "test", // We do not expect reuse because the order names don't match firstOrder ExpectReuse: false, }, { Name: "Superset of order names, same regID", RegistrationID: Registration.Id, Identifiers: append(idents, identifier.NewDNS("blog.zombo.com")), Profile: "test", // We do not expect reuse because the order names don't match firstOrder ExpectReuse: false, }, { Name: "Missing profile, same regID", RegistrationID: Registration.Id, Identifiers: append(idents, identifier.NewDNS("blog.zombo.com")), // We do not expect reuse because the profile is missing ExpectReuse: false, }, { Name: "Missing profile, same regID", RegistrationID: Registration.Id, Identifiers: append(idents, identifier.NewDNS("blog.zombo.com")), Profile: "different", // We do not expect reuse because a different profile is specified ExpectReuse: false, }, { Name: "Duplicate order, different regID", RegistrationID: secondReg.Id, Identifiers: idents, Profile: "test", // We do not expect reuse because the order regID differs from firstOrder ExpectReuse: false, }, // TODO(#7324): Integrate certificate profile variance into this test. } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { // Add the order for the test request order, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: tc.RegistrationID, Identifiers: tc.Identifiers.ToProtoSlice(), CertificateProfileName: tc.Profile, }) test.AssertNotError(t, err, "NewOrder returned an unexpected error") test.AssertNotNil(t, order.Id, "NewOrder returned an order with a nil Id") if tc.ExpectReuse { // If we expected order reuse for this testcase assert that the order // has the same ID as the firstOrder test.AssertEquals(t, order.Id, firstOrder.Id) } else { // Otherwise assert that the order doesn't have the same ID as the // firstOrder test.AssertNotEquals(t, order.Id, firstOrder.Id) } }) } } // TestNewOrder_OrderReuse_Expired tests that expired orders are not reused. // This is not simply a test case in TestNewOrder_OrderReuse because it has // side effects. func TestNewOrder_OrderReuse_Expired(t *testing.T) { _, _, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() // Set the order lifetime to something short and known. ra.profiles.def().orderLifetime = time.Hour // Create an initial order. extant, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{ identifier.NewDNS("a.com").ToProto(), identifier.NewDNS("b.com").ToProto(), }, }) test.AssertNotError(t, err, "creating test order") // Transition the original order to status invalid by jumping forward in time // to when it has expired. fc.Set(extant.Expires.AsTime().Add(2 * time.Hour)) // Now a new order for the same names should not reuse the first one. new, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{ identifier.NewDNS("a.com").ToProto(), identifier.NewDNS("b.com").ToProto(), }, }) test.AssertNotError(t, err, "creating test order") test.AssertNotEquals(t, new.Id, extant.Id) } // TestNewOrder_OrderReuse_Invalid tests that invalid orders are not reused. // This is not simply a test case in TestNewOrder_OrderReuse because it has // side effects. func TestNewOrder_OrderReuse_Invalid(t *testing.T) { _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() // Create an initial order. extant, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{ identifier.NewDNS("a.com").ToProto(), identifier.NewDNS("b.com").ToProto(), }, }) test.AssertNotError(t, err, "creating test order") // Transition the original order to status invalid by invalidating one of its // authorizations. _, err = sa.DeactivateAuthorization2(context.Background(), &sapb.AuthorizationID2{ Id: extant.V2Authorizations[0], }) test.AssertNotError(t, err, "deactivating test authorization") // Now a new order for the same names should not reuse the first one. new, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{ identifier.NewDNS("a.com").ToProto(), identifier.NewDNS("b.com").ToProto(), }, }) test.AssertNotError(t, err, "creating test order") test.AssertNotEquals(t, new.Id, extant.Id) } func TestNewOrder_AuthzReuse(t *testing.T) { _, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() // Create three initial authzs by creating an initial order, then updating // the individual authz statuses. const ( pending = "a-pending.com" valid = "b-valid.com" invalid = "c-invalid.com" ) extant, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{ identifier.NewDNS(pending).ToProto(), identifier.NewDNS(valid).ToProto(), identifier.NewDNS(invalid).ToProto(), }, }) test.AssertNotError(t, err, "creating test order") extantAuthzs := map[string]int64{ // Take advantage of the fact that authz IDs are returned in the same order // as the lexicographically-sorted identifiers. pending: extant.V2Authorizations[0], valid: extant.V2Authorizations[1], invalid: extant.V2Authorizations[2], } _, err = sa.FinalizeAuthorization2(context.Background(), &sapb.FinalizeAuthorizationRequest{ Id: extantAuthzs[valid], Status: string(core.StatusValid), Attempted: "hello", Expires: timestamppb.New(fc.Now().Add(48 * time.Hour)), }) test.AssertNotError(t, err, "marking test authz as valid") _, err = sa.DeactivateAuthorization2(context.Background(), &sapb.AuthorizationID2{ Id: extantAuthzs[invalid], }) test.AssertNotError(t, err, "marking test authz as invalid") // Create a second registration to reference later. acctKeyB, err := AccountKeyB.MarshalJSON() test.AssertNotError(t, err, "failed to marshal account key") input := &corepb.Registration{Key: acctKeyB} secondReg, err := ra.NewRegistration(context.Background(), input) test.AssertNotError(t, err, "Error creating a second test registration") testCases := []struct { Name string RegistrationID int64 Identifier identifier.ACMEIdentifier Profile string ExpectReuse bool }{ { Name: "Reuse pending authz", RegistrationID: Registration.Id, Identifier: identifier.NewDNS(pending), ExpectReuse: true, // TODO(#7715): Invert this. }, { Name: "Reuse valid authz", RegistrationID: Registration.Id, Identifier: identifier.NewDNS(valid), ExpectReuse: true, }, { Name: "Don't reuse invalid authz", RegistrationID: Registration.Id, Identifier: identifier.NewDNS(invalid), ExpectReuse: false, }, { Name: "Don't reuse valid authz with wrong profile", RegistrationID: Registration.Id, Identifier: identifier.NewDNS(valid), Profile: "test", ExpectReuse: false, }, { Name: "Don't reuse valid authz from other acct", RegistrationID: secondReg.Id, Identifier: identifier.NewDNS(valid), ExpectReuse: false, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { new, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: tc.RegistrationID, Identifiers: []*corepb.Identifier{tc.Identifier.ToProto()}, CertificateProfileName: tc.Profile, }) test.AssertNotError(t, err, "creating test order") test.AssertNotEquals(t, new.Id, extant.Id) if tc.ExpectReuse { test.AssertEquals(t, new.V2Authorizations[0], extantAuthzs[tc.Identifier.Value]) } else { test.AssertNotEquals(t, new.V2Authorizations[0], extantAuthzs[tc.Identifier.Value]) } }) } } // TestNewOrder_AuthzReuse_NoPending tests that authz reuse doesn't reuse // pending authzs when a feature flag is set. // This is not simply a test case in TestNewOrder_OrderReuse because it relies // on feature-flag gated behavior. It should be unified with that function when // the feature flag is removed. func TestNewOrder_AuthzReuse_NoPending(t *testing.T) { // TODO(#7715): Integrate these cases into TestNewOrder_AuthzReuse. _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() features.Set(features.Config{NoPendingAuthzReuse: true}) defer features.Reset() // Create an initial order and two pending authzs. extant, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{ identifier.NewDNS("a.com").ToProto(), identifier.NewDNS("b.com").ToProto(), }, }) test.AssertNotError(t, err, "creating test order") // With the feature flag enabled, creating a new order for one of these names // should not reuse the existing pending authz. new, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{identifier.NewDNS("a.com").ToProto()}, }) test.AssertNotError(t, err, "creating test order") test.AssertNotEquals(t, new.Id, extant.Id) test.AssertNotEquals(t, new.V2Authorizations[0], extant.V2Authorizations[0]) } func TestNewOrder_ValidationProfiles(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ra.profiles = &validationProfiles{ defaultName: "one", byName: map[string]*validationProfile{ "one": { pendingAuthzLifetime: 1 * 24 * time.Hour, validAuthzLifetime: 1 * 24 * time.Hour, orderLifetime: 1 * 24 * time.Hour, maxNames: 10, identifierTypes: []identifier.IdentifierType{identifier.TypeDNS}, }, "two": { pendingAuthzLifetime: 2 * 24 * time.Hour, validAuthzLifetime: 2 * 24 * time.Hour, orderLifetime: 2 * 24 * time.Hour, maxNames: 10, identifierTypes: []identifier.IdentifierType{identifier.TypeDNS}, }, }, } for _, tc := range []struct { name string profile string wantExpires time.Time }{ { // A request with no profile should get an order and authzs with one-day lifetimes. name: "no profile specified", profile: "", wantExpires: ra.clk.Now().Add(1 * 24 * time.Hour), }, { // A request for profile one should get an order and authzs with one-day lifetimes. name: "profile one", profile: "one", wantExpires: ra.clk.Now().Add(1 * 24 * time.Hour), }, { // A request for profile two should get an order and authzs with one-day lifetimes. name: "profile two", profile: "two", wantExpires: ra.clk.Now().Add(2 * 24 * time.Hour), }, } { t.Run(tc.name, func(t *testing.T) { order, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{identifier.NewDNS(randomDomain()).ToProto()}, CertificateProfileName: tc.profile, }) if err != nil { t.Fatalf("creating order: %s", err) } gotExpires := order.Expires.AsTime() if gotExpires != tc.wantExpires { t.Errorf("NewOrder(profile: %q).Expires = %s, expected %s", tc.profile, gotExpires, tc.wantExpires) } authz, err := ra.GetAuthorization(context.Background(), &rapb.GetAuthorizationRequest{ Id: order.V2Authorizations[0], }) if err != nil { t.Fatalf("fetching test authz: %s", err) } gotExpires = authz.Expires.AsTime() if gotExpires != tc.wantExpires { t.Errorf("GetAuthorization(profile: %q).Expires = %s, expected %s", tc.profile, gotExpires, tc.wantExpires) } }) } } func TestNewOrder_ProfileSelectionAllowList(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() testCases := []struct { name string profile validationProfile expectErr bool expectErrContains string }{ { name: "Allow all account IDs", profile: validationProfile{allowList: nil}, expectErr: false, }, { name: "Deny all but account Id 1337", profile: validationProfile{allowList: allowlist.NewList([]int64{1337})}, expectErr: true, expectErrContains: "not permitted to use certificate profile", }, { name: "Deny all", profile: validationProfile{allowList: allowlist.NewList([]int64{})}, expectErr: true, expectErrContains: "not permitted to use certificate profile", }, { name: "Allow Registration.Id", profile: validationProfile{allowList: allowlist.NewList([]int64{Registration.Id})}, expectErr: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tc.profile.maxNames = 1 tc.profile.identifierTypes = []identifier.IdentifierType{identifier.TypeDNS} ra.profiles.byName = map[string]*validationProfile{ "test": &tc.profile, } orderReq := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{identifier.NewDNS(randomDomain()).ToProto()}, CertificateProfileName: "test", } _, err := ra.NewOrder(context.Background(), orderReq) if tc.expectErrContains != "" { test.AssertErrorIs(t, err, berrors.Unauthorized) test.AssertContains(t, err.Error(), tc.expectErrContains) } else { test.AssertNotError(t, err, "NewOrder failed") } }) } } func TestNewOrder_ProfileIdentifierTypes(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() testCases := []struct { name string identTypes []identifier.IdentifierType idents []*corepb.Identifier expectErr string }{ { name: "Permit DNS, provide DNS names", identTypes: []identifier.IdentifierType{identifier.TypeDNS}, idents: []*corepb.Identifier{identifier.NewDNS(randomDomain()).ToProto(), identifier.NewDNS(randomDomain()).ToProto()}, }, { name: "Permit IP, provide IPs", identTypes: []identifier.IdentifierType{identifier.TypeIP}, idents: []*corepb.Identifier{identifier.NewIP(randomIPv6()).ToProto(), identifier.NewIP(randomIPv6()).ToProto()}, }, { name: "Permit DNS & IP, provide DNS & IP", identTypes: []identifier.IdentifierType{identifier.TypeDNS, identifier.TypeIP}, idents: []*corepb.Identifier{identifier.NewIP(randomIPv6()).ToProto(), identifier.NewDNS(randomDomain()).ToProto()}, }, { name: "Permit DNS, provide IP", identTypes: []identifier.IdentifierType{identifier.TypeDNS}, idents: []*corepb.Identifier{identifier.NewIP(randomIPv6()).ToProto()}, expectErr: "Profile \"test\" does not permit ip type identifiers", }, { name: "Permit DNS, provide DNS & IP", identTypes: []identifier.IdentifierType{identifier.TypeDNS}, idents: []*corepb.Identifier{identifier.NewDNS(randomDomain()).ToProto(), identifier.NewIP(randomIPv6()).ToProto()}, expectErr: "Profile \"test\" does not permit ip type identifiers", }, { name: "Permit IP, provide DNS", identTypes: []identifier.IdentifierType{identifier.TypeIP}, idents: []*corepb.Identifier{identifier.NewDNS(randomDomain()).ToProto()}, expectErr: "Profile \"test\" does not permit dns type identifiers", }, { name: "Permit IP, provide DNS & IP", identTypes: []identifier.IdentifierType{identifier.TypeIP}, idents: []*corepb.Identifier{identifier.NewIP(randomIPv6()).ToProto(), identifier.NewDNS(randomDomain()).ToProto()}, expectErr: "Profile \"test\" does not permit dns type identifiers", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var profile validationProfile profile.maxNames = 2 profile.identifierTypes = tc.identTypes ra.profiles.byName = map[string]*validationProfile{ "test": &profile, } orderReq := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: tc.idents, CertificateProfileName: "test", } _, err := ra.NewOrder(context.Background(), orderReq) if tc.expectErr != "" { test.AssertErrorIs(t, err, berrors.RejectedIdentifier) test.AssertContains(t, err.Error(), tc.expectErr) } else { test.AssertNotError(t, err, "NewOrder failed") } }) } } // mockSAWithAuthzs has a GetAuthorizations2 method that returns the protobuf // version of its authzs struct member. It also has a fake GetOrderForNames // which always fails, and a fake NewOrderAndAuthzs which always succeeds, to // facilitate the full execution of RA.NewOrder. type mockSAWithAuthzs struct { sapb.StorageAuthorityClient authzs []*core.Authorization } // GetOrderForNames is a mock which always returns NotFound so that NewOrder // proceeds to attempt authz reuse instead of wholesale order reuse. func (msa *mockSAWithAuthzs) GetOrderForNames(ctx context.Context, req *sapb.GetOrderForNamesRequest, _ ...grpc.CallOption) (*corepb.Order, error) { return nil, berrors.NotFoundError("no such order") } // GetValidAuthorizations2 returns a _bizarre_ authorization for "*.zombo.com" that // was validated by HTTP-01. This should never happen in real life since the // name is a wildcard. We use this mock to test that we reject this bizarre // situation correctly. func (msa *mockSAWithAuthzs) GetValidAuthorizations2(ctx context.Context, req *sapb.GetValidAuthorizationsRequest, _ ...grpc.CallOption) (*sapb.Authorizations, error) { resp := &sapb.Authorizations{} for _, v := range msa.authzs { authzPB, err := bgrpc.AuthzToPB(*v) if err != nil { return nil, err } resp.Authzs = append(resp.Authzs, authzPB) } return resp, nil } func (msa *mockSAWithAuthzs) GetAuthorizations2(ctx context.Context, req *sapb.GetAuthorizationsRequest, _ ...grpc.CallOption) (*sapb.Authorizations, error) { return msa.GetValidAuthorizations2(ctx, &sapb.GetValidAuthorizationsRequest{ RegistrationID: req.RegistrationID, Identifiers: req.Identifiers, ValidUntil: req.ValidUntil, }) } func (msa *mockSAWithAuthzs) GetAuthorization2(ctx context.Context, req *sapb.AuthorizationID2, _ ...grpc.CallOption) (*corepb.Authorization, error) { for _, authz := range msa.authzs { if authz.ID == fmt.Sprintf("%d", req.Id) { return bgrpc.AuthzToPB(*authz) } } return nil, berrors.NotFoundError("no such authz") } // NewOrderAndAuthzs is a mock which just reflects the incoming request back, // pretending to have created new db rows for the requested newAuthzs. func (msa *mockSAWithAuthzs) NewOrderAndAuthzs(ctx context.Context, req *sapb.NewOrderAndAuthzsRequest, _ ...grpc.CallOption) (*corepb.Order, error) { authzIDs := req.NewOrder.V2Authorizations for range req.NewAuthzs { authzIDs = append(authzIDs, mrand.Int64()) } return &corepb.Order{ // Fields from the input new order request. RegistrationID: req.NewOrder.RegistrationID, Expires: req.NewOrder.Expires, Identifiers: req.NewOrder.Identifiers, V2Authorizations: authzIDs, CertificateProfileName: req.NewOrder.CertificateProfileName, // Mock new fields generated by the database transaction. Id: mrand.Int64(), Created: timestamppb.Now(), // A new order is never processing because it can't have been finalized yet. BeganProcessing: false, Status: string(core.StatusPending), }, nil } // TestNewOrderAuthzReuseSafety checks that the RA's safety check for reusing an // authorization for a new-order request with a wildcard name works correctly. // We want to ensure that we never reuse a non-Wildcard authorization (e.g. one // with more than just a DNS-01 challenge) for a wildcard name. See Issue #3420 // for background - this safety check was previously broken! // https://github.com/letsencrypt/boulder/issues/3420 func TestNewOrderAuthzReuseSafety(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ctx := context.Background() idents := identifier.ACMEIdentifiers{identifier.NewDNS("*.zombo.com")} // Use a mock SA that always returns a valid HTTP-01 authz for the name // "zombo.com" expires := time.Now() ra.SA = &mockSAWithAuthzs{ authzs: []*core.Authorization{ { // A static fake ID we can check for in a unit test ID: "1", Identifier: identifier.NewDNS("*.zombo.com"), RegistrationID: Registration.Id, // Authz is valid Status: "valid", Expires: &expires, Challenges: []core.Challenge{ // HTTP-01 challenge is valid { Type: core.ChallengeTypeHTTP01, // The dreaded HTTP-01! X__X Status: core.StatusValid, Token: core.NewToken(), }, // DNS-01 challenge is pending { Type: core.ChallengeTypeDNS01, Status: core.StatusPending, Token: core.NewToken(), }, }, }, { // A static fake ID we can check for in a unit test ID: "2", Identifier: identifier.NewDNS("zombo.com"), RegistrationID: Registration.Id, // Authz is valid Status: "valid", Expires: &expires, Challenges: []core.Challenge{ // HTTP-01 challenge is valid { Type: core.ChallengeTypeHTTP01, Status: core.StatusValid, Token: core.NewToken(), }, // DNS-01 challenge is pending { Type: core.ChallengeTypeDNS01, Status: core.StatusPending, Token: core.NewToken(), }, }, }, }, } // Create an initial request with regA and names orderReq := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: idents.ToProtoSlice(), } // Create an order for that request _, err := ra.NewOrder(ctx, orderReq) // It should fail test.AssertError(t, err, "Added an initial order for regA with invalid challenge(s)") test.AssertContains(t, err.Error(), "SA.GetAuthorizations returned a DNS wildcard authz (1) with invalid challenge(s)") } func TestNewOrderWildcard(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() orderIdents := identifier.ACMEIdentifiers{ identifier.NewDNS("example.com"), identifier.NewDNS("*.welcome.zombo.com"), } wildcardOrderRequest := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: orderIdents.ToProtoSlice(), } order, err := ra.NewOrder(context.Background(), wildcardOrderRequest) test.AssertNotError(t, err, "NewOrder failed for a wildcard order request") // We expect the order to be pending test.AssertEquals(t, order.Status, string(core.StatusPending)) // We expect the order to have two identifiers test.AssertEquals(t, len(order.Identifiers), 2) // We expect the order to have the identifiers we requested test.AssertDeepEquals(t, identifier.Normalize(identifier.FromProtoSlice(order.Identifiers)), identifier.Normalize(orderIdents)) test.AssertEquals(t, numAuthorizations(order), 2) // Check each of the authz IDs in the order for _, authzID := range order.V2Authorizations { // We should be able to retrieve the authz from the db without error authzID := authzID authzPB, err := ra.SA.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: authzID}) test.AssertNotError(t, err, "sa.GetAuthorization2 failed") authz, err := bgrpc.PBToAuthz(authzPB) test.AssertNotError(t, err, "bgrpc.PBToAuthz failed") // We expect the authz is in Pending status test.AssertEquals(t, authz.Status, core.StatusPending) name := authz.Identifier.Value switch name { case "*.welcome.zombo.com": // If the authz is for *.welcome.zombo.com, we expect that it only has one // pending challenge with DNS-01 type test.AssertEquals(t, len(authz.Challenges), 1) test.AssertEquals(t, authz.Challenges[0].Status, core.StatusPending) test.AssertEquals(t, authz.Challenges[0].Type, core.ChallengeTypeDNS01) case "example.com": // If the authz is for example.com, we expect it has normal challenges test.AssertEquals(t, len(authz.Challenges), 3) default: t.Fatalf("Received an authorization for a name not requested: %q", name) } } // An order for a base domain and a wildcard for the same base domain should // return just 2 authz's, one for the wildcard with a DNS-01 // challenge and one for the base domain with the normal challenges. orderIdents = identifier.ACMEIdentifiers{ identifier.NewDNS("zombo.com"), identifier.NewDNS("*.zombo.com"), } wildcardOrderRequest = &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: orderIdents.ToProtoSlice(), } order, err = ra.NewOrder(context.Background(), wildcardOrderRequest) test.AssertNotError(t, err, "NewOrder failed for a wildcard order request") // We expect the order to be pending test.AssertEquals(t, order.Status, string(core.StatusPending)) // We expect the order to have two identifiers test.AssertEquals(t, len(order.Identifiers), 2) // We expect the order to have the identifiers we requested test.AssertDeepEquals(t, identifier.Normalize(identifier.FromProtoSlice(order.Identifiers)), identifier.Normalize(orderIdents)) test.AssertEquals(t, numAuthorizations(order), 2) for _, authzID := range order.V2Authorizations { // We should be able to retrieve the authz from the db without error authzID := authzID authzPB, err := ra.SA.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: authzID}) test.AssertNotError(t, err, "sa.GetAuthorization2 failed") authz, err := bgrpc.PBToAuthz(authzPB) test.AssertNotError(t, err, "bgrpc.PBToAuthz failed") // We expect the authz is in Pending status test.AssertEquals(t, authz.Status, core.StatusPending) switch authz.Identifier.Value { case "zombo.com": // We expect that the base domain identifier auth has the normal number of // challenges test.AssertEquals(t, len(authz.Challenges), 3) case "*.zombo.com": // We expect that the wildcard identifier auth has only a pending // DNS-01 type challenge test.AssertEquals(t, len(authz.Challenges), 1) test.AssertEquals(t, authz.Challenges[0].Status, core.StatusPending) test.AssertEquals(t, authz.Challenges[0].Type, core.ChallengeTypeDNS01) default: t.Fatal("Unexpected authorization value returned from new-order") } } // Make an order for a single domain, no wildcards. This will create a new // pending authz for the domain normalOrderReq := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{identifier.NewDNS("everything.is.possible.zombo.com").ToProto()}, } normalOrder, err := ra.NewOrder(context.Background(), normalOrderReq) test.AssertNotError(t, err, "NewOrder failed for a normal non-wildcard order") test.AssertEquals(t, numAuthorizations(normalOrder), 1) // We expect the order is in Pending status test.AssertEquals(t, order.Status, string(core.StatusPending)) var authz core.Authorization authzPB, err := ra.SA.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: normalOrder.V2Authorizations[0]}) test.AssertNotError(t, err, "sa.GetAuthorization2 failed") authz, err = bgrpc.PBToAuthz(authzPB) test.AssertNotError(t, err, "bgrpc.PBToAuthz failed") // We expect the authz is in Pending status test.AssertEquals(t, authz.Status, core.StatusPending) // We expect the authz is for the identifier the correct domain test.AssertEquals(t, authz.Identifier.Value, "everything.is.possible.zombo.com") // We expect the authz has the normal # of challenges test.AssertEquals(t, len(authz.Challenges), 3) // Now submit an order request for a wildcard of the domain we just created an // order for. We should **NOT** reuse the authorization from the previous // order since we now require a DNS-01 challenge for the `*.` prefixed name. orderIdents = identifier.ACMEIdentifiers{identifier.NewDNS("*.everything.is.possible.zombo.com")} wildcardOrderRequest = &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: orderIdents.ToProtoSlice(), } order, err = ra.NewOrder(context.Background(), wildcardOrderRequest) test.AssertNotError(t, err, "NewOrder failed for a wildcard order request") // We expect the order is in Pending status test.AssertEquals(t, order.Status, string(core.StatusPending)) test.AssertEquals(t, numAuthorizations(order), 1) // The authz should be a different ID than the previous authz test.AssertNotEquals(t, order.V2Authorizations[0], normalOrder.V2Authorizations[0]) // We expect the authorization is available authzPB, err = ra.SA.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: order.V2Authorizations[0]}) test.AssertNotError(t, err, "sa.GetAuthorization2 failed") authz, err = bgrpc.PBToAuthz(authzPB) test.AssertNotError(t, err, "bgrpc.PBToAuthz failed") // We expect the authz is in Pending status test.AssertEquals(t, authz.Status, core.StatusPending) // We expect the authz is for a identifier with the correct domain test.AssertEquals(t, authz.Identifier.Value, "*.everything.is.possible.zombo.com") // We expect the authz has only one challenge test.AssertEquals(t, len(authz.Challenges), 1) // We expect the one challenge is pending test.AssertEquals(t, authz.Challenges[0].Status, core.StatusPending) // We expect that the one challenge is a DNS01 type challenge test.AssertEquals(t, authz.Challenges[0].Type, core.ChallengeTypeDNS01) // Submit an identical wildcard order request dupeOrder, err := ra.NewOrder(context.Background(), wildcardOrderRequest) test.AssertNotError(t, err, "NewOrder failed for a wildcard order request") // We expect the order is in Pending status test.AssertEquals(t, dupeOrder.Status, string(core.StatusPending)) test.AssertEquals(t, numAuthorizations(dupeOrder), 1) // The authz should be the same ID as the previous order's authz. We already // checked that order.Authorizations[0] only has a DNS-01 challenge above so // we don't need to recheck that here. test.AssertEquals(t, dupeOrder.V2Authorizations[0], order.V2Authorizations[0]) } func TestNewOrderExpiry(t *testing.T) { _, _, ra, _, clk, cleanUp := initAuthorities(t) defer cleanUp() ctx := context.Background() idents := identifier.ACMEIdentifiers{identifier.NewDNS("zombo.com")} // Set the order lifetime to 48 hours. ra.profiles.def().orderLifetime = 48 * time.Hour // Use an expiry that is sooner than the configured order expiry but greater // than 24 hours away. fakeAuthzExpires := clk.Now().Add(35 * time.Hour) // Use a mock SA that always returns a soon-to-be-expired valid authz for // "zombo.com". ra.SA = &mockSAWithAuthzs{ authzs: []*core.Authorization{ { // A static fake ID we can check for in a unit test ID: "1", Identifier: identifier.NewDNS("zombo.com"), RegistrationID: Registration.Id, Expires: &fakeAuthzExpires, Status: "valid", Challenges: []core.Challenge{ { Type: core.ChallengeTypeHTTP01, Status: core.StatusValid, Token: core.NewToken(), }, }, }, }, } // Create an initial request with regA and names orderReq := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: idents.ToProtoSlice(), } // Create an order for that request order, err := ra.NewOrder(ctx, orderReq) // It shouldn't fail test.AssertNotError(t, err, "Adding an order for regA failed") test.AssertEquals(t, numAuthorizations(order), 1) // It should be the fake near-expired-authz authz test.AssertEquals(t, order.V2Authorizations[0], int64(1)) // The order's expiry should be the fake authz's expiry since it is sooner // than the order's own expiry. test.AssertEquals(t, order.Expires.AsTime(), fakeAuthzExpires) // Set the order lifetime to be lower than the fakeAuthzLifetime ra.profiles.def().orderLifetime = 12 * time.Hour expectedOrderExpiry := clk.Now().Add(12 * time.Hour) // Create the order again order, err = ra.NewOrder(ctx, orderReq) // It shouldn't fail test.AssertNotError(t, err, "Adding an order for regA failed") test.AssertEquals(t, numAuthorizations(order), 1) // It should be the fake near-expired-authz authz test.AssertEquals(t, order.V2Authorizations[0], int64(1)) // The order's expiry should be the order's own expiry since it is sooner than // the fake authz's expiry. test.AssertEquals(t, order.Expires.AsTime(), expectedOrderExpiry) } func TestFinalizeOrder(t *testing.T) { _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() // Create one finalized authorization for not-example.com and one finalized // authorization for www.not-example.org now := ra.clk.Now() exp := now.Add(365 * 24 * time.Hour) authzIDA := createFinalizedAuthorization(t, sa, identifier.NewDNS("not-example.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) authzIDB := createFinalizedAuthorization(t, sa, identifier.NewDNS("www.not-example.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) testKey, err := rsa.GenerateKey(rand.Reader, 2048) test.AssertNotError(t, err, "error generating test key") policyForbidCSR, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ PublicKey: testKey.PublicKey, SignatureAlgorithm: x509.SHA256WithRSA, DNSNames: []string{"example.org"}, }, testKey) test.AssertNotError(t, err, "Error creating policy forbid CSR") oneDomainCSR, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ PublicKey: testKey.PublicKey, SignatureAlgorithm: x509.SHA256WithRSA, DNSNames: []string{"a.com"}, }, testKey) test.AssertNotError(t, err, "Error creating CSR with one DNS name") twoDomainCSR, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ PublicKey: testKey.PublicKey, SignatureAlgorithm: x509.SHA256WithRSA, DNSNames: []string{"a.com", "b.com"}, }, testKey) test.AssertNotError(t, err, "Error creating CSR with two DNS names") validCSR, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ PublicKey: testKey.Public(), SignatureAlgorithm: x509.SHA256WithRSA, DNSNames: []string{"not-example.com", "www.not-example.com"}, }, testKey) test.AssertNotError(t, err, "Error creating CSR with authorized names") expectedCert := &x509.Certificate{ SerialNumber: big.NewInt(0), Subject: pkix.Name{CommonName: "not-example.com"}, DNSNames: []string{"not-example.com", "www.not-example.com"}, PublicKey: testKey.Public(), NotBefore: now, BasicConstraintsValid: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, } certDER, err := x509.CreateCertificate(rand.Reader, expectedCert, expectedCert, testKey.Public(), testKey) test.AssertNotError(t, err, "failed to construct test certificate") ra.CA.(*mocks.MockCA).PEM = pem.EncodeToMemory(&pem.Block{Bytes: certDER, Type: "CERTIFICATE"}) fakeRegID := int64(0xB00) // NOTE(@cpu): We use unique `names` for each of these orders because // otherwise only *one* order is created & reused. The first test case to // finalize the order will put it into processing state and the other tests // will fail because you can't finalize an order that is already being // processed. // Add a new order for the fake reg ID fakeRegOrder, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{identifier.NewDNS("001.example.com").ToProto()}, }) test.AssertNotError(t, err, "Could not add test order for fake reg ID order ID") missingAuthzOrder, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{identifier.NewDNS("002.example.com").ToProto()}, }) test.AssertNotError(t, err, "Could not add test order for missing authz order ID") validatedOrder, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ NewOrder: &sapb.NewOrderRequest{ RegistrationID: Registration.Id, Expires: timestamppb.New(exp), Identifiers: []*corepb.Identifier{ identifier.NewDNS("not-example.com").ToProto(), identifier.NewDNS("www.not-example.com").ToProto(), }, V2Authorizations: []int64{authzIDA, authzIDB}, }, }) test.AssertNotError(t, err, "Could not add test order with finalized authz IDs, ready status") testCases := []struct { Name string OrderReq *rapb.FinalizeOrderRequest ExpectedErrMsg string ExpectIssuance bool }{ { Name: "No id in order", OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{}, Csr: oneDomainCSR, }, ExpectedErrMsg: "invalid order ID: 0", }, { Name: "No account id in order", OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Id: 1, }, Csr: oneDomainCSR, }, ExpectedErrMsg: "invalid account ID: 0", }, { Name: "No names in order", OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Id: 1, RegistrationID: 1, Status: string(core.StatusReady), Identifiers: []*corepb.Identifier{}, }, Csr: oneDomainCSR, }, ExpectedErrMsg: "Order has no associated identifiers", }, { Name: "Wrong order state (valid)", OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Id: 1, RegistrationID: 1, Status: string(core.StatusValid), Identifiers: []*corepb.Identifier{identifier.NewDNS("a.com").ToProto()}, }, Csr: oneDomainCSR, }, ExpectedErrMsg: `Order's status ("valid") is not acceptable for finalization`, }, { Name: "Wrong order state (pending)", OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Id: 1, RegistrationID: 1, Status: string(core.StatusPending), Identifiers: []*corepb.Identifier{identifier.NewDNS("a.com").ToProto()}, }, Csr: oneDomainCSR, }, ExpectIssuance: false, ExpectedErrMsg: `Order's status ("pending") is not acceptable for finalization`, }, { Name: "Invalid CSR", OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Id: 1, RegistrationID: 1, Status: string(core.StatusReady), Identifiers: []*corepb.Identifier{identifier.NewDNS("a.com").ToProto()}, }, Csr: []byte{0xC0, 0xFF, 0xEE}, }, ExpectedErrMsg: "unable to parse CSR: asn1: syntax error: truncated tag or length", }, { Name: "CSR and Order with diff number of names", OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Id: 1, RegistrationID: 1, Status: string(core.StatusReady), Identifiers: []*corepb.Identifier{ identifier.NewDNS("a.com").ToProto(), identifier.NewDNS("b.com").ToProto(), }, }, Csr: oneDomainCSR, }, ExpectedErrMsg: "CSR does not specify same identifiers as Order", }, { Name: "CSR and Order with diff number of names (other way)", OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Id: 1, RegistrationID: 1, Status: string(core.StatusReady), Identifiers: []*corepb.Identifier{identifier.NewDNS("a.com").ToProto()}, }, Csr: twoDomainCSR, }, ExpectedErrMsg: "CSR does not specify same identifiers as Order", }, { Name: "CSR missing an order name", OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Id: 1, RegistrationID: 1, Status: string(core.StatusReady), Identifiers: []*corepb.Identifier{identifier.NewDNS("foobar.com").ToProto()}, }, Csr: oneDomainCSR, }, ExpectedErrMsg: "CSR does not specify same identifiers as Order", }, { Name: "CSR with policy forbidden name", OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Id: 1, RegistrationID: 1, Status: string(core.StatusReady), Identifiers: []*corepb.Identifier{identifier.NewDNS("example.org").ToProto()}, Expires: timestamppb.New(exp), CertificateSerial: "", BeganProcessing: false, }, Csr: policyForbidCSR, }, ExpectedErrMsg: "Cannot issue for \"example.org\": The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy", }, { Name: "Order with missing registration", OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Status: string(core.StatusReady), Identifiers: []*corepb.Identifier{identifier.NewDNS("a.com").ToProto()}, Id: fakeRegOrder.Id, RegistrationID: fakeRegID, Expires: timestamppb.New(exp), CertificateSerial: "", BeganProcessing: false, Created: timestamppb.New(now), }, Csr: oneDomainCSR, }, ExpectedErrMsg: fmt.Sprintf("registration with ID '%d' not found", fakeRegID), }, { Name: "Order with missing authorizations", OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Status: string(core.StatusReady), Identifiers: []*corepb.Identifier{ identifier.NewDNS("a.com").ToProto(), identifier.NewDNS("b.com").ToProto(), }, Id: missingAuthzOrder.Id, RegistrationID: Registration.Id, Expires: timestamppb.New(exp), CertificateSerial: "", BeganProcessing: false, Created: timestamppb.New(now), }, Csr: twoDomainCSR, }, ExpectedErrMsg: "authorizations for these identifiers not found: a.com, b.com", }, { Name: "Order with correct authorizations, ready status", OrderReq: &rapb.FinalizeOrderRequest{ Order: validatedOrder, Csr: validCSR, }, ExpectIssuance: true, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { _, result := ra.FinalizeOrder(context.Background(), tc.OrderReq) // If we don't expect issuance we expect an error if !tc.ExpectIssuance { // Check that the error happened and the message matches expected test.AssertError(t, result, "FinalizeOrder did not fail when expected to") test.AssertEquals(t, result.Error(), tc.ExpectedErrMsg) } else { // Otherwise we expect an issuance and no error test.AssertNotError(t, result, fmt.Sprintf("FinalizeOrder result was %#v, expected nil", result)) // Check that the order now has a serial for the issued certificate updatedOrder, err := sa.GetOrder( context.Background(), &sapb.OrderRequest{Id: tc.OrderReq.Order.Id}) test.AssertNotError(t, err, "Error getting order to check serial") test.AssertNotEquals(t, updatedOrder.CertificateSerial, "") test.AssertEquals(t, updatedOrder.Status, "valid") test.AssertEquals(t, updatedOrder.Expires.AsTime(), exp) } }) } } func TestFinalizeOrderWithMixedSANAndCN(t *testing.T) { _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() // Pick an expiry in the future now := ra.clk.Now() exp := now.Add(365 * 24 * time.Hour) // Create one finalized authorization for Registration.Id for not-example.com and // one finalized authorization for Registration.Id for www.not-example.org authzIDA := createFinalizedAuthorization(t, sa, identifier.NewDNS("not-example.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) authzIDB := createFinalizedAuthorization(t, sa, identifier.NewDNS("www.not-example.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) // Create a new order to finalize with names in SAN and CN mixedOrder, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ NewOrder: &sapb.NewOrderRequest{ RegistrationID: Registration.Id, Expires: timestamppb.New(exp), Identifiers: []*corepb.Identifier{ identifier.NewDNS("not-example.com").ToProto(), identifier.NewDNS("www.not-example.com").ToProto(), }, V2Authorizations: []int64{authzIDA, authzIDB}, }, }) test.AssertNotError(t, err, "Could not add test order with finalized authz IDs") testKey, err := rsa.GenerateKey(rand.Reader, 2048) test.AssertNotError(t, err, "error generating test key") mixedCSR, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ PublicKey: testKey.PublicKey, SignatureAlgorithm: x509.SHA256WithRSA, Subject: pkix.Name{CommonName: "not-example.com"}, DNSNames: []string{"www.not-example.com"}, }, testKey) test.AssertNotError(t, err, "Could not create mixed CSR") template := &x509.Certificate{ SerialNumber: big.NewInt(12), Subject: pkix.Name{CommonName: "not-example.com"}, DNSNames: []string{"www.not-example.com", "not-example.com"}, NotBefore: time.Now(), BasicConstraintsValid: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, } cert, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey) test.AssertNotError(t, err, "Failed to create mixed cert") ra.CA = &mocks.MockCA{ PEM: pem.EncodeToMemory(&pem.Block{ Bytes: cert, }), } _, result := ra.FinalizeOrder(context.Background(), &rapb.FinalizeOrderRequest{Order: mixedOrder, Csr: mixedCSR}) test.AssertNotError(t, result, "FinalizeOrder failed") // Check that the order now has a serial for the issued certificate updatedOrder, err := sa.GetOrder( context.Background(), &sapb.OrderRequest{Id: mixedOrder.Id}) test.AssertNotError(t, err, "Error getting order to check serial") test.AssertNotEquals(t, updatedOrder.CertificateSerial, "") test.AssertEquals(t, updatedOrder.Status, "valid") } func TestFinalizeOrderWildcard(t *testing.T) { _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() // Pick an expiry in the future now := ra.clk.Now() exp := now.Add(365 * 24 * time.Hour) testKey, err := rsa.GenerateKey(rand.Reader, 2048) test.AssertNotError(t, err, "Error creating test RSA key") wildcardCSR, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ PublicKey: testKey.PublicKey, SignatureAlgorithm: x509.SHA256WithRSA, DNSNames: []string{"*.zombo.com"}, }, testKey) test.AssertNotError(t, err, "Error creating CSR with wildcard DNS name") template := &x509.Certificate{ SerialNumber: big.NewInt(1337), NotBefore: time.Now(), NotAfter: time.Now().AddDate(0, 0, 1), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, BasicConstraintsValid: true, Subject: pkix.Name{CommonName: "*.zombo.com"}, DNSNames: []string{"*.zombo.com"}, } certBytes, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey) test.AssertNotError(t, err, "Error creating test certificate") certPEM := pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: certBytes, }) // Set up a mock CA capable of giving back a cert for the wildcardCSR above ca := &mocks.MockCA{ PEM: certPEM, } ra.CA = ca // Create a new order for a wildcard domain orderIdents := identifier.ACMEIdentifiers{identifier.NewDNS("*.zombo.com")} test.AssertNotError(t, err, "Converting identifiers to DNS names") wildcardOrderRequest := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: orderIdents.ToProtoSlice(), } order, err := ra.NewOrder(context.Background(), wildcardOrderRequest) test.AssertNotError(t, err, "NewOrder failed for wildcard domain order") // Create one standard finalized authorization for Registration.Id for zombo.com _ = createFinalizedAuthorization(t, sa, identifier.NewDNS("zombo.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) // Finalizing the order should *not* work since the existing validated authz // is not a special DNS-01-Wildcard challenge authz, so the order will be // "pending" not "ready". finalizeReq := &rapb.FinalizeOrderRequest{ Order: order, Csr: wildcardCSR, } _, err = ra.FinalizeOrder(context.Background(), finalizeReq) test.AssertError(t, err, "FinalizeOrder did not fail for unauthorized "+ "wildcard order") test.AssertEquals(t, err.Error(), `Order's status ("pending") is not acceptable for finalization`) // Creating another order for the wildcard name validOrder, err := ra.NewOrder(context.Background(), wildcardOrderRequest) test.AssertNotError(t, err, "NewOrder failed for wildcard domain order") test.AssertEquals(t, numAuthorizations(validOrder), 1) // We expect to be able to get the authorization by ID _, err = sa.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: validOrder.V2Authorizations[0]}) test.AssertNotError(t, err, "sa.GetAuthorization2 failed") // Finalize the authorization with the challenge validated expires := now.Add(time.Hour * 24 * 7) _, err = sa.FinalizeAuthorization2(ctx, &sapb.FinalizeAuthorizationRequest{ Id: validOrder.V2Authorizations[0], Status: string(core.StatusValid), Expires: timestamppb.New(expires), Attempted: string(core.ChallengeTypeDNS01), AttemptedAt: timestamppb.New(now), }) test.AssertNotError(t, err, "sa.FinalizeAuthorization2 failed") // Refresh the order so the SA sets its status validOrder, err = sa.GetOrder(ctx, &sapb.OrderRequest{ Id: validOrder.Id, }) test.AssertNotError(t, err, "Could not refresh valid order from SA") // Now it should be possible to finalize the order finalizeReq = &rapb.FinalizeOrderRequest{ Order: validOrder, Csr: wildcardCSR, } _, err = ra.FinalizeOrder(context.Background(), finalizeReq) test.AssertNotError(t, err, "FinalizeOrder failed for authorized "+ "wildcard order") } func TestFinalizeOrderDisabledChallenge(t *testing.T) { _, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() domain := randomDomain() ident := identifier.NewDNS(domain) // Create a finalized authorization for that domain authzID := createFinalizedAuthorization( t, sa, ident, fc.Now().Add(24*time.Hour), core.ChallengeTypeHTTP01, fc.Now().Add(-1*time.Hour)) // Create an order that reuses that authorization order, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{ident.ToProto()}, }) test.AssertNotError(t, err, "creating test order") test.AssertEquals(t, order.V2Authorizations[0], authzID) // Create a CSR for this order testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "generating test key") csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ PublicKey: testKey.PublicKey, DNSNames: []string{domain}, }, testKey) test.AssertNotError(t, err, "Error creating policy forbid CSR") // Replace the Policy Authority with one which has this challenge type disabled pa, err := policy.New( map[identifier.IdentifierType]bool{ identifier.TypeDNS: true, identifier.TypeIP: true, }, map[core.AcmeChallenge]bool{ core.ChallengeTypeDNS01: true, core.ChallengeTypeTLSALPN01: true, }, ra.log) test.AssertNotError(t, err, "creating test PA") err = pa.LoadHostnamePolicyFile("../test/hostname-policy.yaml") test.AssertNotError(t, err, "loading test hostname policy") ra.PA = pa // Now finalizing this order should fail _, err = ra.FinalizeOrder(context.Background(), &rapb.FinalizeOrderRequest{ Order: order, Csr: csr, }) test.AssertError(t, err, "finalization should fail") // Unfortunately we can't test for the PA's "which is now disabled" error // message directly, because the RA discards it and collects all invalid names // into a single more generic error message. But it does at least distinguish // between missing, expired, and invalid, so we can test for "invalid". test.AssertContains(t, err.Error(), "authorizations for these identifiers not valid") } func TestFinalizeWithMustStaple(t *testing.T) { _, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() ocspMustStapleExt := pkix.Extension{ // RFC 7633: id-pe-tlsfeature OBJECT IDENTIFIER ::= { id-pe 24 } Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, // ASN.1 encoding of: // SEQUENCE // INTEGER 5 // where "5" is the status_request feature (RFC 6066) Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, } domain := randomDomain() authzID := createFinalizedAuthorization( t, sa, identifier.NewDNS(domain), fc.Now().Add(24*time.Hour), core.ChallengeTypeHTTP01, fc.Now().Add(-1*time.Hour)) order, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{identifier.NewDNS(domain).ToProto()}, }) test.AssertNotError(t, err, "creating test order") test.AssertEquals(t, order.V2Authorizations[0], authzID) testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "generating test key") csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ PublicKey: testKey.Public(), DNSNames: []string{domain}, ExtraExtensions: []pkix.Extension{ocspMustStapleExt}, }, testKey) test.AssertNotError(t, err, "creating must-staple CSR") serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) test.AssertNotError(t, err, "generating random serial number") template := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{CommonName: domain}, DNSNames: []string{domain}, NotBefore: fc.Now(), NotAfter: fc.Now().Add(365 * 24 * time.Hour), BasicConstraintsValid: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, ExtraExtensions: []pkix.Extension{ocspMustStapleExt}, } cert, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey) test.AssertNotError(t, err, "creating certificate") ra.CA = &mocks.MockCA{ PEM: pem.EncodeToMemory(&pem.Block{ Bytes: cert, Type: "CERTIFICATE", }), } _, err = ra.FinalizeOrder(context.Background(), &rapb.FinalizeOrderRequest{ Order: order, Csr: csr, }) test.AssertError(t, err, "finalization should fail") test.AssertContains(t, err.Error(), "no longer available") test.AssertMetricWithLabelsEquals(t, ra.mustStapleRequestsCounter, prometheus.Labels{"allowlist": "denied"}, 1) } func TestIssueCertificateAuditLog(t *testing.T) { _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() // Make some valid authorizations for some names using different challenge types names := []string{"not-example.com", "www.not-example.com", "still.not-example.com", "definitely.not-example.com"} idents := identifier.ACMEIdentifiers{ identifier.NewDNS("not-example.com"), identifier.NewDNS("www.not-example.com"), identifier.NewDNS("still.not-example.com"), identifier.NewDNS("definitely.not-example.com"), } exp := ra.clk.Now().Add(ra.profiles.def().orderLifetime) challs := []core.AcmeChallenge{core.ChallengeTypeHTTP01, core.ChallengeTypeDNS01, core.ChallengeTypeHTTP01, core.ChallengeTypeDNS01} var authzIDs []int64 for i, ident := range idents { authzIDs = append(authzIDs, createFinalizedAuthorization(t, sa, ident, exp, challs[i], ra.clk.Now())) } // Create a pending order for all of the names order, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ NewOrder: &sapb.NewOrderRequest{ RegistrationID: Registration.Id, Expires: timestamppb.New(exp), Identifiers: idents.ToProtoSlice(), V2Authorizations: authzIDs, }, }) test.AssertNotError(t, err, "Could not add test order with finalized authz IDs") // Generate a CSR covering the order names with a random RSA key testKey, err := rsa.GenerateKey(rand.Reader, 2048) test.AssertNotError(t, err, "error generating test key") csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ PublicKey: testKey.PublicKey, SignatureAlgorithm: x509.SHA256WithRSA, Subject: pkix.Name{CommonName: "not-example.com"}, DNSNames: names, }, testKey) test.AssertNotError(t, err, "Could not create test order CSR") // Create a mock certificate for the fake CA to return template := &x509.Certificate{ SerialNumber: big.NewInt(12), Subject: pkix.Name{ CommonName: "not-example.com", }, DNSNames: names, NotBefore: time.Now(), NotAfter: time.Now().AddDate(0, 0, 1), BasicConstraintsValid: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, } cert, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey) test.AssertNotError(t, err, "Failed to create mock cert for test CA") // Set up the RA's CA with a mock that returns the cert from above ra.CA = &mocks.MockCA{ PEM: pem.EncodeToMemory(&pem.Block{ Bytes: cert, }), } // The mock cert needs to be parsed to get its notbefore/notafter dates parsedCerts, err := x509.ParseCertificates(cert) test.AssertNotError(t, err, "Failed to parse mock cert DER bytes") test.AssertEquals(t, len(parsedCerts), 1) parsedCert := parsedCerts[0] // Cast the RA's mock log so we can ensure its cleared and can access the // matched log lines mockLog := ra.log.(*blog.Mock) mockLog.Clear() // Finalize the order with the CSR order.Status = string(core.StatusReady) _, err = ra.FinalizeOrder(context.Background(), &rapb.FinalizeOrderRequest{ Order: order, Csr: csr, }) test.AssertNotError(t, err, "Error finalizing test order") // Get the logged lines from the audit logger loglines := mockLog.GetAllMatching("Certificate request - successful JSON=") // There should be exactly 1 matching log line test.AssertEquals(t, len(loglines), 1) // Strip away the stuff before 'JSON=' jsonContent := strings.TrimPrefix(loglines[0], "INFO: [AUDIT] Certificate request - successful JSON=") // Unmarshal the JSON into a certificate request event object var event certificateRequestEvent err = json.Unmarshal([]byte(jsonContent), &event) // The JSON should unmarshal without error test.AssertNotError(t, err, "Error unmarshalling logged JSON issuance event") // The event should have no error test.AssertEquals(t, event.Error, "") // The event requester should be the expected reg ID test.AssertEquals(t, event.Requester, Registration.Id) // The event order ID should be the expected order ID test.AssertEquals(t, event.OrderID, order.Id) // The event serial number should be the expected serial number test.AssertEquals(t, event.SerialNumber, core.SerialToString(template.SerialNumber)) // The event verified fields should be the expected value test.AssertDeepEquals(t, event.VerifiedFields, []string{"subject.commonName", "subjectAltName"}) // The event CommonName should match the expected common name test.AssertEquals(t, event.CommonName, "not-example.com") // The event identifiers should match the order identifiers test.AssertDeepEquals(t, identifier.Normalize(event.Identifiers), identifier.Normalize(identifier.FromProtoSlice(order.Identifiers))) // The event's NotBefore and NotAfter should match the cert's test.AssertEquals(t, event.NotBefore, parsedCert.NotBefore) test.AssertEquals(t, event.NotAfter, parsedCert.NotAfter) // There should be one event Authorization entry for each name test.AssertEquals(t, len(event.Authorizations), len(names)) // Check the authz entry for each name for i, name := range names { authzEntry := event.Authorizations[name] // The authz entry should have the correct authz ID test.AssertEquals(t, authzEntry.ID, fmt.Sprintf("%d", authzIDs[i])) // The authz entry should have the correct challenge type test.AssertEquals(t, authzEntry.ChallengeType, challs[i]) } } func TestIssueCertificateCAACheckLog(t *testing.T) { _, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() ra.VA = va.RemoteClients{CAAClient: &noopCAA{}} exp := fc.Now().Add(24 * time.Hour) recent := fc.Now().Add(-1 * time.Hour) older := fc.Now().Add(-8 * time.Hour) // Make some valid authzs for four names. Half of them were validated // recently and half were validated in excess of our CAA recheck time. names := []string{ "not-example.com", "www.not-example.com", "still.not-example.com", "definitely.not-example.com", } idents := identifier.NewDNSSlice(names) var authzIDs []int64 for i, ident := range idents { attemptedAt := older if i%2 == 0 { attemptedAt = recent } authzIDs = append(authzIDs, createFinalizedAuthorization(t, sa, ident, exp, core.ChallengeTypeHTTP01, attemptedAt)) } // Create a pending order for all of the names. order, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ NewOrder: &sapb.NewOrderRequest{ RegistrationID: Registration.Id, Expires: timestamppb.New(exp), Identifiers: idents.ToProtoSlice(), V2Authorizations: authzIDs, }, }) test.AssertNotError(t, err, "Could not add test order with finalized authz IDs") // Generate a CSR covering the order names with a random RSA key. testKey, err := rsa.GenerateKey(rand.Reader, 2048) test.AssertNotError(t, err, "error generating test key") csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ PublicKey: testKey.PublicKey, SignatureAlgorithm: x509.SHA256WithRSA, Subject: pkix.Name{CommonName: "not-example.com"}, DNSNames: names, }, testKey) test.AssertNotError(t, err, "Could not create test order CSR") // Create a mock certificate for the fake CA to return. template := &x509.Certificate{ SerialNumber: big.NewInt(12), Subject: pkix.Name{ CommonName: "not-example.com", }, DNSNames: names, NotBefore: time.Now(), NotAfter: time.Now().AddDate(0, 0, 1), BasicConstraintsValid: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, } cert, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey) test.AssertNotError(t, err, "Failed to create mock cert for test CA") // Set up the RA's CA with a mock that returns the cert from above. ra.CA = &mocks.MockCA{ PEM: pem.EncodeToMemory(&pem.Block{ Bytes: cert, }), } // Cast the RA's mock log so we can ensure its cleared and can access the // matched log lines. mockLog := ra.log.(*blog.Mock) mockLog.Clear() // Finalize the order with the CSR. order.Status = string(core.StatusReady) _, err = ra.FinalizeOrder(context.Background(), &rapb.FinalizeOrderRequest{ Order: order, Csr: csr, }) test.AssertNotError(t, err, "Error finalizing test order") // Get the logged lines from the mock logger. loglines := mockLog.GetAllMatching("FinalizationCaaCheck JSON=") // There should be exactly 1 matching log line. test.AssertEquals(t, len(loglines), 1) // Strip away the stuff before 'JSON='. jsonContent := strings.TrimPrefix(loglines[0], "INFO: FinalizationCaaCheck JSON=") // Unmarshal the JSON into an event object. var event finalizationCAACheckEvent err = json.Unmarshal([]byte(jsonContent), &event) // The JSON should unmarshal without error. test.AssertNotError(t, err, "Error unmarshalling logged JSON issuance event.") // The event requester should be the expected registration ID. test.AssertEquals(t, event.Requester, Registration.Id) // The event should have the expected number of Authzs where CAA was reused. test.AssertEquals(t, event.Reused, 2) // The event should have the expected number of Authzs where CAA was // rechecked. test.AssertEquals(t, event.Rechecked, 2) } // TestUpdateMissingAuthorization tests the race condition where a challenge is // updated to valid concurrently with another attempt to have the challenge // updated. Previously this would return a `berrors.InternalServer` error when // the row was found missing from `pendingAuthorizations` by the 2nd update // since the 1st had already deleted it. We accept this may happen and now test // for a `berrors.NotFound` error return. // // See https://github.com/letsencrypt/boulder/issues/3201 func TestUpdateMissingAuthorization(t *testing.T) { _, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() ctx := context.Background() authzPB := createPendingAuthorization(t, sa, identifier.NewDNS("example.com"), fc.Now().Add(12*time.Hour)) authz, err := bgrpc.PBToAuthz(authzPB) test.AssertNotError(t, err, "failed to deserialize authz") // Twiddle the authz to pretend its been validated by the VA authz.Challenges[0].Status = "valid" err = ra.recordValidation(ctx, authz.ID, fc.Now().Add(24*time.Hour), &authz.Challenges[0]) test.AssertNotError(t, err, "ra.recordValidation failed") // Try to record the same validation a second time. err = ra.recordValidation(ctx, authz.ID, fc.Now().Add(25*time.Hour), &authz.Challenges[0]) test.AssertError(t, err, "ra.recordValidation didn't fail") test.AssertErrorIs(t, err, berrors.NotFound) } func TestPerformValidationBadChallengeType(t *testing.T) { _, _, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() pa, err := policy.New(map[identifier.IdentifierType]bool{}, map[core.AcmeChallenge]bool{}, blog.NewMock()) test.AssertNotError(t, err, "Couldn't create PA") ra.PA = pa exp := fc.Now().Add(10 * time.Hour) authz := core.Authorization{ ID: "1337", Identifier: identifier.NewDNS("not-example.com"), RegistrationID: 1, Status: "valid", Challenges: []core.Challenge{ { Status: core.StatusValid, Type: core.ChallengeTypeHTTP01, Token: "exampleToken", }, }, Expires: &exp, } authzPB, err := bgrpc.AuthzToPB(authz) test.AssertNotError(t, err, "AuthzToPB failed") _, err = ra.PerformValidation(context.Background(), &rapb.PerformValidationRequest{ Authz: authzPB, ChallengeIndex: 0, }) test.AssertError(t, err, "ra.PerformValidation allowed a update to a authorization") test.AssertEquals(t, err.Error(), "challenge type \"http-01\" no longer allowed") } type timeoutPub struct { } func (mp *timeoutPub) SubmitToSingleCTWithResult(_ context.Context, _ *pubpb.Request, _ ...grpc.CallOption) (*pubpb.Result, error) { return nil, context.DeadlineExceeded } func TestCTPolicyMeasurements(t *testing.T) { _, _, ra, _, _, cleanup := initAuthorities(t) defer cleanup() ra.ctpolicy = ctpolicy.New(&timeoutPub{}, loglist.List{ {Name: "LogA1", Operator: "OperA", Url: "UrlA1", Key: []byte("KeyA1")}, {Name: "LogB1", Operator: "OperB", Url: "UrlB1", Key: []byte("KeyB1")}, }, nil, nil, 0, log, metrics.NoopRegisterer) _, cert := test.ThrowAwayCert(t, clock.NewFake()) _, err := ra.GetSCTs(context.Background(), &rapb.SCTRequest{ PrecertDER: cert.Raw, }) test.AssertError(t, err, "GetSCTs should have failed when SCTs timed out") test.AssertContains(t, err.Error(), "failed to get 2 SCTs") test.AssertMetricWithLabelsEquals(t, ra.ctpolicyResults, prometheus.Labels{"result": "failure"}, 1) } func TestWildcardOverlap(t *testing.T) { err := wildcardOverlap(identifier.ACMEIdentifiers{ identifier.NewDNS("*.example.com"), identifier.NewDNS("*.example.net"), }) if err != nil { t.Errorf("Got error %q, expected none", err) } err = wildcardOverlap(identifier.ACMEIdentifiers{ identifier.NewDNS("*.example.com"), identifier.NewDNS("*.example.net"), identifier.NewDNS("www.example.com"), }) if err == nil { t.Errorf("Got no error, expected one") } test.AssertErrorIs(t, err, berrors.Malformed) err = wildcardOverlap(identifier.ACMEIdentifiers{ identifier.NewDNS("*.foo.example.com"), identifier.NewDNS("*.example.net"), identifier.NewDNS("www.example.com"), }) if err != nil { t.Errorf("Got error %q, expected none", err) } } type MockCARecordingProfile struct { inner *mocks.MockCA profileName string } func (ca *MockCARecordingProfile) IssueCertificate(ctx context.Context, req *capb.IssueCertificateRequest, _ ...grpc.CallOption) (*capb.IssueCertificateResponse, error) { ca.profileName = req.CertProfileName return ca.inner.IssueCertificate(ctx, req) } type mockSAWithFinalize struct { sapb.StorageAuthorityClient } func (sa *mockSAWithFinalize) FinalizeOrder(ctx context.Context, req *sapb.FinalizeOrderRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { return &emptypb.Empty{}, nil } func (sa *mockSAWithFinalize) FQDNSetTimestampsForWindow(ctx context.Context, in *sapb.CountFQDNSetsRequest, opts ...grpc.CallOption) (*sapb.Timestamps, error) { return &sapb.Timestamps{ Timestamps: []*timestamppb.Timestamp{ timestamppb.Now(), }, }, nil } func TestIssueCertificateOuter(t *testing.T) { _, _, ra, _, fc, cleanup := initAuthorities(t) defer cleanup() ra.SA = &mockSAWithFinalize{} // Create a CSR to submit and a certificate for the fake CA to return. testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "generating test key") csrDER, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{DNSNames: []string{"example.com"}}, testKey) test.AssertNotError(t, err, "creating test csr") csr, err := x509.ParseCertificateRequest(csrDER) test.AssertNotError(t, err, "parsing test csr") certDER, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{ SerialNumber: big.NewInt(1), DNSNames: []string{"example.com"}, NotBefore: fc.Now(), BasicConstraintsValid: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, }, &x509.Certificate{}, testKey.Public(), testKey) test.AssertNotError(t, err, "creating test cert") certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) for _, tc := range []struct { name string profile string wantProfile string }{ { name: "select default profile when none specified", wantProfile: "test", // matches ra.defaultProfileName }, { name: "default profile specified", profile: "test", wantProfile: "test", }, { name: "other profile specified", profile: "other", wantProfile: "other", }, } { t.Run(tc.name, func(t *testing.T) { // Use a mock CA that will record the profile name and profile hash included // in the RA's request messages. Populate it with the cert generated above. mockCA := MockCARecordingProfile{inner: &mocks.MockCA{PEM: certPEM}} ra.CA = &mockCA order := &corepb.Order{ RegistrationID: Registration.Id, Expires: timestamppb.New(fc.Now().Add(24 * time.Hour)), Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, CertificateProfileName: tc.profile, } order, err = ra.issueCertificateOuter(context.Background(), order, csr, certificateRequestEvent{}) // The resulting order should have new fields populated if order.Status != string(core.StatusValid) { t.Errorf("order.Status = %+v, want %+v", order.Status, core.StatusValid) } if order.CertificateSerial != core.SerialToString(big.NewInt(1)) { t.Errorf("CertificateSerial = %+v, want %+v", order.CertificateSerial, 1) } // The recorded profile and profile hash should match what we expect. if mockCA.profileName != tc.wantProfile { t.Errorf("recorded profileName = %+v, want %+v", mockCA.profileName, tc.wantProfile) } }) } } func TestNewOrderMaxNames(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ra.profiles.def().maxNames = 2 _, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: 1, Identifiers: []*corepb.Identifier{ identifier.NewDNS("a").ToProto(), identifier.NewDNS("b").ToProto(), identifier.NewDNS("c").ToProto(), }, }) test.AssertError(t, err, "NewOrder didn't fail with too many names in request") test.AssertEquals(t, err.Error(), "Order cannot contain more than 2 identifiers") test.AssertErrorIs(t, err, berrors.Malformed) } // CSR generated by Go: // * Random public key // * CN = not-example.com // * DNSNames = not-example.com, www.not-example.com var CSRPEM = []byte(` -----BEGIN CERTIFICATE REQUEST----- MIICrjCCAZYCAQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMTD25vdC1leGFtcGxl LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKT1B7UsonZuLOp7 qq2pw+COo0I9ZheuhN9ltu1+bAMWBYUb8KFPNGGp8Ygt6YCLjlnWOche7Fjb5lPj hV6U2BkEt85mdaGTDg6mU3qjk2/cnZeAvJWW5ewYOBGxN/g/KHgdYZ+uhHH/PbGt Wktcv5bRJ9Dxbjxsy7l8SLQ6fd/MF/3z6sBJzIHkcDupDOFdPN/Z0KOw7BOPHAbg ghLJTmiESA1Ljxb8848bENlCz8pVizIu2Ilr4xBPtA5oUfO0FJKbT1T66JZoqwy/ drfrlHA7F6c8kYlAmwiOfWHzlWCkE1YuZPJrZQrt4tJ70rrPxV1qEGJDumzgcEbU /aYYiBsCAwEAAaBCMEAGCSqGSIb3DQEJDjEzMDEwLwYDVR0RBCgwJoIPbm90LWV4 YW1wbGUuY29tghN3d3cubm90LWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IB AQBuFo5SHqN1lWmM6rKaOBXFezAdzZyGb9x8+5Zq/eh9pSxpn0MTOmq/u+sDHxsC ywcshUO3P9//9u4ALtNn/jsJmSrElsTvG3SH5owl9muNEiOgf+6/rY/X8Zcnv/e0 Ar9r73BcCkjoAOFbr7xiLLYu5EaBQjSj6/m4ujwJTWS2SqobK5VfdpzmDp4wT3eB V4FPLxyxxOLuWLzcBkDdLw/zh922HtR5fqk155Y4pj3WS9NnI/NMHmclrlfY/2P4 dJrBVM+qVbPTzM19QplMkiy7FxpDx6toUXDYM4KdKKV0+yX/zw/V0/Gb7K7yIjVB wqjllqgMjN4nvHjiDXFx/kPY -----END CERTIFICATE REQUEST----- `) var eeCertPEM = []byte(` -----BEGIN CERTIFICATE----- MIIEfTCCAmWgAwIBAgISCr9BRk0C9OOGVke6CAa8F+AXMA0GCSqGSIb3DQEBCwUA MDExCzAJBgNVBAYTAlVTMRAwDgYDVQQKDAdUZXN0IENBMRAwDgYDVQQDDAdUZXN0 IENBMB4XDTE2MDMyMDE4MTEwMFoXDTE2MDMyMDE5MTEwMFowHjEcMBoGA1UEAxMT d3d3Lm5vdC1leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAKT1B7UsonZuLOp7qq2pw+COo0I9ZheuhN9ltu1+bAMWBYUb8KFPNGGp8Ygt 6YCLjlnWOche7Fjb5lPjhV6U2BkEt85mdaGTDg6mU3qjk2/cnZeAvJWW5ewYOBGx N/g/KHgdYZ+uhHH/PbGtWktcv5bRJ9Dxbjxsy7l8SLQ6fd/MF/3z6sBJzIHkcDup DOFdPN/Z0KOw7BOPHAbgghLJTmiESA1Ljxb8848bENlCz8pVizIu2Ilr4xBPtA5o UfO0FJKbT1T66JZoqwy/drfrlHA7F6c8kYlAmwiOfWHzlWCkE1YuZPJrZQrt4tJ7 0rrPxV1qEGJDumzgcEbU/aYYiBsCAwEAAaOBoTCBnjAdBgNVHSUEFjAUBggrBgEF BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUIEr9ryJ0aJuD CwBsCp7Eun8Hx4AwHwYDVR0jBBgwFoAUmiamd/N/8knrCb1QlhwB4WXCqaswLwYD VR0RBCgwJoIPbm90LWV4YW1wbGUuY29tghN3d3cubm90LWV4YW1wbGUuY29tMA0G CSqGSIb3DQEBCwUAA4ICAQBpGLrCt38Z+knbuE1ALEB3hqUQCAm1OPDW6HR+v2nO f2ERxTwL9Cad++3vONxgB68+6KQeIf5ph48OGnS5DgO13mb2cxLlmM2IJpkbSFtW VeRNFt/WxRJafpbKw2hgQNJ/sxEAsCyA+kVeh1oCxGQyPO7IIXtw5FecWfIiNNwM mVM17uchtvsM5BRePvet9xZxrKOFnn6TQRs8vC4e59Y8h52On+L2Q/ytAa7j3+fb 7OYCe+yWypGeosekamZTMBjHFV3RRxsGdRATSuZkv1uewyUnEPmsy5Ow4doSYZKW QmKjti+vv1YhAhFxPArob0SG3YOiFuKzZ9rSOhUtzSg01ml/kRyOiC7rfO7NRzHq idhPUhu2QBmdJTLLOBQLvKDNDOHqDYwKdIHJ7pup2y0Fvm4T96q5bnrSdmz/QAlB XVw08HWMcjeOeHYiHST3yxYfQivTNm2PlKfUACb7vcrQ6pYhOnVdYgJZm6gkV4Xd K1HKja36snIevv/gSgsE7bGcBYLVCvf16o3IRt9K8CpDoSsWn0iAVcwUP2CyPLm4 QsqA1afjTUPKQTAgDKRecDPhrT1+FjtBwdpXetpRiBK0UE5exfnI4nszZ9+BYG1l xGUhoOJp0T++nz6R3TX7Rwk7KmG6xX3vWr/MFu5A3c8fvkqj987Vti5BeBezCXfs rA== -----END CERTIFICATE----- `) // mockSARevocation is a fake which includes all of the SA methods called in the // course of a revocation. Its behavior can be customized by providing sets of // issued (known) certs, already-revoked certs, and already-blocked keys. It // also updates the sets of revoked certs and blocked keys when certain methods // are called, to allow for more complex test logic. type mockSARevocation struct { sapb.StorageAuthorityClient known map[string]*x509.Certificate revoked map[string]*corepb.CertificateStatus blocked []*sapb.AddBlockedKeyRequest } func newMockSARevocation(known *x509.Certificate) *mockSARevocation { return &mockSARevocation{ known: map[string]*x509.Certificate{core.SerialToString(known.SerialNumber): known}, revoked: make(map[string]*corepb.CertificateStatus), blocked: make([]*sapb.AddBlockedKeyRequest, 0), } } func (msar *mockSARevocation) reset() { msar.revoked = make(map[string]*corepb.CertificateStatus) msar.blocked = make([]*sapb.AddBlockedKeyRequest, 0) } func (msar *mockSARevocation) AddBlockedKey(_ context.Context, req *sapb.AddBlockedKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { msar.blocked = append(msar.blocked, req) return &emptypb.Empty{}, nil } func (msar *mockSARevocation) GetSerialMetadata(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*sapb.SerialMetadata, error) { if cert, present := msar.known[req.Serial]; present { return &sapb.SerialMetadata{ Serial: req.Serial, RegistrationID: 1, Created: timestamppb.New(cert.NotBefore), Expires: timestamppb.New(cert.NotAfter), }, nil } return nil, berrors.UnknownSerialError() } func (msar *mockSARevocation) GetLintPrecertificate(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) { if cert, present := msar.known[req.Serial]; present { return &corepb.Certificate{Der: cert.Raw}, nil } return nil, berrors.UnknownSerialError() } func (msar *mockSARevocation) GetCertificateStatus(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.CertificateStatus, error) { if status, present := msar.revoked[req.Serial]; present { return status, nil } if cert, present := msar.known[req.Serial]; present { return &corepb.CertificateStatus{ Serial: core.SerialToString(cert.SerialNumber), IssuerID: int64(issuance.IssuerNameID(cert)), }, nil } return nil, berrors.UnknownSerialError() } func (msar *mockSARevocation) GetCertificate(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) { var serialBytes [16]byte _, _ = rand.Read(serialBytes[:]) serial := big.NewInt(0).SetBytes(serialBytes[:]) key, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) if err != nil { return nil, err } template := &x509.Certificate{ SerialNumber: serial, DNSNames: []string{"revokememaybe.example.com"}, NotBefore: time.Now(), NotAfter: time.Now().Add(6 * 24 * time.Hour), IssuingCertificateURL: []string{"http://localhost:4001/acme/issuer-cert/1234"}, CRLDistributionPoints: []string{"http://example.com/123.crl"}, } testCertDER, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) if err != nil { return nil, err } return &corepb.Certificate{ Der: testCertDER, }, nil } func (msar *mockSARevocation) RevokeCertificate(_ context.Context, req *sapb.RevokeCertificateRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { if _, present := msar.revoked[req.Serial]; present { return nil, berrors.AlreadyRevokedError("already revoked") } cert, present := msar.known[req.Serial] if !present { return nil, berrors.UnknownSerialError() } msar.revoked[req.Serial] = &corepb.CertificateStatus{ Serial: req.Serial, IssuerID: int64(issuance.IssuerNameID(cert)), Status: string(core.OCSPStatusRevoked), RevokedReason: req.Reason, } return &emptypb.Empty{}, nil } func (msar *mockSARevocation) UpdateRevokedCertificate(_ context.Context, req *sapb.RevokeCertificateRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { status, present := msar.revoked[req.Serial] if !present { return nil, errors.New("not already revoked") } if req.Reason != ocsp.KeyCompromise { return nil, errors.New("cannot re-revoke except for keyCompromise") } if present && status.RevokedReason == ocsp.KeyCompromise { return nil, berrors.AlreadyRevokedError("already revoked for keyCompromise") } msar.revoked[req.Serial].RevokedReason = req.Reason return &emptypb.Empty{}, nil } type mockOCSPA struct { mocks.MockCA } func (mcao *mockOCSPA) GenerateOCSP(context.Context, *capb.GenerateOCSPRequest, ...grpc.CallOption) (*capb.OCSPResponse, error) { return &capb.OCSPResponse{Response: []byte{1, 2, 3}}, nil } type mockPurger struct{} func (mp *mockPurger) Purge(context.Context, *akamaipb.PurgeRequest, ...grpc.CallOption) (*emptypb.Empty, error) { return &emptypb.Empty{}, nil } // mockSAGenerateOCSP is a mock SA that always returns a good OCSP response, with a constant NotAfter. type mockSAGenerateOCSP struct { sapb.StorageAuthorityClient expiration time.Time } func (msgo *mockSAGenerateOCSP) GetCertificateStatus(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.CertificateStatus, error) { return &corepb.CertificateStatus{ Serial: req.Serial, Status: "good", NotAfter: timestamppb.New(msgo.expiration.UTC()), }, nil } func TestGenerateOCSP(t *testing.T) { _, _, ra, _, clk, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} ra.SA = &mockSAGenerateOCSP{expiration: clk.Now().Add(time.Hour)} req := &rapb.GenerateOCSPRequest{ Serial: core.SerialToString(big.NewInt(1)), } resp, err := ra.GenerateOCSP(context.Background(), req) test.AssertNotError(t, err, "generating OCSP") test.AssertByteEquals(t, resp.Response, []byte{1, 2, 3}) ra.SA = &mockSAGenerateOCSP{expiration: clk.Now().Add(-time.Hour)} _, err = ra.GenerateOCSP(context.Background(), req) if !errors.Is(err, berrors.NotFound) { t.Errorf("expected NotFound error, got %s", err) } } // mockSALongExpiredSerial is a mock SA that treats every serial as if it expired a long time ago. // Specifically, it returns NotFound to GetCertificateStatus (simulating the serial having been // removed from the certificateStatus table), but returns success to GetSerialMetadata (simulating // a serial number staying in the `serials` table indefinitely). type mockSALongExpiredSerial struct { sapb.StorageAuthorityClient } func (msgo *mockSALongExpiredSerial) GetCertificateStatus(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.CertificateStatus, error) { return nil, berrors.NotFoundError("not found") } func (msgo *mockSALongExpiredSerial) GetSerialMetadata(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*sapb.SerialMetadata, error) { return &sapb.SerialMetadata{ Serial: req.Serial, }, nil } func TestGenerateOCSPLongExpiredSerial(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} ra.SA = &mockSALongExpiredSerial{} req := &rapb.GenerateOCSPRequest{ Serial: core.SerialToString(big.NewInt(1)), } _, err := ra.GenerateOCSP(context.Background(), req) test.AssertError(t, err, "generating OCSP") if !errors.Is(err, berrors.NotFound) { t.Errorf("expected NotFound error, got %#v", err) } } // mockSAUnknownSerial is a mock SA that always returns NotFound to certificate status and serial lookups. // It emulates an SA that has never issued a certificate. type mockSAUnknownSerial struct { mockSALongExpiredSerial } func (msgo *mockSAUnknownSerial) GetSerialMetadata(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*sapb.SerialMetadata, error) { return nil, berrors.NotFoundError("not found") } func TestGenerateOCSPUnknownSerial(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} ra.SA = &mockSAUnknownSerial{} req := &rapb.GenerateOCSPRequest{ Serial: core.SerialToString(big.NewInt(1)), } _, err := ra.GenerateOCSP(context.Background(), req) test.AssertError(t, err, "generating OCSP") if !errors.Is(err, berrors.UnknownSerial) { t.Errorf("expected UnknownSerial error, got %#v", err) } } func TestRevokeCertByApplicant_Subscriber(t *testing.T) { _, _, ra, _, clk, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} ra.purger = &mockPurger{} // Use the same self-signed cert as both issuer and issuee for revocation. _, cert := test.ThrowAwayCert(t, clk) cert.IsCA = true ic, err := issuance.NewCertificate(cert) test.AssertNotError(t, err, "failed to create issuer cert") ra.issuersByNameID = map[issuance.NameID]*issuance.Certificate{ ic.NameID(): ic, } ra.SA = newMockSARevocation(cert) // Revoking without a regID should fail. _, err = ra.RevokeCertByApplicant(context.Background(), &rapb.RevokeCertByApplicantRequest{ Cert: cert.Raw, Code: ocsp.Unspecified, RegID: 0, }) test.AssertError(t, err, "should have failed with no RegID") test.AssertContains(t, err.Error(), "incomplete") // Revoking for a disallowed reason should fail. _, err = ra.RevokeCertByApplicant(context.Background(), &rapb.RevokeCertByApplicantRequest{ Cert: cert.Raw, Code: ocsp.CertificateHold, RegID: 1, }) test.AssertError(t, err, "should have failed with bad reasonCode") test.AssertContains(t, err.Error(), "disallowed revocation reason") // Revoking with the correct regID should succeed. _, err = ra.RevokeCertByApplicant(context.Background(), &rapb.RevokeCertByApplicantRequest{ Cert: cert.Raw, Code: ocsp.Unspecified, RegID: 1, }) test.AssertNotError(t, err, "should have succeeded") // Revoking an already-revoked serial should fail. _, err = ra.RevokeCertByApplicant(context.Background(), &rapb.RevokeCertByApplicantRequest{ Cert: cert.Raw, Code: ocsp.Unspecified, RegID: 1, }) test.AssertError(t, err, "should have failed with bad reasonCode") test.AssertContains(t, err.Error(), "already revoked") } // mockSARevocationWithAuthzs embeds a mockSARevocation and so inherits all its // methods, but also adds GetValidAuthorizations2 so that it can pretend to // either be authorized or not for all of the names in the to-be-revoked cert. type mockSARevocationWithAuthzs struct { *mockSARevocation authorized bool } func (msa *mockSARevocationWithAuthzs) GetValidAuthorizations2(ctx context.Context, req *sapb.GetValidAuthorizationsRequest, _ ...grpc.CallOption) (*sapb.Authorizations, error) { authzs := &sapb.Authorizations{} if !msa.authorized { return authzs, nil } for _, ident := range req.Identifiers { authzs.Authzs = append(authzs.Authzs, &corepb.Authorization{Identifier: ident}) } return authzs, nil } func TestRevokeCertByApplicant_Controller(t *testing.T) { _, _, ra, _, clk, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} ra.purger = &mockPurger{} // Use the same self-signed cert as both issuer and issuee for revocation. _, cert := test.ThrowAwayCert(t, clk) cert.IsCA = true ic, err := issuance.NewCertificate(cert) test.AssertNotError(t, err, "failed to create issuer cert") ra.issuersByNameID = map[issuance.NameID]*issuance.Certificate{ ic.NameID(): ic, } mockSA := newMockSARevocation(cert) // Revoking when the account doesn't have valid authzs for the name should fail. // We use RegID 2 here and below because the mockSARevocation believes regID 1 // is the original issuer. ra.SA = &mockSARevocationWithAuthzs{mockSA, false} _, err = ra.RevokeCertByApplicant(context.Background(), &rapb.RevokeCertByApplicantRequest{ Cert: cert.Raw, Code: ocsp.Unspecified, RegID: 2, }) test.AssertError(t, err, "should have failed with wrong RegID") test.AssertContains(t, err.Error(), "requester does not control all identifiers") // Revoking when the account does have valid authzs for the name should succeed, // but override the revocation reason to cessationOfOperation. ra.SA = &mockSARevocationWithAuthzs{mockSA, true} _, err = ra.RevokeCertByApplicant(context.Background(), &rapb.RevokeCertByApplicantRequest{ Cert: cert.Raw, Code: ocsp.Unspecified, RegID: 2, }) test.AssertNotError(t, err, "should have succeeded") test.AssertEquals(t, mockSA.revoked[core.SerialToString(cert.SerialNumber)].RevokedReason, int64(ocsp.CessationOfOperation)) } func TestRevokeCertByKey(t *testing.T) { _, _, ra, _, clk, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} ra.purger = &mockPurger{} // Use the same self-signed cert as both issuer and issuee for revocation. _, cert := test.ThrowAwayCert(t, clk) digest, err := core.KeyDigest(cert.PublicKey) test.AssertNotError(t, err, "core.KeyDigest failed") cert.IsCA = true ic, err := issuance.NewCertificate(cert) test.AssertNotError(t, err, "failed to create issuer cert") ra.issuersByNameID = map[issuance.NameID]*issuance.Certificate{ ic.NameID(): ic, } mockSA := newMockSARevocation(cert) ra.SA = mockSA // Revoking should work, but override the requested reason and block the key. _, err = ra.RevokeCertByKey(context.Background(), &rapb.RevokeCertByKeyRequest{ Cert: cert.Raw, }) test.AssertNotError(t, err, "should have succeeded") test.AssertEquals(t, len(mockSA.blocked), 1) test.Assert(t, bytes.Equal(digest[:], mockSA.blocked[0].KeyHash), "key hash mismatch") test.AssertEquals(t, mockSA.blocked[0].Source, "API") test.AssertEquals(t, len(mockSA.blocked[0].Comment), 0) test.AssertEquals(t, mockSA.revoked[core.SerialToString(cert.SerialNumber)].RevokedReason, int64(ocsp.KeyCompromise)) // Re-revoking should fail, because it is already revoked for keyCompromise. _, err = ra.RevokeCertByKey(context.Background(), &rapb.RevokeCertByKeyRequest{ Cert: cert.Raw, }) test.AssertError(t, err, "should have failed") // Reset and have the Subscriber revoke for a different reason. // Then re-revoking using the key should work. mockSA.revoked = make(map[string]*corepb.CertificateStatus) _, err = ra.RevokeCertByApplicant(context.Background(), &rapb.RevokeCertByApplicantRequest{ Cert: cert.Raw, Code: ocsp.Unspecified, RegID: 1, }) test.AssertNotError(t, err, "should have succeeded") _, err = ra.RevokeCertByKey(context.Background(), &rapb.RevokeCertByKeyRequest{ Cert: cert.Raw, }) test.AssertNotError(t, err, "should have succeeded") } func TestAdministrativelyRevokeCertificate(t *testing.T) { _, _, ra, _, clk, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} ra.purger = &mockPurger{} // Use the same self-signed cert as both issuer and issuee for revocation. serial, cert := test.ThrowAwayCert(t, clk) digest, err := core.KeyDigest(cert.PublicKey) test.AssertNotError(t, err, "core.KeyDigest failed") cert.IsCA = true ic, err := issuance.NewCertificate(cert) test.AssertNotError(t, err, "failed to create issuer cert") ra.issuersByNameID = map[issuance.NameID]*issuance.Certificate{ ic.NameID(): ic, } mockSA := newMockSARevocation(cert) ra.SA = mockSA // Revoking with an empty request should fail immediately. _, err = ra.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{}) test.AssertError(t, err, "AdministrativelyRevokeCertificate should have failed for nil request object") // Revoking with no serial should fail immediately. mockSA.reset() _, err = ra.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{ Code: ocsp.Unspecified, AdminName: "root", }) test.AssertError(t, err, "AdministrativelyRevokeCertificate should have failed with no cert or serial") // Revoking without an admin name should fail immediately. mockSA.reset() _, err = ra.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{ Serial: serial, Code: ocsp.Unspecified, AdminName: "", }) test.AssertError(t, err, "AdministrativelyRevokeCertificate should have failed with empty string for `AdminName`") // Revoking for a forbidden reason should fail immediately. mockSA.reset() _, err = ra.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{ Serial: serial, Code: ocsp.CertificateHold, AdminName: "root", }) test.AssertError(t, err, "AdministrativelyRevokeCertificate should have failed with forbidden revocation reason") // Revoking a cert for an unspecified reason should work but not block the key. mockSA.reset() _, err = ra.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{ Serial: serial, Code: ocsp.Unspecified, AdminName: "root", }) test.AssertNotError(t, err, "AdministrativelyRevokeCertificate failed") test.AssertEquals(t, len(mockSA.blocked), 0) // Revoking a serial for an unspecified reason should work but not block the key. mockSA.reset() _, err = ra.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{ Serial: serial, Code: ocsp.Unspecified, AdminName: "root", }) test.AssertNotError(t, err, "AdministrativelyRevokeCertificate failed") test.AssertEquals(t, len(mockSA.blocked), 0) // Duplicate administrative revocation of a serial for an unspecified reason // should succeed because the akamai cache purge succeeds. // Note that we *don't* call reset() here, so it recognizes the duplicate. _, err = ra.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{ Serial: serial, Code: ocsp.Unspecified, AdminName: "root", }) test.AssertNotError(t, err, "AdministrativelyRevokeCertificate failed") test.AssertEquals(t, len(mockSA.blocked), 0) // Duplicate administrative revocation of a serial for a *malformed* cert for // an unspecified reason should fail because we can't attempt an akamai cache // purge so the underlying AlreadyRevoked error gets propagated upwards. // Note that we *don't* call reset() here, so it recognizes the duplicate. _, err = ra.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{ Serial: serial, Code: ocsp.Unspecified, AdminName: "root", Malformed: true, }) test.AssertError(t, err, "Should be revoked") test.AssertContains(t, err.Error(), "already revoked") test.AssertEquals(t, len(mockSA.blocked), 0) // Revoking a cert for key compromise with skipBlockKey set should work but // not block the key. mockSA.reset() _, err = ra.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{ Serial: serial, Code: ocsp.KeyCompromise, AdminName: "root", SkipBlockKey: true, }) test.AssertNotError(t, err, "AdministrativelyRevokeCertificate failed") test.AssertEquals(t, len(mockSA.blocked), 0) // Revoking a cert for key compromise should work and block the key. mockSA.reset() _, err = ra.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{ Serial: serial, Code: ocsp.KeyCompromise, AdminName: "root", }) test.AssertNotError(t, err, "AdministrativelyRevokeCertificate failed") test.AssertEquals(t, len(mockSA.blocked), 1) test.Assert(t, bytes.Equal(digest[:], mockSA.blocked[0].KeyHash), "key hash mismatch") test.AssertEquals(t, mockSA.blocked[0].Source, "admin-revoker") test.AssertEquals(t, mockSA.blocked[0].Comment, "revoked by root") test.AssertEquals(t, mockSA.blocked[0].Added.AsTime(), clk.Now()) // Revoking a malformed cert for key compromise should fail because we don't // have the pubkey to block. mockSA.reset() _, err = ra.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{ Serial: core.SerialToString(cert.SerialNumber), Code: ocsp.KeyCompromise, AdminName: "root", Malformed: true, }) test.AssertError(t, err, "AdministrativelyRevokeCertificate should have failed with just serial for keyCompromise") } // An authority that returns an error from NewOrderAndAuthzs if the // "ReplacesSerial" field of the request is empty. type mockNewOrderMustBeReplacementAuthority struct { mockSAWithAuthzs } func (sa *mockNewOrderMustBeReplacementAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb.NewOrderAndAuthzsRequest, _ ...grpc.CallOption) (*corepb.Order, error) { if req.NewOrder.ReplacesSerial == "" { return nil, status.Error(codes.InvalidArgument, "NewOrder is not a replacement") } return &corepb.Order{ Id: 1, RegistrationID: req.NewOrder.RegistrationID, Expires: req.NewOrder.Expires, Status: string(core.StatusPending), Created: timestamppb.New(time.Now()), Identifiers: req.NewOrder.Identifiers, }, nil } func TestNewOrderReplacesSerialCarriesThroughToSA(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() exampleOrder := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, ReplacesSerial: "1234", } // Mock SA that returns an error from NewOrderAndAuthzs if the // "ReplacesSerial" field of the request is empty. ra.SA = &mockNewOrderMustBeReplacementAuthority{mockSAWithAuthzs{}} _, err := ra.NewOrder(ctx, exampleOrder) test.AssertNotError(t, err, "order with ReplacesSerial should have succeeded") } // newMockSAUnpauseAccount is a fake which includes all of the SA methods called // in the course of an account unpause. Its behavior can be customized by // providing the number of unpaused account identifiers to allow testing of // various scenarios. type mockSAUnpauseAccount struct { sapb.StorageAuthorityClient identsToUnpause int64 receivedRegID int64 } func (sa *mockSAUnpauseAccount) UnpauseAccount(_ context.Context, req *sapb.RegistrationID, _ ...grpc.CallOption) (*sapb.Count, error) { sa.receivedRegID = req.Id return &sapb.Count{Count: sa.identsToUnpause}, nil } // TestUnpauseAccount tests that the RA's UnpauseAccount method correctly passes // the requested RegID to the SA, and correctly passes the SA's count back to // the caller. func TestUnpauseAccount(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() mockSA := mockSAUnpauseAccount{identsToUnpause: 0} ra.SA = &mockSA res, err := ra.UnpauseAccount(context.Background(), &rapb.UnpauseAccountRequest{ RegistrationID: 1, }) test.AssertNotError(t, err, "Should have been able to unpause account") test.AssertEquals(t, res.Count, int64(0)) test.AssertEquals(t, mockSA.receivedRegID, int64(1)) mockSA.identsToUnpause = 50001 res, err = ra.UnpauseAccount(context.Background(), &rapb.UnpauseAccountRequest{ RegistrationID: 1, }) test.AssertNotError(t, err, "Should have been able to unpause account") test.AssertEquals(t, res.Count, int64(50001)) } func TestGetAuthorization(t *testing.T) { _, _, ra, _, _, cleanup := initAuthorities(t) defer cleanup() ra.SA = &mockSAWithAuthzs{ authzs: []*core.Authorization{ { ID: "1", Identifier: identifier.NewDNS("example.com"), Status: "valid", Challenges: []core.Challenge{ { Type: core.ChallengeTypeHTTP01, Status: core.StatusValid, }, }, }, }, } // With HTTP01 enabled, GetAuthorization should pass the mock challenge through. pa, err := policy.New( map[identifier.IdentifierType]bool{ identifier.TypeDNS: true, identifier.TypeIP: true, }, map[core.AcmeChallenge]bool{ core.ChallengeTypeHTTP01: true, core.ChallengeTypeDNS01: true, }, blog.NewMock()) test.AssertNotError(t, err, "Couldn't create PA") ra.PA = pa authz, err := ra.GetAuthorization(context.Background(), &rapb.GetAuthorizationRequest{Id: 1}) test.AssertNotError(t, err, "should not fail") test.AssertEquals(t, len(authz.Challenges), 1) test.AssertEquals(t, authz.Challenges[0].Type, string(core.ChallengeTypeHTTP01)) // With HTTP01 disabled, GetAuthorization should filter out the mock challenge. pa, err = policy.New( map[identifier.IdentifierType]bool{ identifier.TypeDNS: true, identifier.TypeIP: true, }, map[core.AcmeChallenge]bool{ core.ChallengeTypeDNS01: true, }, blog.NewMock()) test.AssertNotError(t, err, "Couldn't create PA") ra.PA = pa authz, err = ra.GetAuthorization(context.Background(), &rapb.GetAuthorizationRequest{Id: 1}) test.AssertNotError(t, err, "should not fail") test.AssertEquals(t, len(authz.Challenges), 0) } type NoUpdateSA struct { sapb.StorageAuthorityClient } func (sa *NoUpdateSA) UpdateRegistrationContact(_ context.Context, _ *sapb.UpdateRegistrationContactRequest, _ ...grpc.CallOption) (*corepb.Registration, error) { return nil, fmt.Errorf("UpdateRegistrationContact() is mocked to always error") } func (sa *NoUpdateSA) UpdateRegistrationKey(_ context.Context, _ *sapb.UpdateRegistrationKeyRequest, _ ...grpc.CallOption) (*corepb.Registration, error) { return nil, fmt.Errorf("UpdateRegistrationKey() is mocked to always error") } // mockSARecordingRegistration tests UpdateRegistrationContact and UpdateRegistrationKey. type mockSARecordingRegistration struct { sapb.StorageAuthorityClient providedRegistrationID int64 providedContacts []string providedJwk []byte } // UpdateRegistrationContact records the registration ID and updated contacts // (optional) provided. func (sa *mockSARecordingRegistration) UpdateRegistrationContact(ctx context.Context, req *sapb.UpdateRegistrationContactRequest, _ ...grpc.CallOption) (*corepb.Registration, error) { sa.providedRegistrationID = req.RegistrationID sa.providedContacts = req.Contacts return &corepb.Registration{ Id: req.RegistrationID, Contact: req.Contacts, }, nil } // UpdateRegistrationKey records the registration ID and updated key provided. func (sa *mockSARecordingRegistration) UpdateRegistrationKey(ctx context.Context, req *sapb.UpdateRegistrationKeyRequest, _ ...grpc.CallOption) (*corepb.Registration, error) { sa.providedRegistrationID = req.RegistrationID sa.providedJwk = req.Jwk return &corepb.Registration{ Id: req.RegistrationID, Key: req.Jwk, }, nil } // TestUpdateRegistrationContact tests that the RA's UpdateRegistrationContact // method correctly: requires a registration ID; validates the contact provided; // does not require a contact; passes the requested registration ID and contact // to the SA; passes the updated Registration back to the caller; and can return // an error. func TestUpdateRegistrationContact(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() expectRegID := int64(1) expectContacts := []string{"mailto:test@contoso.com"} mockSA := mockSARecordingRegistration{} ra.SA = &mockSA _, err := ra.UpdateRegistrationContact(context.Background(), &rapb.UpdateRegistrationContactRequest{}) test.AssertError(t, err, "should not have been able to update registration contact without a registration ID") test.AssertContains(t, err.Error(), "incomplete gRPC request message") _, err = ra.UpdateRegistrationContact(context.Background(), &rapb.UpdateRegistrationContactRequest{ RegistrationID: expectRegID, Contacts: []string{"tel:+44123"}, }) test.AssertError(t, err, "should not have been able to update registration contact to an invalid contact") test.AssertContains(t, err.Error(), "invalid contact") res, err := ra.UpdateRegistrationContact(context.Background(), &rapb.UpdateRegistrationContactRequest{ RegistrationID: expectRegID, }) test.AssertNotError(t, err, "should have been able to update registration with a blank contact") test.AssertEquals(t, res.Id, expectRegID) test.AssertEquals(t, mockSA.providedRegistrationID, expectRegID) test.AssertDeepEquals(t, res.Contact, []string(nil)) test.AssertDeepEquals(t, mockSA.providedContacts, []string(nil)) res, err = ra.UpdateRegistrationContact(context.Background(), &rapb.UpdateRegistrationContactRequest{ RegistrationID: expectRegID, Contacts: expectContacts, }) test.AssertNotError(t, err, "should have been able to update registration with a populated contact") test.AssertEquals(t, res.Id, expectRegID) test.AssertEquals(t, mockSA.providedRegistrationID, expectRegID) test.AssertDeepEquals(t, res.Contact, expectContacts) test.AssertDeepEquals(t, mockSA.providedContacts, expectContacts) // Switch to a mock SA that will always error if UpdateRegistrationContact() // is called. ra.SA = &NoUpdateSA{} _, err = ra.UpdateRegistrationContact(context.Background(), &rapb.UpdateRegistrationContactRequest{ RegistrationID: expectRegID, Contacts: expectContacts, }) test.AssertError(t, err, "should have received an error from the SA") test.AssertContains(t, err.Error(), "failed to update registration contact") test.AssertContains(t, err.Error(), "mocked to always error") } // TestUpdateRegistrationKey tests that the RA's UpdateRegistrationKey method // correctly requires a registration ID and key, passes them to the SA, and // passes the updated Registration back to the caller. func TestUpdateRegistrationKey(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() expectRegID := int64(1) expectJwk := AccountKeyJSONA mockSA := mockSARecordingRegistration{} ra.SA = &mockSA _, err := ra.UpdateRegistrationKey(context.Background(), &rapb.UpdateRegistrationKeyRequest{}) test.AssertError(t, err, "should not have been able to update registration key without a registration ID or key") test.AssertContains(t, err.Error(), "incomplete gRPC request message") _, err = ra.UpdateRegistrationKey(context.Background(), &rapb.UpdateRegistrationKeyRequest{RegistrationID: expectRegID}) test.AssertError(t, err, "should not have been able to update registration key without a key") test.AssertContains(t, err.Error(), "incomplete gRPC request message") _, err = ra.UpdateRegistrationKey(context.Background(), &rapb.UpdateRegistrationKeyRequest{Jwk: expectJwk}) test.AssertError(t, err, "should not have been able to update registration key without a registration ID") test.AssertContains(t, err.Error(), "incomplete gRPC request message") res, err := ra.UpdateRegistrationKey(context.Background(), &rapb.UpdateRegistrationKeyRequest{ RegistrationID: expectRegID, Jwk: expectJwk, }) test.AssertNotError(t, err, "should have been able to update registration key") test.AssertEquals(t, res.Id, expectRegID) test.AssertEquals(t, mockSA.providedRegistrationID, expectRegID) test.AssertDeepEquals(t, res.Key, expectJwk) test.AssertDeepEquals(t, mockSA.providedJwk, expectJwk) // Switch to a mock SA that will always error if UpdateRegistrationKey() is // called. ra.SA = &NoUpdateSA{} _, err = ra.UpdateRegistrationKey(context.Background(), &rapb.UpdateRegistrationKeyRequest{ RegistrationID: expectRegID, Jwk: expectJwk, }) test.AssertError(t, err, "should have received an error from the SA") test.AssertContains(t, err.Error(), "failed to update registration key") test.AssertContains(t, err.Error(), "mocked to always error") } func TestCRLShard(t *testing.T) { var cdp []string n, err := crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) if err != nil || n != 0 { t.Errorf("crlShard(%+v) = %d, %s, want 0, nil", cdp, n, err) } cdp = []string{ "https://example.com/123.crl", "https://example.net/123.crl", } n, err = crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) if err == nil { t.Errorf("crlShard(%+v) = %d, %s, want 0, some error", cdp, n, err) } cdp = []string{ "https://example.com/abc", } n, err = crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) if err == nil { t.Errorf("crlShard(%+v) = %d, %s, want 0, some error", cdp, n, err) } cdp = []string{ "example", } n, err = crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) if err == nil { t.Errorf("crlShard(%+v) = %d, %s, want 0, some error", cdp, n, err) } cdp = []string{ "https://example.com/abc/-77.crl", } n, err = crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) if err == nil { t.Errorf("crlShard(%+v) = %d, %s, want 0, some error", cdp, n, err) } cdp = []string{ "https://example.com/abc/123", } n, err = crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) if err != nil || n != 123 { t.Errorf("crlShard(%+v) = %d, %s, want 123, nil", cdp, n, err) } cdp = []string{ "https://example.com/abc/123.crl", } n, err = crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) if err != nil || n != 123 { t.Errorf("crlShard(%+v) = %d, %s, want 123, nil", cdp, n, err) } } type mockSAWithOverrides struct { sapb.StorageAuthorityClient inserted *sapb.AddRateLimitOverrideRequest } func (sa *mockSAWithOverrides) AddRateLimitOverride(ctx context.Context, req *sapb.AddRateLimitOverrideRequest, _ ...grpc.CallOption) (*sapb.AddRateLimitOverrideResponse, error) { sa.inserted = req return &sapb.AddRateLimitOverrideResponse{}, nil } func TestAddRateLimitOverride(t *testing.T) { _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() mockSA := mockSAWithOverrides{} ra.SA = &mockSA expectBucketKey := core.RandomString(10) ov := rapb.AddRateLimitOverrideRequest{ LimitEnum: 1, BucketKey: expectBucketKey, Comment: "insert", Period: durationpb.New(time.Hour), Count: 100, Burst: 100, } _, err := ra.AddRateLimitOverride(ctx, &ov) test.AssertNotError(t, err, "expected successful insert, got error") test.AssertEquals(t, mockSA.inserted.Override.LimitEnum, ov.LimitEnum) test.AssertEquals(t, mockSA.inserted.Override.BucketKey, expectBucketKey) test.AssertEquals(t, mockSA.inserted.Override.Comment, ov.Comment) test.AssertEquals(t, mockSA.inserted.Override.Period.AsDuration(), ov.Period.AsDuration()) test.AssertEquals(t, mockSA.inserted.Override.Count, ov.Count) test.AssertEquals(t, mockSA.inserted.Override.Burst, ov.Burst) }