diff --git a/sa/precertificates.go b/sa/precertificates.go deleted file mode 100644 index 24c8c9646..000000000 --- a/sa/precertificates.go +++ /dev/null @@ -1,161 +0,0 @@ -package sa - -import ( - "context" - "crypto/x509" - "fmt" - "time" - - "google.golang.org/protobuf/types/known/emptypb" - - "github.com/letsencrypt/boulder/core" - "github.com/letsencrypt/boulder/db" - berrors "github.com/letsencrypt/boulder/errors" - "github.com/letsencrypt/boulder/features" - sapb "github.com/letsencrypt/boulder/sa/proto" -) - -// AddSerial writes a record of a serial number generation to the DB. -func (ssa *SQLStorageAuthority) AddSerial(ctx context.Context, req *sapb.AddSerialRequest) (*emptypb.Empty, error) { - if req.Serial == "" || req.RegID == 0 || req.Created == 0 || req.Expires == 0 { - return nil, errIncompleteRequest - } - err := ssa.dbMap.WithContext(ctx).Insert(&recordedSerialModel{ - Serial: req.Serial, - RegistrationID: req.RegID, - Created: time.Unix(0, req.Created), - Expires: time.Unix(0, req.Expires), - }) - if err != nil { - return nil, err - } - return &emptypb.Empty{}, nil -} - -// GetSerialMetadata returns metadata stored alongside the serial number, -// such as the RegID whose certificate request created that serial, and when -// the certificate with that serial will expire. -func (ssa *SQLStorageAuthorityRO) GetSerialMetadata(ctx context.Context, req *sapb.Serial) (*sapb.SerialMetadata, error) { - if req == nil || req.Serial == "" { - return nil, errIncompleteRequest - } - - if !core.ValidSerial(req.Serial) { - return nil, fmt.Errorf("invalid serial %q", req.Serial) - } - - recordedSerial := recordedSerialModel{} - err := ssa.dbReadOnlyMap.WithContext(ctx).SelectOne( - &recordedSerial, - "SELECT * FROM serials WHERE serial = ?", - req.Serial, - ) - if err != nil { - if db.IsNoRows(err) { - return nil, berrors.NotFoundError("serial %q not found", req.Serial) - } - return nil, err - } - - return &sapb.SerialMetadata{ - Serial: recordedSerial.Serial, - RegistrationID: recordedSerial.RegistrationID, - Created: recordedSerial.Created.UnixNano(), - Expires: recordedSerial.Expires.UnixNano(), - }, nil -} - -func (ssa *SQLStorageAuthority) GetSerialMetadata(ctx context.Context, req *sapb.Serial) (*sapb.SerialMetadata, error) { - return ssa.SQLStorageAuthorityRO.GetSerialMetadata(ctx, req) -} - -// AddPrecertificate writes a record of a precertificate generation to the DB. -// Note: this is not idempotent: it does not protect against inserting the same -// certificate multiple times. Calling code needs to first insert the cert's -// serial into the Serials table to ensure uniqueness. -func (ssa *SQLStorageAuthority) AddPrecertificate(ctx context.Context, req *sapb.AddCertificateRequest) (*emptypb.Empty, error) { - if len(req.Der) == 0 || req.RegID == 0 || req.Issued == 0 || req.IssuerNameID == 0 { - return nil, errIncompleteRequest - } - parsed, err := x509.ParseCertificate(req.Der) - if err != nil { - return nil, err - } - serialHex := core.SerialToString(parsed.SerialNumber) - - preCertModel := &precertificateModel{ - Serial: serialHex, - RegistrationID: req.RegID, - DER: req.Der, - Issued: time.Unix(0, req.Issued), - Expires: parsed.NotAfter, - } - - _, overallError := db.WithTransaction(ctx, ssa.dbMap, func(txWithCtx db.Executor) (interface{}, error) { - // Select to see if precert exists - var row struct { - Count int64 - } - err := txWithCtx.SelectOne(&row, "SELECT COUNT(*) as count FROM precertificates WHERE serial=?", serialHex) - if err != nil { - return nil, err - } - if row.Count > 0 { - return nil, berrors.DuplicateError("cannot add a duplicate cert") - } - - err = txWithCtx.Insert(preCertModel) - if err != nil { - return nil, err - } - - cs := &core.CertificateStatus{ - Serial: serialHex, - Status: core.OCSPStatusGood, - OCSPLastUpdated: ssa.clk.Now(), - RevokedDate: time.Time{}, - RevokedReason: 0, - LastExpirationNagSent: time.Time{}, - NotAfter: parsed.NotAfter, - IsExpired: false, - IssuerNameID: req.IssuerNameID, - } - if !features.Enabled(features.ROCSPStage6) { - cs.OCSPResponse = req.Ocsp - } - err = ssa.dbMap.WithContext(ctx).Insert(cs) - if err != nil { - return nil, err - } - - // NOTE(@cpu): When we collect up names to check if an FQDN set exists (e.g. - // that it is a renewal) we use just the DNSNames from the certificate and - // ignore the Subject Common Name (if any). This is a safe assumption because - // if a certificate we issued were to have a Subj. CN not present as a SAN it - // would be a misissuance and miscalculating whether the cert is a renewal or - // not for the purpose of rate limiting is the least of our troubles. - isRenewal, err := ssa.checkFQDNSetExists( - txWithCtx.SelectOne, - parsed.DNSNames) - if err != nil { - return nil, err - } - - err = addIssuedNames(txWithCtx, parsed, isRenewal) - if err != nil { - return nil, err - } - - err = addKeyHash(txWithCtx, parsed) - if err != nil { - return nil, err - } - - return nil, nil - }) - if overallError != nil { - return nil, overallError - } - - return &emptypb.Empty{}, nil -} diff --git a/sa/precertificates_test.go b/sa/precertificates_test.go deleted file mode 100644 index f7f95a253..000000000 --- a/sa/precertificates_test.go +++ /dev/null @@ -1,241 +0,0 @@ -package sa - -import ( - "bytes" - "context" - "crypto/sha256" - "fmt" - "testing" - "time" - - "github.com/letsencrypt/boulder/db" - berrors "github.com/letsencrypt/boulder/errors" - sapb "github.com/letsencrypt/boulder/sa/proto" - "github.com/letsencrypt/boulder/test" -) - -// findIssuedName is a small helper test function to directly query the -// issuedNames table for a given name to find a serial (or return an err). -func findIssuedName(dbMap db.OneSelector, name string) (string, error) { - var issuedNamesSerial string - err := dbMap.SelectOne( - &issuedNamesSerial, - `SELECT serial FROM issuedNames - WHERE reversedName = ? - ORDER BY notBefore DESC - LIMIT 1`, - ReverseName(name)) - return issuedNamesSerial, err -} - -func TestAddSerial(t *testing.T) { - sa, _, cleanUp := initSA(t) - defer cleanUp() - - reg := createWorkingRegistration(t, sa) - serial, testCert := test.ThrowAwayCert(t, 1) - - _, err := sa.AddSerial(context.Background(), &sapb.AddSerialRequest{ - RegID: reg.Id, - Created: testCert.NotBefore.UnixNano(), - Expires: testCert.NotAfter.UnixNano(), - }) - test.AssertError(t, err, "adding without serial should fail") - - _, err = sa.AddSerial(context.Background(), &sapb.AddSerialRequest{ - Serial: serial, - Created: testCert.NotBefore.UnixNano(), - Expires: testCert.NotAfter.UnixNano(), - }) - test.AssertError(t, err, "adding without regid should fail") - - _, err = sa.AddSerial(context.Background(), &sapb.AddSerialRequest{ - Serial: serial, - RegID: reg.Id, - Expires: testCert.NotAfter.UnixNano(), - }) - test.AssertError(t, err, "adding without created should fail") - - _, err = sa.AddSerial(context.Background(), &sapb.AddSerialRequest{ - Serial: serial, - RegID: reg.Id, - Created: testCert.NotBefore.UnixNano(), - }) - test.AssertError(t, err, "adding without expires should fail") - - _, err = sa.AddSerial(context.Background(), &sapb.AddSerialRequest{ - Serial: serial, - RegID: reg.Id, - Created: testCert.NotBefore.UnixNano(), - Expires: testCert.NotAfter.UnixNano(), - }) - test.AssertNotError(t, err, "adding serial should have succeeded") -} - -func TestGetSerialMetadata(t *testing.T) { - sa, clk, cleanUp := initSA(t) - defer cleanUp() - - reg := createWorkingRegistration(t, sa) - serial, _ := test.ThrowAwayCert(t, 1) - - _, err := sa.GetSerialMetadata(context.Background(), &sapb.Serial{Serial: serial}) - test.AssertError(t, err, "getting nonexistent serial should have failed") - - _, err = sa.AddSerial(context.Background(), &sapb.AddSerialRequest{ - Serial: serial, - RegID: reg.Id, - Created: clk.Now().UnixNano(), - Expires: clk.Now().Add(time.Hour).UnixNano(), - }) - test.AssertNotError(t, err, "failed to add test serial") - - m, err := sa.GetSerialMetadata(context.Background(), &sapb.Serial{Serial: serial}) - - test.AssertNotError(t, err, "getting serial should have succeeded") - test.AssertEquals(t, m.Serial, serial) - test.AssertEquals(t, m.RegistrationID, reg.Id) - test.AssertEquals(t, time.Unix(0, m.Created).UTC(), clk.Now()) - test.AssertEquals(t, time.Unix(0, m.Expires).UTC(), clk.Now().Add(time.Hour)) -} - -func TestAddPrecertificate(t *testing.T) { - sa, clk, cleanUp := initSA(t) - defer cleanUp() - - reg := createWorkingRegistration(t, sa) - - // Create a throw-away self signed certificate with a random name and - // serial number - serial, testCert := test.ThrowAwayCert(t, 1) - - // Add the cert as a precertificate - ocspResp := []byte{0, 0, 1} - regID := reg.Id - issuedTime := time.Date(2018, 4, 1, 7, 0, 0, 0, time.UTC) - _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ - Der: testCert.Raw, - RegID: regID, - Ocsp: ocspResp, - Issued: issuedTime.UnixNano(), - IssuerNameID: 1, - }) - test.AssertNotError(t, err, "Couldn't add test cert") - - // It should have the expected certificate status - certStatus, err := sa.GetCertificateStatus(ctx, &sapb.Serial{Serial: serial}) - test.AssertNotError(t, err, "Couldn't get status for test cert") - test.Assert( - t, - bytes.Equal(certStatus.OcspResponse, ocspResp), - fmt.Sprintf("OCSP responses don't match, expected: %x, got %x", certStatus.OcspResponse, ocspResp), - ) - test.AssertEquals(t, clk.Now().UnixNano(), certStatus.OcspLastUpdated) - - // It should show up in the issued names table - issuedNamesSerial, err := findIssuedName(sa.dbMap, testCert.DNSNames[0]) - test.AssertNotError(t, err, "expected no err querying issuedNames for precert") - test.AssertEquals(t, issuedNamesSerial, serial) - - // We should also be able to call AddCertificate with the same cert - // without it being an error. The duplicate err on inserting to - // issuedNames should be ignored. - _, err = sa.AddCertificate(ctx, &sapb.AddCertificateRequest{ - Der: testCert.Raw, - RegID: regID, - Issued: issuedTime.UnixNano(), - }) - test.AssertNotError(t, err, "unexpected err adding final cert after precert") -} - -func TestAddPrecertificateNoOCSP(t *testing.T) { - sa, _, cleanUp := initSA(t) - defer cleanUp() - - reg := createWorkingRegistration(t, sa) - _, testCert := test.ThrowAwayCert(t, 1) - - regID := reg.Id - issuedTime := time.Date(2018, 4, 1, 7, 0, 0, 0, time.UTC) - _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ - Der: testCert.Raw, - RegID: regID, - Issued: issuedTime.UnixNano(), - IssuerNameID: 1, - }) - test.AssertNotError(t, err, "Couldn't add test cert") -} - -func TestAddPreCertificateDuplicate(t *testing.T) { - sa, clk, cleanUp := initSA(t) - defer cleanUp() - - reg := createWorkingRegistration(t, sa) - - _, testCert := test.ThrowAwayCert(t, 1) - - _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ - Der: testCert.Raw, - Issued: clk.Now().UnixNano(), - RegID: reg.Id, - IssuerNameID: 1, - }) - test.AssertNotError(t, err, "Couldn't add test certificate") - - _, err = sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ - Der: testCert.Raw, - Issued: clk.Now().UnixNano(), - RegID: reg.Id, - IssuerNameID: 1, - }) - test.AssertDeepEquals(t, err, berrors.DuplicateError("cannot add a duplicate cert")) -} - -func TestAddPrecertificateIncomplete(t *testing.T) { - sa, _, cleanUp := initSA(t) - defer cleanUp() - - reg := createWorkingRegistration(t, sa) - - // Create a throw-away self signed certificate with a random name and - // serial number - _, testCert := test.ThrowAwayCert(t, 1) - - // Add the cert as a precertificate - ocspResp := []byte{0, 0, 1} - regID := reg.Id - _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ - Der: testCert.Raw, - RegID: regID, - Ocsp: ocspResp, - Issued: time.Date(2018, 4, 1, 7, 0, 0, 0, time.UTC).UnixNano(), - // Leaving out IssuerNameID - }) - - test.AssertError(t, err, "Adding precert with no issuer did not fail") -} - -func TestAddPrecertificateKeyHash(t *testing.T) { - sa, _, cleanUp := initSA(t) - defer cleanUp() - reg := createWorkingRegistration(t, sa) - - serial, testCert := test.ThrowAwayCert(t, 1) - _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ - Der: testCert.Raw, - RegID: reg.Id, - Ocsp: []byte{1, 2, 3}, - Issued: testCert.NotBefore.UnixNano(), - IssuerNameID: 1, - }) - test.AssertNotError(t, err, "failed to add precert") - - var keyHashes []keyHashModel - _, err = sa.dbMap.Select(&keyHashes, "SELECT * FROM keyHashToSerial") - test.AssertNotError(t, err, "failed to retrieve rows from keyHashToSerial") - test.AssertEquals(t, len(keyHashes), 1) - test.AssertEquals(t, keyHashes[0].CertSerial, serial) - test.AssertEquals(t, keyHashes[0].CertNotAfter, testCert.NotAfter) - spkiHash := sha256.Sum256(testCert.RawSubjectPublicKeyInfo) - test.Assert(t, bytes.Equal(keyHashes[0].KeyHash, spkiHash[:]), "spki hash mismatch") -} diff --git a/sa/sa.go b/sa/sa.go index 46fadec84..e4439b55a 100644 --- a/sa/sa.go +++ b/sa/sa.go @@ -159,6 +159,114 @@ func (ssa *SQLStorageAuthority) UpdateRegistration(ctx context.Context, req *cor return &emptypb.Empty{}, nil } +// AddSerial writes a record of a serial number generation to the DB. +func (ssa *SQLStorageAuthority) AddSerial(ctx context.Context, req *sapb.AddSerialRequest) (*emptypb.Empty, error) { + if req.Serial == "" || req.RegID == 0 || req.Created == 0 || req.Expires == 0 { + return nil, errIncompleteRequest + } + err := ssa.dbMap.WithContext(ctx).Insert(&recordedSerialModel{ + Serial: req.Serial, + RegistrationID: req.RegID, + Created: time.Unix(0, req.Created), + Expires: time.Unix(0, req.Expires), + }) + if err != nil { + return nil, err + } + return &emptypb.Empty{}, nil +} + +// AddPrecertificate writes a record of a precertificate generation to the DB. +// Note: this is not idempotent: it does not protect against inserting the same +// certificate multiple times. Calling code needs to first insert the cert's +// serial into the Serials table to ensure uniqueness. +func (ssa *SQLStorageAuthority) AddPrecertificate(ctx context.Context, req *sapb.AddCertificateRequest) (*emptypb.Empty, error) { + if len(req.Der) == 0 || req.RegID == 0 || req.Issued == 0 || req.IssuerNameID == 0 { + return nil, errIncompleteRequest + } + parsed, err := x509.ParseCertificate(req.Der) + if err != nil { + return nil, err + } + serialHex := core.SerialToString(parsed.SerialNumber) + + preCertModel := &precertificateModel{ + Serial: serialHex, + RegistrationID: req.RegID, + DER: req.Der, + Issued: time.Unix(0, req.Issued), + Expires: parsed.NotAfter, + } + + _, overallError := db.WithTransaction(ctx, ssa.dbMap, func(txWithCtx db.Executor) (interface{}, error) { + // Select to see if precert exists + var row struct { + Count int64 + } + err := txWithCtx.SelectOne(&row, "SELECT COUNT(*) as count FROM precertificates WHERE serial=?", serialHex) + if err != nil { + return nil, err + } + if row.Count > 0 { + return nil, berrors.DuplicateError("cannot add a duplicate cert") + } + + err = txWithCtx.Insert(preCertModel) + if err != nil { + return nil, err + } + + cs := &core.CertificateStatus{ + Serial: serialHex, + Status: core.OCSPStatusGood, + OCSPLastUpdated: ssa.clk.Now(), + RevokedDate: time.Time{}, + RevokedReason: 0, + LastExpirationNagSent: time.Time{}, + NotAfter: parsed.NotAfter, + IsExpired: false, + IssuerNameID: req.IssuerNameID, + } + if !features.Enabled(features.ROCSPStage6) { + cs.OCSPResponse = req.Ocsp + } + err = ssa.dbMap.WithContext(ctx).Insert(cs) + if err != nil { + return nil, err + } + + // NOTE(@cpu): When we collect up names to check if an FQDN set exists (e.g. + // that it is a renewal) we use just the DNSNames from the certificate and + // ignore the Subject Common Name (if any). This is a safe assumption because + // if a certificate we issued were to have a Subj. CN not present as a SAN it + // would be a misissuance and miscalculating whether the cert is a renewal or + // not for the purpose of rate limiting is the least of our troubles. + isRenewal, err := ssa.checkFQDNSetExists( + txWithCtx.SelectOne, + parsed.DNSNames) + if err != nil { + return nil, err + } + + err = addIssuedNames(txWithCtx, parsed, isRenewal) + if err != nil { + return nil, err + } + + err = addKeyHash(txWithCtx, parsed) + if err != nil { + return nil, err + } + + return nil, nil + }) + if overallError != nil { + return nil, overallError + } + + return &emptypb.Empty{}, nil +} + // AddCertificate stores an issued certificate, returning an error if it is a // duplicate or if any other failure occurs. func (ssa *SQLStorageAuthority) AddCertificate(ctx context.Context, req *sapb.AddCertificateRequest) (*emptypb.Empty, error) { diff --git a/sa/sa_test.go b/sa/sa_test.go index 1b8be2656..9d138c67b 100644 --- a/sa/sa_test.go +++ b/sa/sa_test.go @@ -5,6 +5,7 @@ import ( "context" "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" "database/sql" "encoding/base64" @@ -274,6 +275,232 @@ func TestReplicationLagRetries(t *testing.T) { test.AssertMetricWithLabelsEquals(t, sa.lagFactorCounter, prometheus.Labels{"method": "GetRegistration", "result": "notfound"}, 1) } +// findIssuedName is a small helper test function to directly query the +// issuedNames table for a given name to find a serial (or return an err). +func findIssuedName(dbMap db.OneSelector, name string) (string, error) { + var issuedNamesSerial string + err := dbMap.SelectOne( + &issuedNamesSerial, + `SELECT serial FROM issuedNames + WHERE reversedName = ? + ORDER BY notBefore DESC + LIMIT 1`, + ReverseName(name)) + return issuedNamesSerial, err +} + +func TestAddSerial(t *testing.T) { + sa, _, cleanUp := initSA(t) + defer cleanUp() + + reg := createWorkingRegistration(t, sa) + serial, testCert := test.ThrowAwayCert(t, 1) + + _, err := sa.AddSerial(context.Background(), &sapb.AddSerialRequest{ + RegID: reg.Id, + Created: testCert.NotBefore.UnixNano(), + Expires: testCert.NotAfter.UnixNano(), + }) + test.AssertError(t, err, "adding without serial should fail") + + _, err = sa.AddSerial(context.Background(), &sapb.AddSerialRequest{ + Serial: serial, + Created: testCert.NotBefore.UnixNano(), + Expires: testCert.NotAfter.UnixNano(), + }) + test.AssertError(t, err, "adding without regid should fail") + + _, err = sa.AddSerial(context.Background(), &sapb.AddSerialRequest{ + Serial: serial, + RegID: reg.Id, + Expires: testCert.NotAfter.UnixNano(), + }) + test.AssertError(t, err, "adding without created should fail") + + _, err = sa.AddSerial(context.Background(), &sapb.AddSerialRequest{ + Serial: serial, + RegID: reg.Id, + Created: testCert.NotBefore.UnixNano(), + }) + test.AssertError(t, err, "adding without expires should fail") + + _, err = sa.AddSerial(context.Background(), &sapb.AddSerialRequest{ + Serial: serial, + RegID: reg.Id, + Created: testCert.NotBefore.UnixNano(), + Expires: testCert.NotAfter.UnixNano(), + }) + test.AssertNotError(t, err, "adding serial should have succeeded") +} + +func TestGetSerialMetadata(t *testing.T) { + sa, clk, cleanUp := initSA(t) + defer cleanUp() + + reg := createWorkingRegistration(t, sa) + serial, _ := test.ThrowAwayCert(t, 1) + + _, err := sa.GetSerialMetadata(context.Background(), &sapb.Serial{Serial: serial}) + test.AssertError(t, err, "getting nonexistent serial should have failed") + + _, err = sa.AddSerial(context.Background(), &sapb.AddSerialRequest{ + Serial: serial, + RegID: reg.Id, + Created: clk.Now().UnixNano(), + Expires: clk.Now().Add(time.Hour).UnixNano(), + }) + test.AssertNotError(t, err, "failed to add test serial") + + m, err := sa.GetSerialMetadata(context.Background(), &sapb.Serial{Serial: serial}) + + test.AssertNotError(t, err, "getting serial should have succeeded") + test.AssertEquals(t, m.Serial, serial) + test.AssertEquals(t, m.RegistrationID, reg.Id) + test.AssertEquals(t, time.Unix(0, m.Created).UTC(), clk.Now()) + test.AssertEquals(t, time.Unix(0, m.Expires).UTC(), clk.Now().Add(time.Hour)) +} + +func TestAddPrecertificate(t *testing.T) { + sa, clk, cleanUp := initSA(t) + defer cleanUp() + + reg := createWorkingRegistration(t, sa) + + // Create a throw-away self signed certificate with a random name and + // serial number + serial, testCert := test.ThrowAwayCert(t, 1) + + // Add the cert as a precertificate + ocspResp := []byte{0, 0, 1} + regID := reg.Id + issuedTime := time.Date(2018, 4, 1, 7, 0, 0, 0, time.UTC) + _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ + Der: testCert.Raw, + RegID: regID, + Ocsp: ocspResp, + Issued: issuedTime.UnixNano(), + IssuerNameID: 1, + }) + test.AssertNotError(t, err, "Couldn't add test cert") + + // It should have the expected certificate status + certStatus, err := sa.GetCertificateStatus(ctx, &sapb.Serial{Serial: serial}) + test.AssertNotError(t, err, "Couldn't get status for test cert") + test.Assert( + t, + bytes.Equal(certStatus.OcspResponse, ocspResp), + fmt.Sprintf("OCSP responses don't match, expected: %x, got %x", certStatus.OcspResponse, ocspResp), + ) + test.AssertEquals(t, clk.Now().UnixNano(), certStatus.OcspLastUpdated) + + // It should show up in the issued names table + issuedNamesSerial, err := findIssuedName(sa.dbMap, testCert.DNSNames[0]) + test.AssertNotError(t, err, "expected no err querying issuedNames for precert") + test.AssertEquals(t, issuedNamesSerial, serial) + + // We should also be able to call AddCertificate with the same cert + // without it being an error. The duplicate err on inserting to + // issuedNames should be ignored. + _, err = sa.AddCertificate(ctx, &sapb.AddCertificateRequest{ + Der: testCert.Raw, + RegID: regID, + Issued: issuedTime.UnixNano(), + }) + test.AssertNotError(t, err, "unexpected err adding final cert after precert") +} + +func TestAddPrecertificateNoOCSP(t *testing.T) { + sa, _, cleanUp := initSA(t) + defer cleanUp() + + reg := createWorkingRegistration(t, sa) + _, testCert := test.ThrowAwayCert(t, 1) + + regID := reg.Id + issuedTime := time.Date(2018, 4, 1, 7, 0, 0, 0, time.UTC) + _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ + Der: testCert.Raw, + RegID: regID, + Issued: issuedTime.UnixNano(), + IssuerNameID: 1, + }) + test.AssertNotError(t, err, "Couldn't add test cert") +} + +func TestAddPreCertificateDuplicate(t *testing.T) { + sa, clk, cleanUp := initSA(t) + defer cleanUp() + + reg := createWorkingRegistration(t, sa) + + _, testCert := test.ThrowAwayCert(t, 1) + + _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ + Der: testCert.Raw, + Issued: clk.Now().UnixNano(), + RegID: reg.Id, + IssuerNameID: 1, + }) + test.AssertNotError(t, err, "Couldn't add test certificate") + + _, err = sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ + Der: testCert.Raw, + Issued: clk.Now().UnixNano(), + RegID: reg.Id, + IssuerNameID: 1, + }) + test.AssertDeepEquals(t, err, berrors.DuplicateError("cannot add a duplicate cert")) +} + +func TestAddPrecertificateIncomplete(t *testing.T) { + sa, _, cleanUp := initSA(t) + defer cleanUp() + + reg := createWorkingRegistration(t, sa) + + // Create a throw-away self signed certificate with a random name and + // serial number + _, testCert := test.ThrowAwayCert(t, 1) + + // Add the cert as a precertificate + ocspResp := []byte{0, 0, 1} + regID := reg.Id + _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ + Der: testCert.Raw, + RegID: regID, + Ocsp: ocspResp, + Issued: time.Date(2018, 4, 1, 7, 0, 0, 0, time.UTC).UnixNano(), + // Leaving out IssuerNameID + }) + + test.AssertError(t, err, "Adding precert with no issuer did not fail") +} + +func TestAddPrecertificateKeyHash(t *testing.T) { + sa, _, cleanUp := initSA(t) + defer cleanUp() + reg := createWorkingRegistration(t, sa) + + serial, testCert := test.ThrowAwayCert(t, 1) + _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ + Der: testCert.Raw, + RegID: reg.Id, + Ocsp: []byte{1, 2, 3}, + Issued: testCert.NotBefore.UnixNano(), + IssuerNameID: 1, + }) + test.AssertNotError(t, err, "failed to add precert") + + var keyHashes []keyHashModel + _, err = sa.dbMap.Select(&keyHashes, "SELECT * FROM keyHashToSerial") + test.AssertNotError(t, err, "failed to retrieve rows from keyHashToSerial") + test.AssertEquals(t, len(keyHashes), 1) + test.AssertEquals(t, keyHashes[0].CertSerial, serial) + test.AssertEquals(t, keyHashes[0].CertNotAfter, testCert.NotAfter) + spkiHash := sha256.Sum256(testCert.RawSubjectPublicKeyInfo) + test.Assert(t, bytes.Equal(keyHashes[0].KeyHash, spkiHash[:]), "spki hash mismatch") +} + func TestAddCertificate(t *testing.T) { sa, clk, cleanUp := initSA(t) defer cleanUp() diff --git a/sa/saro.go b/sa/saro.go index 6783f68cc..3817e3217 100644 --- a/sa/saro.go +++ b/sa/saro.go @@ -375,6 +375,43 @@ func ReverseName(domain string) string { return strings.Join(labels, ".") } +// GetSerialMetadata returns metadata stored alongside the serial number, +// such as the RegID whose certificate request created that serial, and when +// the certificate with that serial will expire. +func (ssa *SQLStorageAuthorityRO) GetSerialMetadata(ctx context.Context, req *sapb.Serial) (*sapb.SerialMetadata, error) { + if req == nil || req.Serial == "" { + return nil, errIncompleteRequest + } + + if !core.ValidSerial(req.Serial) { + return nil, fmt.Errorf("invalid serial %q", req.Serial) + } + + recordedSerial := recordedSerialModel{} + err := ssa.dbReadOnlyMap.WithContext(ctx).SelectOne( + &recordedSerial, + "SELECT * FROM serials WHERE serial = ?", + req.Serial, + ) + if err != nil { + if db.IsNoRows(err) { + return nil, berrors.NotFoundError("serial %q not found", req.Serial) + } + return nil, err + } + + return &sapb.SerialMetadata{ + Serial: recordedSerial.Serial, + RegistrationID: recordedSerial.RegistrationID, + Created: recordedSerial.Created.UnixNano(), + Expires: recordedSerial.Expires.UnixNano(), + }, nil +} + +func (ssa *SQLStorageAuthority) GetSerialMetadata(ctx context.Context, req *sapb.Serial) (*sapb.SerialMetadata, error) { + return ssa.SQLStorageAuthorityRO.GetSerialMetadata(ctx, req) +} + // GetCertificate takes a serial number and returns the corresponding // certificate, or error if it does not exist. func (ssa *SQLStorageAuthorityRO) GetCertificate(ctx context.Context, req *sapb.Serial) (*corepb.Certificate, error) {