WFE2: allow revocation of precertificates. (#4433)

When the `features.PrecertificateRevocation` feature flag is enabled the WFE2
will allow revoking certificates for a submitted precertificate. The legacy WFE1
behaviour remains unchanged (as before (pre)certificates issued through the V1
API will be revocable with the V2 API).

Previously the WFE2 vetted the certificate from the revocation request by
looking up a final certificate by the serial number in the requested
certificate, and then doing a byte for byte comparison between the stored and
requested certificate.

Rather than adjust this logic to handle looking up and comparing stored
precertificates against requested precertificates (requiring new RPCs and an
additional round-trip) we choose to instead check the signature on the requested
certificate or precertificate and consider it valid for revocation if the
signature validates with one of the WFE2's known issuers. We trust the integrity
of our own signatures.

An integration test that performs a revocation of a precertificate (in this case
one that never had a final certificate issued due to SCT embedded errors) with
all of the available authentication mechanisms is included.

Resolves https://github.com/letsencrypt/boulder/issues/4414
This commit is contained in:
Daniel McCarney 2019-09-16 16:40:07 -04:00 committed by GitHub
parent 76afea15d8
commit 1cd9733c24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 707 additions and 231 deletions

View File

@ -94,19 +94,19 @@ type config struct {
// loadCertificateFile loads a PEM certificate from the certFile provided. It // loadCertificateFile loads a PEM certificate from the certFile provided. It
// validates that the PEM is well-formed with no leftover bytes, and contains // validates that the PEM is well-formed with no leftover bytes, and contains
// only a well-formed X509 certificate. If the cert file meets these // only a well-formed X509 certificate. If the cert file meets these
// requirements the PEM bytes from the file are returned, otherwise an error is // requirements the PEM bytes from the file are returned along with the parsed
// returned. If the PEM contents of a certFile do not have a trailing newline // certificate, otherwise an error is returned. If the PEM contents of
// one is added. // a certFile do not have a trailing newline one is added.
func loadCertificateFile(aiaIssuerURL, certFile string) ([]byte, error) { func loadCertificateFile(aiaIssuerURL, certFile string) ([]byte, *x509.Certificate, error) {
pemBytes, err := ioutil.ReadFile(certFile) pemBytes, err := ioutil.ReadFile(certFile)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has an "+ "CertificateChain entry for AIA issuer url %q has an "+
"invalid chain file: %q - error reading contents: %s", "invalid chain file: %q - error reading contents: %s",
aiaIssuerURL, certFile, err) aiaIssuerURL, certFile, err)
} }
if bytes.Contains(pemBytes, []byte("\r\n")) { if bytes.Contains(pemBytes, []byte("\r\n")) {
return nil, fmt.Errorf( return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has an "+ "CertificateChain entry for AIA issuer url %q has an "+
"invalid chain file: %q - contents had CRLF line endings", "invalid chain file: %q - contents had CRLF line endings",
aiaIssuerURL, certFile) aiaIssuerURL, certFile)
@ -114,22 +114,23 @@ func loadCertificateFile(aiaIssuerURL, certFile string) ([]byte, error) {
// Try to decode the contents as PEM // Try to decode the contents as PEM
certBlock, rest := pem.Decode(pemBytes) certBlock, rest := pem.Decode(pemBytes)
if certBlock == nil { if certBlock == nil {
return nil, fmt.Errorf( return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has an "+ "CertificateChain entry for AIA issuer url %q has an "+
"invalid chain file: %q - contents did not decode as PEM", "invalid chain file: %q - contents did not decode as PEM",
aiaIssuerURL, certFile) aiaIssuerURL, certFile)
} }
// The PEM contents must be a CERTIFICATE // The PEM contents must be a CERTIFICATE
if certBlock.Type != "CERTIFICATE" { if certBlock.Type != "CERTIFICATE" {
return nil, fmt.Errorf( return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has an "+ "CertificateChain entry for AIA issuer url %q has an "+
"invalid chain file: %q - PEM block type incorrect, found "+ "invalid chain file: %q - PEM block type incorrect, found "+
"%q, expected \"CERTIFICATE\"", "%q, expected \"CERTIFICATE\"",
aiaIssuerURL, certFile, certBlock.Type) aiaIssuerURL, certFile, certBlock.Type)
} }
// The PEM Certificate must successfully parse // The PEM Certificate must successfully parse
if _, err := x509.ParseCertificate(certBlock.Bytes); err != nil { var cert *x509.Certificate
return nil, fmt.Errorf( if cert, err = x509.ParseCertificate(certBlock.Bytes); err != nil {
return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has an "+ "CertificateChain entry for AIA issuer url %q has an "+
"invalid chain file: %q - certificate bytes failed to parse: %s", "invalid chain file: %q - certificate bytes failed to parse: %s",
aiaIssuerURL, certFile, err) aiaIssuerURL, certFile, err)
@ -137,7 +138,7 @@ func loadCertificateFile(aiaIssuerURL, certFile string) ([]byte, error) {
// If there are bytes leftover we must reject the file otherwise these // If there are bytes leftover we must reject the file otherwise these
// leftover bytes will end up in a served certificate chain. // leftover bytes will end up in a served certificate chain.
if len(rest) != 0 { if len(rest) != 0 {
return nil, fmt.Errorf( return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has an "+ "CertificateChain entry for AIA issuer url %q has an "+
"invalid chain file: %q - PEM contents had unused remainder "+ "invalid chain file: %q - PEM contents had unused remainder "+
"input (%d bytes)", "input (%d bytes)",
@ -147,16 +148,19 @@ func loadCertificateFile(aiaIssuerURL, certFile string) ([]byte, error) {
if pemBytes[len(pemBytes)-1] != '\n' { if pemBytes[len(pemBytes)-1] != '\n' {
pemBytes = append(pemBytes, '\n') pemBytes = append(pemBytes, '\n')
} }
return pemBytes, nil return pemBytes, cert, nil
} }
// loadCertificateChains processes the provided chainConfig of AIA Issuer URLs // loadCertificateChains processes the provided chainConfig of AIA Issuer URLs
// and cert filenames. For each AIA issuer URL all of its cert filenames are // and cert filenames. For each AIA issuer URL all of its cert filenames are
// read, validated as PEM certificates, and concatenated together separated by // read, validated as PEM certificates, and concatenated together separated by
// newlines. The combined PEM certificate chain contents for each are returned // newlines. The combined PEM certificate chain contents for each are returned
// in the results map, keyed by the AIA Issuer URL. // in the results map, keyed by the AIA Issuer URL. Additionally the first
func loadCertificateChains(chainConfig map[string][]string) (map[string][]byte, error) { // certificate in each chain is parsed and returned in a slice of issuer
// certificates.
func loadCertificateChains(chainConfig map[string][]string) (map[string][]byte, []*x509.Certificate, error) {
results := make(map[string][]byte, len(chainConfig)) results := make(map[string][]byte, len(chainConfig))
var issuerCerts []*x509.Certificate
// For each AIA Issuer URL we need to read the chain cert files // For each AIA Issuer URL we need to read the chain cert files
for aiaIssuerURL, certFiles := range chainConfig { for aiaIssuerURL, certFiles := range chainConfig {
@ -164,7 +168,7 @@ func loadCertificateChains(chainConfig map[string][]string) (map[string][]byte,
// There must be at least one chain file specified // There must be at least one chain file specified
if len(certFiles) == 0 { if len(certFiles) == 0 {
return nil, fmt.Errorf( return nil, nil, fmt.Errorf(
"CertificateChain entry for AIA issuer url %q has no chain "+ "CertificateChain entry for AIA issuer url %q has no chain "+
"file names configured", "file names configured",
aiaIssuerURL) aiaIssuerURL)
@ -172,14 +176,19 @@ func loadCertificateChains(chainConfig map[string][]string) (map[string][]byte,
// certFiles are read and appended in the order they appear in the // certFiles are read and appended in the order they appear in the
// configuration // configuration
for _, c := range certFiles { for i, c := range certFiles {
// Prepend a newline before each chain entry // Prepend a newline before each chain entry
buffer.Write([]byte("\n")) buffer.Write([]byte("\n"))
// Read and validate the chain file contents // Read and validate the chain file contents
pemBytes, err := loadCertificateFile(aiaIssuerURL, c) pemBytes, cert, err := loadCertificateFile(aiaIssuerURL, c)
if err != nil { if err != nil {
return nil, err return nil, nil, err
}
// Save the first certificate as a direct issuer certificate
if i == 0 {
issuerCerts = append(issuerCerts, cert)
} }
// Write the PEM bytes to the result buffer for this AIAIssuer // Write the PEM bytes to the result buffer for this AIAIssuer
@ -189,7 +198,8 @@ func loadCertificateChains(chainConfig map[string][]string) (map[string][]byte,
// Save the full PEM chain contents // Save the full PEM chain contents
results[aiaIssuerURL] = buffer.Bytes() results[aiaIssuerURL] = buffer.Bytes()
} }
return results, nil
return results, issuerCerts, nil
} }
func setupWFE(c config, logger blog.Logger, stats metrics.Scope, clk clock.Clock) (core.RegistrationAuthority, core.StorageAuthority, noncepb.NonceServiceClient, map[string]noncepb.NonceServiceClient) { func setupWFE(c config, logger blog.Logger, stats metrics.Scope, clk clock.Clock) (core.RegistrationAuthority, core.StorageAuthority, noncepb.NonceServiceClient, map[string]noncepb.NonceServiceClient) {
@ -232,7 +242,7 @@ func main() {
err := cmd.ReadConfigFile(*configFile, &c) err := cmd.ReadConfigFile(*configFile, &c)
cmd.FailOnError(err, "Reading JSON config file into config structure") cmd.FailOnError(err, "Reading JSON config file into config structure")
certChains, err := loadCertificateChains(c.WFE.CertificateChains) certChains, issuerCerts, err := loadCertificateChains(c.WFE.CertificateChains)
cmd.FailOnError(err, "Couldn't read configured CertificateChains") cmd.FailOnError(err, "Couldn't read configured CertificateChains")
err = features.Set(c.WFE.Features) err = features.Set(c.WFE.Features)
@ -248,7 +258,7 @@ func main() {
kp, err := goodkey.NewKeyPolicy("", c.WFE.BlockedKeyFile) kp, err := goodkey.NewKeyPolicy("", c.WFE.BlockedKeyFile)
cmd.FailOnError(err, "Unable to create key policy") cmd.FailOnError(err, "Unable to create key policy")
rac, sac, rns, npm := setupWFE(c, logger, scope, clk) rac, sac, rns, npm := setupWFE(c, logger, scope, clk)
wfe, err := wfe2.NewWebFrontEndImpl(scope, clk, kp, certChains, rns, npm, logger) wfe, err := wfe2.NewWebFrontEndImpl(scope, clk, kp, certChains, issuerCerts, rns, npm, logger)
cmd.FailOnError(err, "Unable to create WFE") cmd.FailOnError(err, "Unable to create WFE")
wfe.RA = rac wfe.RA = rac
wfe.SA = sac wfe.SA = sac

View File

@ -43,23 +43,20 @@ func TestLoadCertificateChains(t *testing.T) {
test.AssertNotError(t, err, "ioutil.WriteFile failed") test.AssertNotError(t, err, "ioutil.WriteFile failed")
testCases := []struct { testCases := []struct {
Name string Name string
Input map[string][]string Input map[string][]string
ExpectedResult map[string][]byte ExpectedMap map[string][]byte
ExpectedError error ExpectedError error
}{ }{
{ {
Name: "No input", Name: "No input",
Input: nil, Input: nil,
ExpectedResult: nil,
ExpectedError: nil,
}, },
{ {
Name: "AIA Issuer without chain files", Name: "AIA Issuer without chain files",
Input: map[string][]string{ Input: map[string][]string{
"http://break.the.chain.com": []string{}, "http://break.the.chain.com": []string{},
}, },
ExpectedResult: nil,
ExpectedError: fmt.Errorf( ExpectedError: fmt.Errorf(
"CertificateChain entry for AIA issuer url \"http://break.the.chain.com\" " + "CertificateChain entry for AIA issuer url \"http://break.the.chain.com\" " +
"has no chain file names configured"), "has no chain file names configured"),
@ -69,7 +66,6 @@ func TestLoadCertificateChains(t *testing.T) {
Input: map[string][]string{ Input: map[string][]string{
"http://where.is.my.mind": []string{"/tmp/does.not.exist.pem"}, "http://where.is.my.mind": []string{"/tmp/does.not.exist.pem"},
}, },
ExpectedResult: nil,
ExpectedError: fmt.Errorf("CertificateChain entry for AIA issuer url \"http://where.is.my.mind\" " + ExpectedError: fmt.Errorf("CertificateChain entry for AIA issuer url \"http://where.is.my.mind\" " +
"has an invalid chain file: \"/tmp/does.not.exist.pem\" - error reading " + "has an invalid chain file: \"/tmp/does.not.exist.pem\" - error reading " +
"contents: open /tmp/does.not.exist.pem: no such file or directory"), "contents: open /tmp/does.not.exist.pem: no such file or directory"),
@ -79,7 +75,6 @@ func TestLoadCertificateChains(t *testing.T) {
Input: map[string][]string{ Input: map[string][]string{
"http://windows.sad.zone": []string{crlfPEM.Name()}, "http://windows.sad.zone": []string{crlfPEM.Name()},
}, },
ExpectedResult: nil,
ExpectedError: fmt.Errorf("CertificateChain entry for AIA issuer url \"http://windows.sad.zone\" "+ ExpectedError: fmt.Errorf("CertificateChain entry for AIA issuer url \"http://windows.sad.zone\" "+
"has an invalid chain file: %q - contents had CRLF line endings", crlfPEM.Name()), "has an invalid chain file: %q - contents had CRLF line endings", crlfPEM.Name()),
}, },
@ -88,7 +83,6 @@ func TestLoadCertificateChains(t *testing.T) {
Input: map[string][]string{ Input: map[string][]string{
"http://ok.go": []string{invalidPEMFile.Name()}, "http://ok.go": []string{invalidPEMFile.Name()},
}, },
ExpectedResult: nil,
ExpectedError: fmt.Errorf( ExpectedError: fmt.Errorf(
"CertificateChain entry for AIA issuer url \"http://ok.go\" has an "+ "CertificateChain entry for AIA issuer url \"http://ok.go\" has an "+
"invalid chain file: %q - contents did not decode as PEM", "invalid chain file: %q - contents did not decode as PEM",
@ -99,7 +93,6 @@ func TestLoadCertificateChains(t *testing.T) {
Input: map[string][]string{ Input: map[string][]string{
"http://not-a-cert.com": []string{"../../test/test-root.key"}, "http://not-a-cert.com": []string{"../../test/test-root.key"},
}, },
ExpectedResult: nil,
ExpectedError: fmt.Errorf( ExpectedError: fmt.Errorf(
"CertificateChain entry for AIA issuer url \"http://not-a-cert.com\" has " + "CertificateChain entry for AIA issuer url \"http://not-a-cert.com\" has " +
"an invalid chain file: \"../../test/test-root.key\" - PEM block type " + "an invalid chain file: \"../../test/test-root.key\" - PEM block type " +
@ -110,7 +103,6 @@ func TestLoadCertificateChains(t *testing.T) {
Input: map[string][]string{ Input: map[string][]string{
"http://tasty.leftovers.com": []string{leftoverPEMFile.Name()}, "http://tasty.leftovers.com": []string{leftoverPEMFile.Name()},
}, },
ExpectedResult: nil,
ExpectedError: fmt.Errorf( ExpectedError: fmt.Errorf(
"CertificateChain entry for AIA issuer url \"http://tasty.leftovers.com\" "+ "CertificateChain entry for AIA issuer url \"http://tasty.leftovers.com\" "+
"has an invalid chain file: %q - PEM contents had unused remainder input "+ "has an invalid chain file: %q - PEM contents had unused remainder input "+
@ -124,38 +116,35 @@ func TestLoadCertificateChains(t *testing.T) {
Input: map[string][]string{ Input: map[string][]string{
"http://single-cert-chain.com": []string{"../../test/test-ca.pem"}, "http://single-cert-chain.com": []string{"../../test/test-ca.pem"},
}, },
ExpectedResult: map[string][]byte{ ExpectedMap: map[string][]byte{
"http://single-cert-chain.com": []byte(fmt.Sprintf("\n%s", string(certBytesA))), "http://single-cert-chain.com": []byte(fmt.Sprintf("\n%s", string(certBytesA))),
}, },
ExpectedError: nil,
}, },
{ {
Name: "Two PEM file chain", Name: "Two PEM file chain",
Input: map[string][]string{ Input: map[string][]string{
"http://two-cert-chain.com": []string{"../../test/test-ca.pem", "../../test/test-ca2.pem"}, "http://two-cert-chain.com": []string{"../../test/test-ca.pem", "../../test/test-ca2.pem"},
}, },
ExpectedResult: map[string][]byte{ ExpectedMap: map[string][]byte{
"http://two-cert-chain.com": []byte(fmt.Sprintf("\n%s\n%s", string(certBytesA), string(certBytesB))), "http://two-cert-chain.com": []byte(fmt.Sprintf("\n%s\n%s", string(certBytesA), string(certBytesB))),
}, },
ExpectedError: nil,
}, },
{ {
Name: "One PEM file chain, no trailing newline", Name: "One PEM file chain, no trailing newline",
Input: map[string][]string{ Input: map[string][]string{
"http://single-cert-chain.nonewline.com": []string{abruptPEM.Name()}, "http://single-cert-chain.nonewline.com": []string{abruptPEM.Name()},
}, },
ExpectedResult: map[string][]byte{ ExpectedMap: map[string][]byte{
// NOTE(@cpu): There should be a trailing \n added by the WFE that we // NOTE(@cpu): There should be a trailing \n added by the WFE that we
// expect in the format specifier below. // expect in the format specifier below.
"http://single-cert-chain.nonewline.com": []byte(fmt.Sprintf("\n%s\n", string(abruptPEMBytes))), "http://single-cert-chain.nonewline.com": []byte(fmt.Sprintf("\n%s\n", string(abruptPEMBytes))),
}, },
ExpectedError: nil,
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) { t.Run(tc.Name, func(t *testing.T) {
result, err := loadCertificateChains(tc.Input) resultMap, issuers, err := loadCertificateChains(tc.Input)
if tc.ExpectedError == nil && err != nil { if tc.ExpectedError == nil && err != nil {
t.Errorf("Expected nil error, got %#v\n", err) t.Errorf("Expected nil error, got %#v\n", err)
} else if tc.ExpectedError != nil && err == nil { } else if tc.ExpectedError != nil && err == nil {
@ -163,9 +152,10 @@ func TestLoadCertificateChains(t *testing.T) {
} else if tc.ExpectedError != nil { } else if tc.ExpectedError != nil {
test.AssertEquals(t, err.Error(), tc.ExpectedError.Error()) test.AssertEquals(t, err.Error(), tc.ExpectedError.Error())
} }
test.AssertEquals(t, len(result), len(tc.ExpectedResult)) test.AssertEquals(t, len(resultMap), len(tc.ExpectedMap))
for url, chain := range result { test.AssertEquals(t, len(issuers), len(tc.ExpectedMap))
test.Assert(t, bytes.Compare(chain, tc.ExpectedResult[url]) == 0, "Chain bytes did not match expected") for url, chain := range resultMap {
test.Assert(t, bytes.Compare(chain, tc.ExpectedMap[url]) == 0, "Chain bytes did not match expected")
} }
}) })
} }

View File

@ -118,6 +118,7 @@ type StorageGetter interface {
GetValidAuthorizations(ctx context.Context, regID int64, domains []string, now time.Time) (map[string]*Authorization, error) GetValidAuthorizations(ctx context.Context, regID int64, domains []string, now time.Time) (map[string]*Authorization, error)
GetPendingAuthorization(ctx context.Context, req *sapb.GetPendingAuthorizationRequest) (*Authorization, error) GetPendingAuthorization(ctx context.Context, req *sapb.GetPendingAuthorizationRequest) (*Authorization, error)
GetCertificate(ctx context.Context, serial string) (Certificate, error) GetCertificate(ctx context.Context, serial string) (Certificate, error)
GetPrecertificate(ctx context.Context, req *sapb.Serial) (*corepb.Certificate, error)
GetCertificateStatus(ctx context.Context, serial string) (CertificateStatus, error) GetCertificateStatus(ctx context.Context, serial string) (CertificateStatus, error)
CountCertificatesByNames(ctx context.Context, domains []string, earliest, latest time.Time) (countByDomain []*sapb.CountByNames_MapElement, err error) CountCertificatesByNames(ctx context.Context, domains []string, earliest, latest time.Time) (countByDomain []*sapb.CountByNames_MapElement, err error)
CountRegistrationsByIP(ctx context.Context, ip net.IP, earliest, latest time.Time) (int, error) CountRegistrationsByIP(ctx context.Context, ip net.IP, earliest, latest time.Time) (int, error)

View File

@ -35,11 +35,12 @@ func _() {
_ = x[DeleteUnusedChallenges-24] _ = x[DeleteUnusedChallenges-24]
_ = x[V1DisableNewValidations-25] _ = x[V1DisableNewValidations-25]
_ = x[PrecertificateOCSP-26] _ = x[PrecertificateOCSP-26]
_ = x[PrecertificateRevocation-27]
} }
const _FeatureFlag_name = "unusedPerformValidationRPCACME13KeyRolloverSimplifiedVAHTTPTLSSNIRevalidationAllowRenewalFirstRLSetIssuedNamesRenewalBitFasterRateLimitProbeCTLogsRevokeAtRACAAValidationMethodsCAAAccountURIHeadNonceStatusOKNewAuthorizationSchemaDisableAuthz2OrdersEarlyOrderRateLimitEnforceMultiVAMultiVAFullResultsRemoveWFE2AccountIDCheckRenewalFirstMandatoryPOSTAsGETFasterGetOrderForNamesAllowV1RegistrationParallelCheckFailedValidationDeleteUnusedChallengesV1DisableNewValidationsPrecertificateOCSP" const _FeatureFlag_name = "unusedPerformValidationRPCACME13KeyRolloverSimplifiedVAHTTPTLSSNIRevalidationAllowRenewalFirstRLSetIssuedNamesRenewalBitFasterRateLimitProbeCTLogsRevokeAtRACAAValidationMethodsCAAAccountURIHeadNonceStatusOKNewAuthorizationSchemaDisableAuthz2OrdersEarlyOrderRateLimitEnforceMultiVAMultiVAFullResultsRemoveWFE2AccountIDCheckRenewalFirstMandatoryPOSTAsGETFasterGetOrderForNamesAllowV1RegistrationParallelCheckFailedValidationDeleteUnusedChallengesV1DisableNewValidationsPrecertificateOCSPPrecertificateRevocation"
var _FeatureFlag_index = [...]uint16{0, 6, 26, 43, 59, 77, 96, 120, 135, 146, 156, 176, 189, 206, 228, 247, 266, 280, 298, 317, 334, 352, 374, 393, 422, 444, 467, 485} var _FeatureFlag_index = [...]uint16{0, 6, 26, 43, 59, 77, 96, 120, 135, 146, 156, 176, 189, 206, 228, 247, 266, 280, 298, 317, 334, 352, 374, 393, 422, 444, 467, 485, 509}
func (i FeatureFlag) String() string { func (i FeatureFlag) String() string {
if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) { if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) {

View File

@ -72,6 +72,9 @@ const (
// generating a precertificate. This also changes the issuance / storage flow, // generating a precertificate. This also changes the issuance / storage flow,
// adding two new calls from CA to SA: AddSerial and AddPrecertificate. // adding two new calls from CA to SA: AddSerial and AddPrecertificate.
PrecertificateOCSP PrecertificateOCSP
// PrecertificateRevocation allows revocation of precertificates with the
// ACMEv2 interface.
PrecertificateRevocation
) )
// List of features and their default value, protected by fMu // List of features and their default value, protected by fMu
@ -103,6 +106,7 @@ var features = map[FeatureFlag]bool{
DeleteUnusedChallenges: false, DeleteUnusedChallenges: false,
V1DisableNewValidations: false, V1DisableNewValidations: false,
PrecertificateOCSP: false, PrecertificateOCSP: false,
PrecertificateRevocation: false,
} }
var fMu = new(sync.RWMutex) var fMu = new(sync.RWMutex)

View File

@ -56,7 +56,7 @@ func (cac CertificateAuthorityClientWrapper) IssueCertificateForPrecertificate(c
if err != nil { if err != nil {
return core.Certificate{}, err return core.Certificate{}, err
} }
return pbToCert(res) return PBToCert(res)
} }
func (cac CertificateAuthorityClientWrapper) GenerateOCSP(ctx context.Context, ocspReq core.OCSPSigningRequest) ([]byte, error) { func (cac CertificateAuthorityClientWrapper) GenerateOCSP(ctx context.Context, ocspReq core.OCSPSigningRequest) ([]byte, error) {
@ -113,7 +113,7 @@ func (cas *CertificateAuthorityServerWrapper) IssueCertificateForPrecertificate(
if err != nil { if err != nil {
return nil, err return nil, err
} }
return certToPB(cert), nil return CertToPB(cert), nil
} }
func (cas *CertificateAuthorityServerWrapper) GenerateOCSP(ctx context.Context, request *caPB.GenerateOCSPRequest) (*caPB.OCSPResponse, error) { func (cas *CertificateAuthorityServerWrapper) GenerateOCSP(ctx context.Context, request *caPB.GenerateOCSPRequest) (*caPB.OCSPResponse, error) {

View File

@ -426,7 +426,7 @@ func authorizationValid(authz *corepb.Authorization) bool {
return !(authz.Id == nil || authz.Identifier == nil || authz.RegistrationID == nil || authz.Status == nil || authz.Expires == nil) return !(authz.Id == nil || authz.Identifier == nil || authz.RegistrationID == nil || authz.Status == nil || authz.Expires == nil)
} }
func certToPB(cert core.Certificate) *corepb.Certificate { func CertToPB(cert core.Certificate) *corepb.Certificate {
issued, expires := cert.Issued.UnixNano(), cert.Expires.UnixNano() issued, expires := cert.Issued.UnixNano(), cert.Expires.UnixNano()
return &corepb.Certificate{ return &corepb.Certificate{
RegistrationID: &cert.RegistrationID, RegistrationID: &cert.RegistrationID,
@ -438,7 +438,7 @@ func certToPB(cert core.Certificate) *corepb.Certificate {
} }
} }
func pbToCert(pb *corepb.Certificate) (core.Certificate, error) { func PBToCert(pb *corepb.Certificate) (core.Certificate, error) {
if pb == nil || pb.RegistrationID == nil || pb.Serial == nil || pb.Digest == nil || pb.Der == nil || pb.Issued == nil || pb.Expires == nil { if pb == nil || pb.RegistrationID == nil || pb.Serial == nil || pb.Digest == nil || pb.Der == nil || pb.Issued == nil || pb.Expires == nil {
return core.Certificate{}, errIncompleteResponse return core.Certificate{}, errIncompleteResponse
} }

View File

@ -306,8 +306,8 @@ func TestCert(t *testing.T) {
Expires: now.Add(time.Hour), Expires: now.Add(time.Hour),
} }
certPB := certToPB(cert) certPB := CertToPB(cert)
outCert, _ := pbToCert(certPB) outCert, _ := PBToCert(certPB)
test.AssertDeepEquals(t, cert, outCert) test.AssertDeepEquals(t, cert, outCert)
} }

View File

@ -68,7 +68,7 @@ func (rac RegistrationAuthorityClientWrapper) NewCertificate(ctx context.Context
return core.Certificate{}, err return core.Certificate{}, err
} }
return pbToCert(response) return PBToCert(response)
} }
func (rac RegistrationAuthorityClientWrapper) UpdateRegistration(ctx context.Context, base, updates core.Registration) (core.Registration, error) { func (rac RegistrationAuthorityClientWrapper) UpdateRegistration(ctx context.Context, base, updates core.Registration) (core.Registration, error) {
@ -237,7 +237,7 @@ func (ras *RegistrationAuthorityServerWrapper) NewCertificate(ctx context.Contex
if err != nil { if err != nil {
return nil, err return nil, err
} }
return certToPB(cert), nil return CertToPB(cert), nil
} }
func (ras *RegistrationAuthorityServerWrapper) UpdateRegistration(ctx context.Context, request *rapb.UpdateRegistrationRequest) (*corepb.Registration, error) { func (ras *RegistrationAuthorityServerWrapper) UpdateRegistration(ctx context.Context, request *rapb.UpdateRegistrationRequest) (*corepb.Registration, error) {

View File

@ -108,7 +108,18 @@ func (sac StorageAuthorityClientWrapper) GetCertificate(ctx context.Context, ser
return core.Certificate{}, err return core.Certificate{}, err
} }
return pbToCert(response) return PBToCert(response)
}
func (sac StorageAuthorityClientWrapper) GetPrecertificate(ctx context.Context, serial *sapb.Serial) (*corepb.Certificate, error) {
resp, err := sac.inner.GetPrecertificate(ctx, serial)
if err != nil {
return nil, err
}
if resp == nil {
return nil, errIncompleteResponse
}
return resp, nil
} }
func (sac StorageAuthorityClientWrapper) GetCertificateStatus(ctx context.Context, serial string) (core.CertificateStatus, error) { func (sac StorageAuthorityClientWrapper) GetCertificateStatus(ctx context.Context, serial string) (core.CertificateStatus, error) {
@ -729,7 +740,14 @@ func (sas StorageAuthorityServerWrapper) GetCertificate(ctx context.Context, req
return nil, err return nil, err
} }
return certToPB(cert), nil return CertToPB(cert), nil
}
func (sas StorageAuthorityServerWrapper) GetPrecertificate(ctx context.Context, request *sapb.Serial) (*corepb.Certificate, error) {
if request == nil || request.Serial == nil {
return nil, errIncompleteRequest
}
return sas.inner.GetPrecertificate(ctx, request)
} }
func (sas StorageAuthorityServerWrapper) GetCertificateStatus(ctx context.Context, request *sapb.Serial) (*sapb.CertificateStatus, error) { func (sas StorageAuthorityServerWrapper) GetCertificateStatus(ctx context.Context, request *sapb.Serial) (*sapb.CertificateStatus, error) {

View File

@ -303,6 +303,11 @@ func (sa *StorageAuthority) GetCertificate(_ context.Context, serial string) (co
} }
} }
// GetPrecertificate is a mock
func (sa *StorageAuthority) GetPrecertificate(_ context.Context, _ *sapb.Serial) (*corepb.Certificate, error) {
return nil, nil
}
// GetCertificateStatus is a mock // GetCertificateStatus is a mock
func (sa *StorageAuthority) GetCertificateStatus(_ context.Context, serial string) (core.CertificateStatus, error) { func (sa *StorageAuthority) GetCertificateStatus(_ context.Context, serial string) (core.CertificateStatus, error) {
// Serial ee == 238.crt // Serial ee == 238.crt

View File

@ -42,6 +42,10 @@ func (sa *mockInvalidAuthorizationsAuthority) GetCertificate(ctx context.Context
return nil, nil return nil, nil
} }
func (sa *mockInvalidAuthorizationsAuthority) GetPrecertificate(_ context.Context, _ *sapb.Serial, opts ...grpc.CallOption) (*core.Certificate, error) {
return nil, nil
}
func (sa *mockInvalidAuthorizationsAuthority) GetCertificateStatus(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.CertificateStatus, error) { func (sa *mockInvalidAuthorizationsAuthority) GetCertificateStatus(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.CertificateStatus, error) {
return nil, nil return nil, nil
} }

View File

@ -132,6 +132,25 @@ func SelectCertificate(s dbOneSelector, q string, args ...interface{}) (core.Cer
return model, err return model, err
} }
const precertFields = "registrationID, serial, der, issued, expires"
// selectPrecertificate selects all fields of one precertificate object
// identified by serial.
func selectPrecertificate(s dbOneSelector, serial string) (core.Certificate, error) {
var model precertificateModel
err := s.SelectOne(
&model,
"SELECT "+precertFields+" FROM precertificates WHERE serial = ?",
serial)
return core.Certificate{
RegistrationID: model.RegistrationID,
Serial: model.Serial,
DER: model.DER,
Issued: model.Issued,
Expires: model.Expires,
}, err
}
type CertWithID struct { type CertWithID struct {
ID int64 ID int64
core.Certificate core.Certificate

View File

@ -1838,122 +1838,122 @@ func init() {
func init() { proto.RegisterFile("sa/proto/sa.proto", fileDescriptor_099fb35e782a48a6) } func init() { proto.RegisterFile("sa/proto/sa.proto", fileDescriptor_099fb35e782a48a6) }
var fileDescriptor_099fb35e782a48a6 = []byte{ var fileDescriptor_099fb35e782a48a6 = []byte{
// 1826 bytes of a gzipped FileDescriptorProto // 1835 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x59, 0x5f, 0x73, 0xdb, 0xc6, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x59, 0x5f, 0x73, 0xdb, 0xc6,
0x11, 0xe7, 0x1f, 0x53, 0x26, 0x57, 0xb2, 0xfe, 0x9c, 0x25, 0x0a, 0x81, 0x65, 0x9b, 0xbe, 0x3a, 0x11, 0xe7, 0x1f, 0x53, 0x26, 0x57, 0xb2, 0xfe, 0x9c, 0x25, 0x1a, 0x81, 0x65, 0x9b, 0xbe, 0x3a,
0x1e, 0x65, 0x3a, 0xa3, 0xb8, 0x68, 0x27, 0xe9, 0x8c, 0x1a, 0xa7, 0x72, 0x24, 0xcb, 0x4a, 0x1d, 0x1e, 0x65, 0x3a, 0xa3, 0xb8, 0x68, 0x26, 0xe9, 0x8c, 0x1a, 0xa7, 0x72, 0x24, 0xcb, 0x4a, 0x1d,
0x99, 0x01, 0x63, 0xa5, 0xd3, 0xf6, 0x05, 0x21, 0x2e, 0x32, 0x6a, 0x0a, 0x60, 0xee, 0x8e, 0x52, 0x99, 0x01, 0x63, 0xa5, 0xd3, 0xf6, 0x05, 0x21, 0x2e, 0x32, 0x6a, 0x0a, 0x60, 0xee, 0x8e, 0x52,
0xa4, 0xe7, 0xce, 0xb4, 0x9f, 0xa0, 0xd3, 0xc7, 0x7e, 0x8e, 0x7e, 0x89, 0x7e, 0xa1, 0x3e, 0x74, 0xa4, 0xe7, 0xce, 0xb4, 0x9f, 0xa0, 0xd3, 0xc7, 0x7e, 0x86, 0x3e, 0xf6, 0x4b, 0xf4, 0x2b, 0x75,
0x6e, 0xef, 0x00, 0x02, 0xe0, 0x81, 0x72, 0xec, 0x4e, 0xdf, 0xb0, 0x7b, 0xbb, 0x7b, 0x7b, 0x77, 0x6e, 0xef, 0x00, 0x02, 0xe0, 0x81, 0xf2, 0x9f, 0x4e, 0xde, 0xb0, 0x7b, 0xbb, 0x7b, 0x7b, 0x77,
0xfb, 0xe7, 0xb7, 0x24, 0xac, 0x89, 0xe0, 0xe3, 0x31, 0x4f, 0x64, 0xf2, 0xb1, 0x08, 0x76, 0xf0, 0xfb, 0xe7, 0xb7, 0x24, 0xac, 0x89, 0xe0, 0xe3, 0x31, 0x4f, 0x64, 0xf2, 0xb1, 0x08, 0xb6, 0xf1,
0x83, 0x34, 0x44, 0xe0, 0x6e, 0x0c, 0x13, 0xce, 0xcc, 0x82, 0xfa, 0xd4, 0x4b, 0xb4, 0x07, 0xcb, 0x83, 0x34, 0x44, 0xe0, 0x6e, 0x0c, 0x13, 0xce, 0xcc, 0x82, 0xfa, 0xd4, 0x4b, 0xb4, 0x07, 0xcb,
0x3e, 0x3b, 0x8d, 0x84, 0xe4, 0x81, 0x8c, 0x92, 0xf8, 0x68, 0x9f, 0x2c, 0x43, 0x23, 0x0a, 0x9d, 0x3e, 0x3b, 0x89, 0x84, 0xe4, 0x81, 0x8c, 0x92, 0xf8, 0x70, 0x8f, 0x2c, 0x43, 0x23, 0x0a, 0x9d,
0x7a, 0xaf, 0xbe, 0xdd, 0xf4, 0x1b, 0x51, 0x48, 0xef, 0x01, 0x7c, 0x39, 0x78, 0x79, 0xfc, 0x2d, 0x7a, 0xaf, 0xbe, 0xd5, 0xf4, 0x1b, 0x51, 0x48, 0xef, 0x02, 0x7c, 0x35, 0x78, 0x71, 0xf4, 0x1d,
0xfb, 0xee, 0x77, 0xec, 0x92, 0xac, 0x42, 0xf3, 0xcf, 0x17, 0x6f, 0x70, 0x79, 0xc9, 0x57, 0x9f, 0xfb, 0xfe, 0xf7, 0xec, 0x82, 0xac, 0x42, 0xf3, 0x2f, 0xe7, 0xaf, 0x71, 0x79, 0xc9, 0x57, 0x9f,
0xf4, 0x01, 0xac, 0xec, 0x4d, 0xe4, 0xeb, 0x84, 0x47, 0x57, 0xb3, 0x26, 0x3a, 0x68, 0xe2, 0x5f, 0xf4, 0x3e, 0xac, 0xec, 0x4e, 0xe4, 0xab, 0x84, 0x47, 0x97, 0xb3, 0x26, 0x3a, 0x68, 0xe2, 0x3f,
0x75, 0xb8, 0x77, 0xc8, 0x64, 0x9f, 0xc5, 0x61, 0x14, 0x9f, 0x16, 0xa4, 0x7d, 0xf6, 0xc3, 0x84, 0x75, 0xb8, 0x7b, 0xc0, 0x64, 0x9f, 0xc5, 0x61, 0x14, 0x9f, 0x14, 0xa4, 0x7d, 0xf6, 0xe3, 0x84,
0x09, 0x49, 0x1e, 0xc1, 0x32, 0x2f, 0xf8, 0x61, 0x3c, 0x28, 0x71, 0x95, 0x5c, 0x14, 0xb2, 0x58, 0x09, 0x49, 0x1e, 0xc2, 0x32, 0x2f, 0xf8, 0x61, 0x3c, 0x28, 0x71, 0x95, 0x5c, 0x14, 0xb2, 0x58,
0x46, 0xdf, 0x47, 0x8c, 0x7f, 0x73, 0x39, 0x66, 0x4e, 0x03, 0xb7, 0x29, 0x71, 0xc9, 0x36, 0xac, 0x46, 0x3f, 0x44, 0x8c, 0x7f, 0x7b, 0x31, 0x66, 0x4e, 0x03, 0xb7, 0x29, 0x71, 0xc9, 0x16, 0xac,
0x4c, 0x39, 0x27, 0xc1, 0x68, 0xc2, 0x9c, 0x26, 0x0a, 0x96, 0xd9, 0xe4, 0x1e, 0xc0, 0x79, 0x30, 0x4c, 0x39, 0xc7, 0xc1, 0x68, 0xc2, 0x9c, 0x26, 0x0a, 0x96, 0xd9, 0xe4, 0x2e, 0xc0, 0x59, 0x30,
0x8a, 0xc2, 0x57, 0xb1, 0x8c, 0x46, 0xce, 0x0d, 0xdc, 0x35, 0xc7, 0xa1, 0x02, 0xee, 0x1e, 0x32, 0x8a, 0xc2, 0x97, 0xb1, 0x8c, 0x46, 0xce, 0x35, 0xdc, 0x35, 0xc7, 0xa1, 0x02, 0xee, 0x1c, 0x30,
0x79, 0xa2, 0x18, 0x05, 0xcf, 0xc5, 0x4f, 0x75, 0xdd, 0x81, 0x9b, 0x61, 0x72, 0x16, 0x44, 0xb1, 0x79, 0xac, 0x18, 0x05, 0xcf, 0xc5, 0xdb, 0xba, 0xee, 0xc0, 0xf5, 0x30, 0x39, 0x0d, 0xa2, 0x58,
0x70, 0x1a, 0xbd, 0xe6, 0x76, 0xc7, 0x4f, 0x49, 0x75, 0xa9, 0x71, 0x72, 0x81, 0x0e, 0x36, 0x7d, 0x38, 0x8d, 0x5e, 0x73, 0xab, 0xe3, 0xa7, 0xa4, 0xba, 0xd4, 0x38, 0x39, 0x47, 0x07, 0x9b, 0xbe,
0xf5, 0x49, 0xff, 0x59, 0x87, 0xdb, 0x96, 0x2d, 0xc9, 0xaf, 0xa1, 0x85, 0xae, 0x39, 0xf5, 0x5e, 0xfa, 0xa4, 0xff, 0xaa, 0xc3, 0x4d, 0xcb, 0x96, 0xe4, 0x37, 0xd0, 0x42, 0xd7, 0x9c, 0x7a, 0xaf,
0x73, 0x7b, 0xd1, 0xa3, 0x3b, 0x22, 0xd8, 0xb1, 0xc8, 0xed, 0x7c, 0x15, 0x8c, 0x0f, 0x46, 0xec, 0xb9, 0xb5, 0xe8, 0xd1, 0x6d, 0x11, 0x6c, 0x5b, 0xe4, 0xb6, 0xbf, 0x0e, 0xc6, 0xfb, 0x23, 0x76,
0x8c, 0xc5, 0xd2, 0xd7, 0x0a, 0xee, 0x4b, 0x80, 0x29, 0x93, 0x74, 0x61, 0x41, 0x6f, 0x6e, 0x5e, 0xca, 0x62, 0xe9, 0x6b, 0x05, 0xf7, 0x05, 0xc0, 0x94, 0x49, 0xba, 0xb0, 0xa0, 0x37, 0x37, 0xaf,
0xc9, 0x50, 0xe4, 0x23, 0x68, 0x05, 0x13, 0xf9, 0xfa, 0x0a, 0x6f, 0x75, 0xd1, 0xbb, 0xbd, 0x83, 0x64, 0x28, 0xf2, 0x11, 0xb4, 0x82, 0x89, 0x7c, 0x75, 0x89, 0xb7, 0xba, 0xe8, 0xdd, 0xdc, 0xc6,
0xa1, 0x52, 0x7c, 0x31, 0x2d, 0x41, 0xff, 0xdd, 0x80, 0xb5, 0x2f, 0x18, 0x57, 0x57, 0x39, 0x0c, 0x50, 0x29, 0xbe, 0x98, 0x96, 0xa0, 0xff, 0x6d, 0xc0, 0xda, 0x97, 0x8c, 0xab, 0xab, 0x1c, 0x06,
0x24, 0x1b, 0xc8, 0x40, 0x4e, 0x84, 0x32, 0x2c, 0x18, 0x8f, 0x82, 0x51, 0x6a, 0x58, 0x53, 0xc8, 0x92, 0x0d, 0x64, 0x20, 0x27, 0x42, 0x19, 0x16, 0x8c, 0x47, 0xc1, 0x28, 0x35, 0xac, 0x29, 0xe4,
0x47, 0x09, 0xf3, 0x0c, 0x86, 0x52, 0xef, 0x94, 0x0c, 0xc5, 0xf8, 0x45, 0x20, 0xe4, 0xab, 0x71, 0xa3, 0x84, 0x79, 0x06, 0x43, 0xa9, 0x77, 0x4a, 0x86, 0x62, 0xfc, 0x3c, 0x10, 0xf2, 0xe5, 0x38,
0x18, 0x48, 0x16, 0x9a, 0x27, 0x28, 0xb3, 0x49, 0x0f, 0x16, 0x39, 0x3b, 0x4f, 0xde, 0xb0, 0x70, 0x0c, 0x24, 0x0b, 0xcd, 0x13, 0x94, 0xd9, 0xa4, 0x07, 0x8b, 0x9c, 0x9d, 0x25, 0xaf, 0x59, 0xb8,
0x3f, 0x90, 0xcc, 0x69, 0xa1, 0x54, 0x9e, 0x45, 0x1e, 0xc2, 0x2d, 0x43, 0xfa, 0x2c, 0x10, 0x49, 0x17, 0x48, 0xe6, 0xb4, 0x50, 0x2a, 0xcf, 0x22, 0x0f, 0xe0, 0x86, 0x21, 0x7d, 0x16, 0x88, 0x24,
0xec, 0x2c, 0xa0, 0x4c, 0x91, 0x49, 0x7e, 0x05, 0x1b, 0xa3, 0x40, 0xc8, 0x83, 0x1f, 0xc7, 0x91, 0x76, 0x16, 0x50, 0xa6, 0xc8, 0x24, 0x9f, 0xc0, 0xc6, 0x28, 0x10, 0x72, 0xff, 0xa7, 0x71, 0xa4,
0x7e, 0x9a, 0xe3, 0xe0, 0x74, 0xc0, 0x62, 0xe9, 0xdc, 0x44, 0x69, 0xfb, 0x22, 0xa1, 0xb0, 0xa4, 0x9f, 0xe6, 0x28, 0x38, 0x19, 0xb0, 0x58, 0x3a, 0xd7, 0x51, 0xda, 0xbe, 0x48, 0x28, 0x2c, 0x29,
0x1c, 0xf2, 0x99, 0x18, 0x27, 0xb1, 0x60, 0x4e, 0x1b, 0x13, 0xa0, 0xc0, 0x23, 0x2e, 0xb4, 0xe3, 0x87, 0x7c, 0x26, 0xc6, 0x49, 0x2c, 0x98, 0xd3, 0xc6, 0x04, 0x28, 0xf0, 0x88, 0x0b, 0xed, 0x38,
0x44, 0xee, 0x7d, 0x2f, 0x19, 0x77, 0x3a, 0x68, 0x2c, 0xa3, 0xc9, 0x16, 0x74, 0x22, 0x81, 0x66, 0x91, 0xbb, 0x3f, 0x48, 0xc6, 0x9d, 0x0e, 0x1a, 0xcb, 0x68, 0xb2, 0x09, 0x9d, 0x48, 0xa0, 0x59,
0x59, 0xe8, 0x40, 0xaf, 0xbe, 0xdd, 0xf6, 0xa7, 0x8c, 0x2f, 0x6f, 0xb4, 0x1b, 0xab, 0x4d, 0xda, 0x16, 0x3a, 0xd0, 0xab, 0x6f, 0xb5, 0xfd, 0x29, 0xe3, 0xab, 0x6b, 0xed, 0xc6, 0x6a, 0x93, 0xf6,
0x83, 0x85, 0xc1, 0xf4, 0xb6, 0x2c, 0xb7, 0x48, 0x77, 0xa1, 0xe5, 0x07, 0xf1, 0x29, 0x6e, 0xc5, 0x60, 0x61, 0x30, 0xbd, 0x2d, 0xcb, 0x2d, 0xd2, 0x1d, 0x68, 0xf9, 0x41, 0x7c, 0x82, 0x5b, 0xb1,
0x02, 0x3e, 0x8a, 0x98, 0x90, 0x26, 0xda, 0x32, 0x5a, 0x29, 0x8f, 0x02, 0xa9, 0x56, 0x1a, 0xb8, 0x80, 0x8f, 0x22, 0x26, 0xa4, 0x89, 0xb6, 0x8c, 0x56, 0xca, 0xa3, 0x40, 0xaa, 0x95, 0x06, 0xae,
0x62, 0x28, 0x7a, 0x17, 0x5a, 0x5f, 0x24, 0x93, 0x58, 0x92, 0x75, 0x68, 0x0d, 0xd5, 0x87, 0xd1, 0x18, 0x8a, 0xde, 0x81, 0xd6, 0x97, 0xc9, 0x24, 0x96, 0x64, 0x1d, 0x5a, 0x43, 0xf5, 0x61, 0x34,
0xd4, 0x04, 0xfd, 0x3d, 0xdc, 0xc7, 0xe5, 0xdc, 0x9b, 0x8a, 0xa7, 0x97, 0xc7, 0xc1, 0x19, 0xcb, 0x35, 0x41, 0xff, 0x00, 0xf7, 0x70, 0x39, 0xf7, 0xa6, 0xe2, 0xc9, 0xc5, 0x51, 0x70, 0xca, 0xb2,
0x22, 0xfd, 0x3e, 0xb4, 0xb8, 0xda, 0x1e, 0x15, 0x17, 0xbd, 0x8e, 0x8a, 0x3e, 0xf4, 0xc7, 0xd7, 0x48, 0xbf, 0x07, 0x2d, 0xae, 0xb6, 0x47, 0xc5, 0x45, 0xaf, 0xa3, 0xa2, 0x0f, 0xfd, 0xf1, 0x35,
0x7c, 0x65, 0x39, 0x56, 0x0a, 0x26, 0xc0, 0x35, 0x41, 0xff, 0x5a, 0x87, 0x25, 0x34, 0x6d, 0xcc, 0x5f, 0x59, 0x8e, 0x95, 0x82, 0x09, 0x70, 0x4d, 0xd0, 0xbf, 0xd5, 0x61, 0x09, 0x4d, 0x1b, 0x73,
0x91, 0xcf, 0x61, 0x69, 0x98, 0xa3, 0x4d, 0x30, 0xdf, 0x51, 0xe6, 0xf2, 0x72, 0xf9, 0x28, 0x2e, 0xe4, 0x0b, 0x58, 0x1a, 0xe6, 0x68, 0x13, 0xcc, 0xb7, 0x95, 0xb9, 0xbc, 0x5c, 0x3e, 0x8a, 0x0b,
0x28, 0xb8, 0x9f, 0x14, 0x82, 0x99, 0xc0, 0x0d, 0xb5, 0x91, 0xb9, 0x2b, 0xfc, 0x9e, 0x9e, 0xb1, 0x0a, 0xee, 0xa7, 0x85, 0x60, 0x26, 0x70, 0x4d, 0x6d, 0x64, 0xee, 0x0a, 0xbf, 0xa7, 0x67, 0x6c,
0x91, 0x3f, 0x63, 0x1f, 0xee, 0xe2, 0x06, 0xf9, 0x92, 0x27, 0x9e, 0x5e, 0x1e, 0xf5, 0xd3, 0x13, 0xe4, 0xcf, 0xd8, 0x87, 0x3b, 0xb8, 0x41, 0xbe, 0xe4, 0x89, 0x27, 0x17, 0x87, 0xfd, 0xf4, 0x84,
0xaa, 0xca, 0x35, 0x36, 0xd5, 0xad, 0x11, 0x8d, 0xa7, 0x27, 0x6e, 0xd8, 0x4f, 0x4c, 0xff, 0x56, 0xaa, 0x72, 0x8d, 0x4d, 0x75, 0x6b, 0x44, 0xe3, 0xe9, 0x89, 0x1b, 0xf6, 0x13, 0xd3, 0xbf, 0xd7,
0x87, 0x07, 0x68, 0xf2, 0x28, 0x3e, 0x7f, 0xff, 0x12, 0xe1, 0x42, 0xfb, 0x75, 0x22, 0x24, 0x9e, 0xe1, 0x3e, 0x9a, 0x3c, 0x8c, 0xcf, 0xde, 0xbf, 0x44, 0xb8, 0xd0, 0x7e, 0x95, 0x08, 0x89, 0xa7,
0x46, 0xd7, 0xb5, 0x8c, 0x9e, 0xba, 0xd2, 0xac, 0x70, 0x65, 0x00, 0x04, 0x3d, 0x79, 0xc9, 0x43, 0xd1, 0x75, 0x2d, 0xa3, 0xa7, 0xae, 0x34, 0x2b, 0x5c, 0x19, 0x00, 0x41, 0x4f, 0x5e, 0xf0, 0x90,
0xc6, 0xb3, 0xad, 0xb7, 0xa0, 0x13, 0x0c, 0xf1, 0xf4, 0xd9, 0xae, 0x53, 0xc6, 0xf5, 0xe7, 0x7b, 0xf1, 0x6c, 0xeb, 0x4d, 0xe8, 0x04, 0x43, 0x3c, 0x7d, 0xb6, 0xeb, 0x94, 0x71, 0xf5, 0xf9, 0x9e,
0x0e, 0xeb, 0x68, 0xf4, 0xd9, 0xd7, 0xfb, 0xc7, 0x03, 0x26, 0x33, 0xb3, 0x5d, 0x58, 0xb8, 0x88, 0xc1, 0x3a, 0x1a, 0x7d, 0xfa, 0xcd, 0xde, 0xd1, 0x80, 0xc9, 0xcc, 0x6c, 0x17, 0x16, 0xce, 0xa3,
0xe2, 0x30, 0xb9, 0x30, 0x36, 0x0d, 0x55, 0x5d, 0xe4, 0xe8, 0x63, 0x58, 0x37, 0x46, 0x0e, 0x7e, 0x38, 0x4c, 0xce, 0x8d, 0x4d, 0x43, 0x55, 0x17, 0x39, 0xfa, 0x08, 0xd6, 0x8d, 0x91, 0xfd, 0x9f,
0x8c, 0xc4, 0xd4, 0x52, 0x4e, 0xa3, 0x5e, 0xd4, 0xe8, 0x43, 0xaf, 0xcf, 0xd9, 0x79, 0x94, 0x4c, 0x22, 0x31, 0xb5, 0x94, 0xd3, 0xa8, 0x17, 0x35, 0xfa, 0xd0, 0xeb, 0x73, 0x76, 0x16, 0x25, 0x13,
0x44, 0x2e, 0x28, 0x8b, 0xda, 0x55, 0x85, 0x6c, 0x1d, 0x5a, 0x9c, 0x9d, 0x1e, 0xed, 0xa7, 0xef, 0x91, 0x0b, 0xca, 0xa2, 0x76, 0x55, 0x21, 0x5b, 0x87, 0x16, 0x67, 0x27, 0x87, 0x7b, 0xe9, 0xfb,
0x8f, 0x84, 0xca, 0x30, 0xad, 0xae, 0xf4, 0x18, 0x7e, 0xa1, 0x5e, 0xdb, 0x37, 0x14, 0x95, 0xb0, 0x23, 0xa1, 0x32, 0x4c, 0xab, 0x2b, 0x3d, 0x86, 0x5f, 0xa8, 0xd7, 0xf6, 0x0d, 0x45, 0x25, 0xac,
0xba, 0x17, 0x86, 0x3a, 0x0d, 0xd3, 0x3d, 0x32, 0x5b, 0xf5, 0x9c, 0xad, 0x5c, 0x8e, 0x36, 0x0a, 0xee, 0x86, 0xa1, 0x4e, 0xc3, 0x74, 0x8f, 0xcc, 0x56, 0x3d, 0x67, 0x2b, 0x97, 0xa3, 0x8d, 0x42,
0x95, 0xce, 0x81, 0x9b, 0x43, 0xce, 0xb0, 0x92, 0xe9, 0x82, 0x9e, 0x92, 0x6a, 0x85, 0x61, 0xc2, 0xa5, 0x73, 0xe0, 0xfa, 0x90, 0x33, 0xac, 0x64, 0xba, 0xa0, 0xa7, 0xa4, 0x5a, 0x61, 0x98, 0xf0,
0x0b, 0x53, 0xe3, 0x52, 0x92, 0xbe, 0x81, 0x8d, 0xbd, 0x30, 0xcc, 0x1d, 0x32, 0xdd, 0x7a, 0x15, 0xc2, 0xd4, 0xb8, 0x94, 0xa4, 0xaf, 0x61, 0x63, 0x37, 0x0c, 0x73, 0x87, 0x4c, 0xb7, 0x5e, 0x85,
0x9a, 0x21, 0xe3, 0x69, 0xbb, 0x0d, 0x19, 0xb7, 0x1f, 0x4c, 0xa5, 0x80, 0x2a, 0x45, 0xb8, 0xe3, 0x66, 0xc8, 0x78, 0xda, 0x6e, 0x43, 0xc6, 0xed, 0x07, 0x53, 0x29, 0xa0, 0x4a, 0x11, 0xee, 0xb8,
0x92, 0x8f, 0xdf, 0xca, 0xc1, 0x48, 0x88, 0x49, 0x56, 0x51, 0x0d, 0x45, 0x1f, 0x43, 0xb7, 0xbc, 0xe4, 0xe3, 0xb7, 0x72, 0x30, 0x12, 0x62, 0x92, 0x55, 0x54, 0x43, 0xd1, 0x47, 0xd0, 0x2d, 0x6f,
0x99, 0x29, 0x60, 0xea, 0x32, 0xa3, 0xd3, 0xb4, 0xa6, 0xa8, 0xcb, 0x44, 0x8a, 0xf6, 0x61, 0x09, 0x66, 0x0a, 0x98, 0xba, 0xcc, 0xe8, 0x24, 0xad, 0x29, 0xea, 0x32, 0x91, 0xa2, 0x7d, 0x58, 0xc2,
0x83, 0x2a, 0x9f, 0x25, 0x39, 0x88, 0x40, 0x1e, 0xc3, 0xed, 0x89, 0x60, 0x27, 0x5e, 0x31, 0xf8, 0xa0, 0xca, 0x67, 0x49, 0x0e, 0x22, 0x90, 0x47, 0x70, 0x73, 0x22, 0xd8, 0xb1, 0x57, 0x0c, 0x7e,
0xd1, 0xc3, 0xb6, 0x6f, 0x5b, 0xa2, 0x2f, 0x80, 0xa6, 0x4d, 0x15, 0x2d, 0xdb, 0xd3, 0xa6, 0xbc, 0xf4, 0xb0, 0xed, 0xdb, 0x96, 0xe8, 0x73, 0xa0, 0x69, 0x53, 0x45, 0xcb, 0xf6, 0xb4, 0x29, 0xef,
0x4f, 0x17, 0x16, 0x82, 0xe1, 0x50, 0x66, 0x87, 0x37, 0x14, 0xbd, 0x84, 0xcd, 0x43, 0xa6, 0xe3, 0xd3, 0x85, 0x85, 0x60, 0x38, 0x94, 0xd9, 0xe1, 0x0d, 0x45, 0x2f, 0xe0, 0xd6, 0x01, 0xd3, 0x71,
0xfe, 0x59, 0xc2, 0x0b, 0x25, 0x6b, 0xaa, 0x52, 0xcf, 0xab, 0xd8, 0x2b, 0x55, 0xd5, 0x41, 0x9a, 0xff, 0x34, 0xe1, 0x85, 0x92, 0x35, 0x55, 0xa9, 0xe7, 0x55, 0xec, 0x95, 0xaa, 0xea, 0x20, 0xcd,
0xd5, 0x07, 0xf9, 0x47, 0x1d, 0x9c, 0x43, 0x26, 0xff, 0x6f, 0xc8, 0x40, 0x35, 0x4c, 0xce, 0x7e, 0xea, 0x83, 0xfc, 0xb3, 0x0e, 0xce, 0x01, 0x93, 0x3f, 0x1b, 0x32, 0x50, 0x0d, 0x93, 0xb3, 0x1f,
0x98, 0x44, 0xdc, 0xf8, 0x72, 0xa5, 0x83, 0xa9, 0xed, 0x97, 0xd9, 0xf4, 0xef, 0x75, 0x58, 0x2e, 0x27, 0x11, 0x37, 0xbe, 0x5c, 0xea, 0x60, 0x6a, 0xfb, 0x65, 0x36, 0xfd, 0x47, 0x1d, 0x96, 0x4b,
0xc1, 0x87, 0x5f, 0xa6, 0xed, 0x5d, 0x57, 0xdc, 0xbb, 0x2a, 0xdd, 0xe7, 0x20, 0x07, 0x94, 0xfd, 0xf0, 0xe1, 0xd7, 0x69, 0x7b, 0xd7, 0x15, 0xf7, 0x8e, 0x4a, 0xf7, 0x39, 0xc8, 0x01, 0x65, 0xff,
0xdf, 0x23, 0x87, 0x17, 0x70, 0x7f, 0x2f, 0x0c, 0x6d, 0x68, 0x30, 0xbb, 0xb9, 0x8f, 0x8a, 0x8e, 0xff, 0xc8, 0xe1, 0x39, 0xdc, 0xdb, 0x0d, 0x43, 0x1b, 0x1a, 0xcc, 0x6e, 0xee, 0xa3, 0xa2, 0xa3,
0xce, 0xb3, 0xf6, 0x10, 0x56, 0x4b, 0xf8, 0x13, 0xaf, 0x2d, 0x0a, 0xd3, 0x7a, 0xa2, 0x3e, 0x29, 0xf3, 0xac, 0x3d, 0x80, 0xd5, 0x12, 0xfe, 0xc4, 0x6b, 0x8b, 0xc2, 0xb4, 0x9e, 0xa8, 0x4f, 0x4a,
0x9d, 0x91, 0xf2, 0x66, 0x90, 0xee, 0x87, 0xb0, 0x56, 0x90, 0xf1, 0x4a, 0xa6, 0x9a, 0xda, 0xd4, 0x67, 0xa4, 0xbc, 0x19, 0xa4, 0xfb, 0x21, 0xac, 0x15, 0x64, 0xbc, 0x92, 0xa9, 0xa6, 0x36, 0x75,
0x15, 0x38, 0x3e, 0x22, 0x0a, 0x4b, 0xbe, 0xce, 0x81, 0x3f, 0x5c, 0x63, 0x12, 0x13, 0xb9, 0x9a, 0x09, 0x8e, 0x8f, 0x88, 0xc2, 0x92, 0xaf, 0x73, 0xe0, 0x0f, 0xd7, 0x98, 0xc4, 0x44, 0xae, 0xa6,
0x52, 0x79, 0xab, 0xd0, 0x8d, 0x79, 0x60, 0xfc, 0x56, 0x4d, 0x80, 0xa7, 0x30, 0xe3, 0x06, 0xe6, 0x54, 0xde, 0x2a, 0x74, 0x63, 0x1e, 0x18, 0xbf, 0x55, 0x13, 0xe0, 0x29, 0xcc, 0xb8, 0x86, 0xf9,
0x73, 0x46, 0xd3, 0xbf, 0x34, 0x60, 0xeb, 0x59, 0x14, 0x07, 0xa3, 0xe8, 0x8a, 0x59, 0x71, 0xb4, 0x9c, 0xd1, 0xf4, 0xaf, 0x0d, 0xd8, 0x7c, 0x1a, 0xc5, 0xc1, 0x28, 0xba, 0x64, 0x56, 0x1c, 0x6d,
0x25, 0x65, 0x0c, 0xee, 0x6a, 0x14, 0x70, 0x57, 0xae, 0x16, 0x35, 0x0b, 0xb5, 0x08, 0x1b, 0x86, 0x49, 0x19, 0x83, 0xbb, 0x1a, 0x05, 0xdc, 0x95, 0xab, 0x45, 0xcd, 0x42, 0x2d, 0xc2, 0x86, 0x21,
0x94, 0xec, 0x6c, 0x9c, 0x62, 0xb1, 0x8e, 0x3f, 0x65, 0x90, 0x7d, 0x58, 0xc3, 0x3e, 0x67, 0x36, 0x25, 0x3b, 0x1d, 0xa7, 0x58, 0xac, 0xe3, 0x4f, 0x19, 0x64, 0x0f, 0xd6, 0xb0, 0xcf, 0x99, 0x4d,
0x1d, 0x26, 0x3c, 0x14, 0x4e, 0x0b, 0x1f, 0xa9, 0xab, 0x1f, 0xe9, 0xa4, 0xb4, 0xec, 0xcf, 0x2a, 0x87, 0x09, 0x0f, 0x85, 0xd3, 0xc2, 0x47, 0xea, 0xea, 0x47, 0x3a, 0x2e, 0x2d, 0xfb, 0xb3, 0x0a,
0x90, 0x27, 0xb0, 0x32, 0x65, 0x1e, 0x70, 0x9e, 0x70, 0xc4, 0x6a, 0x8b, 0xde, 0xba, 0xb6, 0xd1, 0xe4, 0x31, 0xac, 0x4c, 0x99, 0xfb, 0x9c, 0x27, 0x1c, 0xb1, 0xda, 0xa2, 0xb7, 0xae, 0x6d, 0xf4,
0xe7, 0xc9, 0x77, 0x23, 0x76, 0xb6, 0xcf, 0x64, 0x10, 0x8d, 0x84, 0x5f, 0x16, 0xf6, 0xfe, 0xb3, 0x79, 0xf2, 0xfd, 0x88, 0x9d, 0xee, 0x31, 0x19, 0x44, 0x23, 0xe1, 0x97, 0x85, 0xbd, 0x7f, 0x3b,
0x09, 0xab, 0x03, 0x99, 0xf0, 0xe0, 0x34, 0xbd, 0x05, 0x79, 0x49, 0x76, 0x61, 0xe5, 0x90, 0x15, 0xb0, 0x3a, 0x90, 0x09, 0x0f, 0x4e, 0xd2, 0x5b, 0x90, 0x17, 0x64, 0x07, 0x56, 0x0e, 0x58, 0xa1,
0x5a, 0x3b, 0x21, 0xd8, 0xcf, 0x0a, 0xc9, 0xe6, 0x12, 0xbd, 0x45, 0x9e, 0x4b, 0x6b, 0xe4, 0x37, 0xb5, 0x13, 0x82, 0xfd, 0xac, 0x90, 0x6c, 0x2e, 0xd1, 0x5b, 0xe4, 0xb9, 0xb4, 0x46, 0x7e, 0x0b,
0xb0, 0x5e, 0x52, 0x7e, 0x7a, 0xa9, 0xe6, 0x9d, 0x65, 0x65, 0x61, 0x3a, 0xff, 0x54, 0x68, 0x3f, 0xeb, 0x25, 0xe5, 0x27, 0x17, 0x6a, 0xde, 0x59, 0x56, 0x16, 0xa6, 0xf3, 0x4f, 0x85, 0xf6, 0x63,
0x81, 0xd5, 0x72, 0x11, 0x20, 0xb7, 0x67, 0x92, 0xeb, 0x68, 0xdf, 0xb5, 0x05, 0x32, 0xad, 0x91, 0x58, 0x2d, 0x17, 0x01, 0x72, 0x73, 0x26, 0xb9, 0x0e, 0xf7, 0x5c, 0x5b, 0x20, 0xd3, 0x1a, 0xf9,
0x6f, 0xb0, 0x80, 0xd9, 0x32, 0x82, 0x20, 0xc4, 0x9f, 0x3f, 0x3c, 0x55, 0x59, 0x3d, 0x81, 0xae, 0x16, 0x0b, 0x98, 0x2d, 0x23, 0x08, 0x42, 0xfc, 0xf9, 0xc3, 0x53, 0x95, 0xd5, 0x63, 0xe8, 0xda,
0x7d, 0x72, 0x21, 0x0f, 0x8c, 0xd1, 0xea, 0xa9, 0xc6, 0xdd, 0xac, 0x18, 0x2d, 0x68, 0x8d, 0xfc, 0x27, 0x17, 0x72, 0xdf, 0x18, 0xad, 0x9e, 0x6a, 0xdc, 0x5b, 0x15, 0xa3, 0x05, 0xad, 0x91, 0x5f,
0x02, 0x96, 0x0f, 0x59, 0x1e, 0x27, 0x12, 0x50, 0xc2, 0xba, 0x69, 0xba, 0x6b, 0xda, 0x99, 0xdc, 0xc1, 0xf2, 0x01, 0xcb, 0xe3, 0x44, 0x02, 0x4a, 0x58, 0x37, 0x4d, 0x77, 0x4d, 0x3b, 0x93, 0x5b,
0x32, 0xad, 0x91, 0x5d, 0xbc, 0xde, 0xd9, 0x71, 0x21, 0xaf, 0xb8, 0x81, 0xf8, 0xaf, 0x2c, 0x42, 0xa6, 0x35, 0xf2, 0x09, 0xac, 0xa9, 0x33, 0x70, 0x36, 0x7c, 0x1b, 0xad, 0x1d, 0x7c, 0x94, 0xd9,
0x6b, 0x64, 0x00, 0x4e, 0x15, 0x32, 0x25, 0x3f, 0xcb, 0x40, 0x63, 0x35, 0x6e, 0x75, 0x57, 0xcb, 0x21, 0x23, 0xaf, 0xb8, 0x81, 0xa8, 0xb1, 0x2c, 0x42, 0x6b, 0x64, 0x00, 0x4e, 0x15, 0x9e, 0x25,
0xc8, 0x92, 0xd6, 0xc8, 0x73, 0xe8, 0xda, 0xa1, 0xa0, 0xbe, 0x9c, 0xb9, 0x30, 0xd1, 0xed, 0x64, 0xbf, 0xc8, 0xa0, 0x66, 0x35, 0xda, 0x75, 0x57, 0xcb, 0x78, 0x94, 0xd6, 0xc8, 0x33, 0xe8, 0xda,
0x22, 0xb4, 0x46, 0xbe, 0x82, 0x3b, 0x15, 0xd2, 0x88, 0x89, 0x7f, 0xaa, 0xb9, 0xcf, 0xc0, 0xc5, 0x01, 0xa4, 0xbe, 0xd2, 0xb9, 0xe0, 0xd2, 0xed, 0x64, 0x22, 0xb4, 0x46, 0xbe, 0x86, 0xdb, 0x15,
0x4f, 0x6b, 0x7d, 0xb4, 0x46, 0x74, 0x41, 0xdd, 0x83, 0xc5, 0x1c, 0x0a, 0x24, 0xdd, 0x6c, 0xad, 0xd2, 0x88, 0xa4, 0xdf, 0xd6, 0xdc, 0xe7, 0xe0, 0xe2, 0xa7, 0xb5, 0xaa, 0x5a, 0xf3, 0xa0, 0xa0,
0x00, 0x0b, 0x8b, 0x3a, 0x7d, 0xb3, 0xa5, 0x15, 0xc3, 0x92, 0x0f, 0x33, 0xd1, 0x79, 0x18, 0xb7, 0xee, 0xc1, 0x62, 0x0e, 0x3b, 0x92, 0x6e, 0xb6, 0x56, 0x00, 0x93, 0x45, 0x9d, 0xbe, 0xd9, 0xd2,
0x68, 0xf1, 0x13, 0xb8, 0x55, 0x80, 0x8d, 0xc4, 0xc9, 0x56, 0x4b, 0x48, 0xb2, 0xa8, 0xf7, 0x29, 0x8a, 0x7c, 0xc9, 0x87, 0x99, 0xe8, 0x3c, 0x64, 0x5c, 0xb4, 0xf8, 0x29, 0xdc, 0x28, 0x80, 0x4d,
0xdc, 0x2a, 0x80, 0x44, 0xad, 0x67, 0xc3, 0x8d, 0x2e, 0x86, 0x8e, 0x66, 0xd1, 0x1a, 0x79, 0x09, 0xe2, 0x64, 0xab, 0x25, 0xfc, 0x59, 0xd4, 0xfb, 0x0c, 0x6e, 0x14, 0xa0, 0xa5, 0xd6, 0xb3, 0xa1,
0x1f, 0x54, 0x62, 0x45, 0xf2, 0x50, 0x89, 0x5e, 0x07, 0x25, 0x4b, 0x06, 0x7f, 0x0b, 0x6b, 0xe5, 0x4d, 0x17, 0x43, 0x47, 0xb3, 0x68, 0x8d, 0xbc, 0x80, 0x0f, 0x2a, 0x11, 0x26, 0x79, 0xa0, 0x44,
0x94, 0xf6, 0xc8, 0xba, 0x25, 0xa7, 0xbd, 0xaa, 0xf4, 0x7b, 0x0e, 0x64, 0x06, 0x19, 0x78, 0x64, 0xaf, 0x02, 0xa0, 0x25, 0x83, 0xbf, 0xc3, 0x38, 0x2f, 0x76, 0x11, 0xb2, 0x6e, 0xa9, 0x04, 0x5e,
0xcb, 0xa4, 0x9e, 0xfd, 0x12, 0xc9, 0x6c, 0x47, 0xa6, 0x35, 0xf2, 0x0a, 0x31, 0x86, 0x2d, 0x20, 0x55, 0xd2, 0x3e, 0x03, 0x32, 0x83, 0x27, 0x3c, 0xb2, 0x69, 0x12, 0xd6, 0x7e, 0x89, 0x64, 0xb6,
0xbc, 0xf7, 0xa9, 0x0f, 0x4f, 0x4c, 0xe0, 0x5a, 0x23, 0xcd, 0xbb, 0x3e, 0xd4, 0xfe, 0x04, 0x5b, 0x8f, 0xd3, 0x1a, 0x79, 0x89, 0xc8, 0xc4, 0x16, 0x10, 0xde, 0xfb, 0x54, 0x95, 0xc7, 0x26, 0x70,
0x73, 0x40, 0x9c, 0x47, 0x1e, 0xe5, 0xab, 0x4c, 0x35, 0xcc, 0xab, 0x38, 0xf4, 0xd7, 0xc6, 0x3b, 0xad, 0x91, 0xe6, 0x5d, 0x1d, 0x6a, 0x7f, 0x86, 0xcd, 0x39, 0xd0, 0xcf, 0x23, 0x0f, 0xf3, 0xb5,
0x6b, 0xd0, 0x79, 0xef, 0x14, 0x95, 0x3e, 0x96, 0xd9, 0x13, 0x9b, 0xb9, 0xb7, 0xa8, 0x88, 0x76, 0xa9, 0x1a, 0x1c, 0x56, 0x1c, 0xfa, 0x1b, 0xe3, 0x9d, 0x35, 0xe8, 0xbc, 0x77, 0x8a, 0x4a, 0x1f,
0x37, 0x77, 0x61, 0xe5, 0x98, 0x5d, 0x94, 0xba, 0xce, 0x4c, 0x8f, 0xa8, 0xe8, 0x1b, 0x9f, 0x02, 0x8b, 0xf3, 0xb1, 0xcd, 0xdc, 0x1b, 0xd4, 0x51, 0xbb, 0x9b, 0x3b, 0xb0, 0x72, 0xc4, 0xce, 0x4b,
0xd1, 0x3f, 0x6f, 0x5c, 0xab, 0xbf, 0xa8, 0x79, 0x07, 0x67, 0x63, 0x79, 0x49, 0x6b, 0xe4, 0x00, 0xbd, 0x6a, 0xa6, 0xb3, 0x54, 0x74, 0x9b, 0xcf, 0x80, 0xe8, 0x1f, 0x45, 0xae, 0xd4, 0x5f, 0xd4,
0x36, 0x8f, 0xd9, 0x85, 0xb5, 0x61, 0xd8, 0x1e, 0xbb, 0x2a, 0x02, 0x76, 0x61, 0xc3, 0x8a, 0x26, 0xbc, 0xfd, 0xd3, 0xb1, 0xbc, 0xa0, 0x35, 0xb2, 0x0f, 0xb7, 0x8e, 0xd8, 0xb9, 0xb5, 0xcd, 0xd8,
0xec, 0x46, 0x4a, 0x3e, 0x1c, 0xc1, 0x72, 0x71, 0x8e, 0x20, 0x1f, 0xe0, 0x0d, 0xd9, 0x06, 0x19, 0x1e, 0xbb, 0x2a, 0x02, 0x76, 0x60, 0xc3, 0x8a, 0x41, 0xec, 0x46, 0x4a, 0x3e, 0x1c, 0xc2, 0x72,
0xd7, 0xb5, 0x2d, 0x19, 0x50, 0xa3, 0x6a, 0xde, 0x9a, 0x42, 0x84, 0x9c, 0x0d, 0xdf, 0xce, 0x5a, 0x71, 0xfa, 0x20, 0x1f, 0xe0, 0x0d, 0xd9, 0xc6, 0x1f, 0xd7, 0xb5, 0x2d, 0x19, 0x28, 0xa4, 0x6a,
0xc9, 0x93, 0xc7, 0xd0, 0xc9, 0x86, 0x36, 0x93, 0xa3, 0xa5, 0x19, 0xae, 0xac, 0xb1, 0x0b, 0xdd, 0xde, 0x9a, 0xc2, 0x91, 0xc5, 0xa6, 0x32, 0xc7, 0x5a, 0xc9, 0x93, 0x47, 0xd0, 0xc9, 0x46, 0x3d,
0x7d, 0x16, 0x0c, 0x65, 0x74, 0x3e, 0x7b, 0xf9, 0xb3, 0x51, 0x5f, 0x52, 0xfe, 0x0c, 0x36, 0xa7, 0x93, 0xa3, 0xa5, 0xc9, 0xaf, 0xac, 0xb1, 0x03, 0xdd, 0x3d, 0x16, 0x0c, 0x65, 0x74, 0x36, 0x7b,
0xca, 0x6f, 0xd1, 0xf4, 0x4b, 0xea, 0x8f, 0xa0, 0x7d, 0xcc, 0x2e, 0x30, 0x1f, 0x88, 0x59, 0x42, 0xf9, 0xb3, 0x51, 0x5f, 0x52, 0xfe, 0x1c, 0x6e, 0x4d, 0x95, 0xdf, 0x00, 0x2a, 0x94, 0xd4, 0x1f,
0xc2, 0xcd, 0x13, 0x78, 0x2a, 0x32, 0x30, 0x53, 0x4d, 0x9f, 0x27, 0x43, 0x26, 0x44, 0x14, 0x9f, 0x42, 0xfb, 0x88, 0x9d, 0x63, 0x3e, 0x10, 0xb3, 0x84, 0x84, 0x9b, 0x27, 0xf0, 0x54, 0x64, 0x60,
0x5a, 0x35, 0x52, 0xcb, 0x3f, 0x87, 0x5b, 0xa9, 0x06, 0xe2, 0xa4, 0xeb, 0x84, 0xd3, 0xb7, 0xaf, 0x66, 0xa1, 0x3e, 0x4f, 0x86, 0x4c, 0x88, 0x28, 0x3e, 0xb1, 0x6a, 0xa4, 0x96, 0x7f, 0x09, 0x37,
0xf6, 0x65, 0x2a, 0xdc, 0x4e, 0x27, 0x2c, 0x82, 0xdd, 0x34, 0x3f, 0x0f, 0x96, 0x1d, 0xff, 0x23, 0x52, 0x0d, 0x44, 0x57, 0x57, 0x09, 0xa7, 0x6f, 0x5f, 0xed, 0xcb, 0x54, 0xb8, 0x9d, 0xce, 0x65,
0xdc, 0x99, 0x93, 0xf5, 0xef, 0x59, 0x16, 0x34, 0xd4, 0x2a, 0xcc, 0x7a, 0xe4, 0x8e, 0xb1, 0x68, 0x04, 0xbb, 0x69, 0x7e, 0x8a, 0x2c, 0x3b, 0xfe, 0x27, 0xb8, 0x3d, 0x27, 0xeb, 0xdf, 0xb3, 0x2c,
0x9b, 0x00, 0xcb, 0xce, 0x1d, 0xce, 0xd6, 0x75, 0xf1, 0x4e, 0x45, 0xf9, 0x5b, 0x70, 0xaa, 0xa6, 0x68, 0x80, 0x56, 0x98, 0x10, 0xc9, 0x6d, 0x63, 0xd1, 0x36, 0x37, 0x96, 0x9d, 0x3b, 0x98, 0xad,
0x18, 0x8d, 0x4a, 0xae, 0x99, 0x71, 0x5c, 0x5b, 0x33, 0x31, 0x9d, 0x67, 0x66, 0xbe, 0xd0, 0x1e, 0xeb, 0xe2, 0x9d, 0x8a, 0xf2, 0x77, 0xe0, 0x54, 0xcd, 0x3e, 0x1a, 0x95, 0x5c, 0x31, 0x19, 0xb9,
0x56, 0x8d, 0x1d, 0xe5, 0xd7, 0xf2, 0x81, 0x1c, 0xb3, 0x8b, 0x72, 0x89, 0x7b, 0x2b, 0xa7, 0x36, 0xb6, 0x66, 0x62, 0x3a, 0xcf, 0xcc, 0x54, 0xa2, 0x3d, 0xac, 0x1a, 0x56, 0xca, 0xaf, 0xe5, 0x03,
0x66, 0x9c, 0xf2, 0xb4, 0x57, 0x47, 0xd0, 0xb5, 0x96, 0x0a, 0x8f, 0xf4, 0xb0, 0x45, 0xcf, 0x19, 0x39, 0x62, 0xe7, 0xe5, 0x12, 0xf7, 0x46, 0x4e, 0x6d, 0xcc, 0x38, 0xe5, 0x69, 0xaf, 0x0e, 0xa1,
0x4a, 0xca, 0xee, 0x7d, 0x0e, 0x4e, 0x45, 0xfe, 0x54, 0x75, 0xd8, 0xa2, 0x81, 0xa7, 0x37, 0xff, 0x6b, 0x2d, 0x15, 0x1e, 0xe9, 0x61, 0x8b, 0x9e, 0x33, 0xca, 0x94, 0xdd, 0xfb, 0x02, 0x9c, 0x8a,
0xd0, 0xc2, 0x7f, 0x2f, 0xfe, 0x1b, 0x00, 0x00, 0xff, 0xff, 0x6f, 0x39, 0x5a, 0x2e, 0xec, 0x18, 0xfc, 0xa9, 0xea, 0xb0, 0x45, 0x03, 0x4f, 0xae, 0xff, 0xb1, 0x85, 0xff, 0x79, 0xfc, 0x2f, 0x00,
0x00, 0x00, 0x00, 0xff, 0xff, 0x50, 0xdd, 0x35, 0xd0, 0x22, 0x19, 0x00, 0x00,
} }
// Reference imports to suppress errors if they are not otherwise used. // Reference imports to suppress errors if they are not otherwise used.
@ -1975,6 +1975,7 @@ type StorageAuthorityClient interface {
GetPendingAuthorization(ctx context.Context, in *GetPendingAuthorizationRequest, opts ...grpc.CallOption) (*proto1.Authorization, error) GetPendingAuthorization(ctx context.Context, in *GetPendingAuthorizationRequest, opts ...grpc.CallOption) (*proto1.Authorization, error)
GetValidAuthorizations(ctx context.Context, in *GetValidAuthorizationsRequest, opts ...grpc.CallOption) (*ValidAuthorizations, error) GetValidAuthorizations(ctx context.Context, in *GetValidAuthorizationsRequest, opts ...grpc.CallOption) (*ValidAuthorizations, error)
GetCertificate(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*proto1.Certificate, error) GetCertificate(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*proto1.Certificate, error)
GetPrecertificate(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*proto1.Certificate, error)
GetCertificateStatus(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*CertificateStatus, error) GetCertificateStatus(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*CertificateStatus, error)
CountCertificatesByNames(ctx context.Context, in *CountCertificatesByNamesRequest, opts ...grpc.CallOption) (*CountByNames, error) CountCertificatesByNames(ctx context.Context, in *CountCertificatesByNamesRequest, opts ...grpc.CallOption) (*CountByNames, error)
CountRegistrationsByIP(ctx context.Context, in *CountRegistrationsByIPRequest, opts ...grpc.CallOption) (*Count, error) CountRegistrationsByIP(ctx context.Context, in *CountRegistrationsByIPRequest, opts ...grpc.CallOption) (*Count, error)
@ -2081,6 +2082,15 @@ func (c *storageAuthorityClient) GetCertificate(ctx context.Context, in *Serial,
return out, nil return out, nil
} }
func (c *storageAuthorityClient) GetPrecertificate(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*proto1.Certificate, error) {
out := new(proto1.Certificate)
err := c.cc.Invoke(ctx, "/sa.StorageAuthority/GetPrecertificate", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *storageAuthorityClient) GetCertificateStatus(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*CertificateStatus, error) { func (c *storageAuthorityClient) GetCertificateStatus(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*CertificateStatus, error) {
out := new(CertificateStatus) out := new(CertificateStatus)
err := c.cc.Invoke(ctx, "/sa.StorageAuthority/GetCertificateStatus", in, out, opts...) err := c.cc.Invoke(ctx, "/sa.StorageAuthority/GetCertificateStatus", in, out, opts...)
@ -2441,6 +2451,7 @@ type StorageAuthorityServer interface {
GetPendingAuthorization(context.Context, *GetPendingAuthorizationRequest) (*proto1.Authorization, error) GetPendingAuthorization(context.Context, *GetPendingAuthorizationRequest) (*proto1.Authorization, error)
GetValidAuthorizations(context.Context, *GetValidAuthorizationsRequest) (*ValidAuthorizations, error) GetValidAuthorizations(context.Context, *GetValidAuthorizationsRequest) (*ValidAuthorizations, error)
GetCertificate(context.Context, *Serial) (*proto1.Certificate, error) GetCertificate(context.Context, *Serial) (*proto1.Certificate, error)
GetPrecertificate(context.Context, *Serial) (*proto1.Certificate, error)
GetCertificateStatus(context.Context, *Serial) (*CertificateStatus, error) GetCertificateStatus(context.Context, *Serial) (*CertificateStatus, error)
CountCertificatesByNames(context.Context, *CountCertificatesByNamesRequest) (*CountByNames, error) CountCertificatesByNames(context.Context, *CountCertificatesByNamesRequest) (*CountByNames, error)
CountRegistrationsByIP(context.Context, *CountRegistrationsByIPRequest) (*Count, error) CountRegistrationsByIP(context.Context, *CountRegistrationsByIPRequest) (*Count, error)
@ -2507,6 +2518,9 @@ func (*UnimplementedStorageAuthorityServer) GetValidAuthorizations(ctx context.C
func (*UnimplementedStorageAuthorityServer) GetCertificate(ctx context.Context, req *Serial) (*proto1.Certificate, error) { func (*UnimplementedStorageAuthorityServer) GetCertificate(ctx context.Context, req *Serial) (*proto1.Certificate, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetCertificate not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetCertificate not implemented")
} }
func (*UnimplementedStorageAuthorityServer) GetPrecertificate(ctx context.Context, req *Serial) (*proto1.Certificate, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetPrecertificate not implemented")
}
func (*UnimplementedStorageAuthorityServer) GetCertificateStatus(ctx context.Context, req *Serial) (*CertificateStatus, error) { func (*UnimplementedStorageAuthorityServer) GetCertificateStatus(ctx context.Context, req *Serial) (*CertificateStatus, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetCertificateStatus not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetCertificateStatus not implemented")
} }
@ -2737,6 +2751,24 @@ func _StorageAuthority_GetCertificate_Handler(srv interface{}, ctx context.Conte
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _StorageAuthority_GetPrecertificate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Serial)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StorageAuthorityServer).GetPrecertificate(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/sa.StorageAuthority/GetPrecertificate",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StorageAuthorityServer).GetPrecertificate(ctx, req.(*Serial))
}
return interceptor(ctx, in, info, handler)
}
func _StorageAuthority_GetCertificateStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _StorageAuthority_GetCertificateStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Serial) in := new(Serial)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -3467,6 +3499,10 @@ var _StorageAuthority_serviceDesc = grpc.ServiceDesc{
MethodName: "GetCertificate", MethodName: "GetCertificate",
Handler: _StorageAuthority_GetCertificate_Handler, Handler: _StorageAuthority_GetCertificate_Handler,
}, },
{
MethodName: "GetPrecertificate",
Handler: _StorageAuthority_GetPrecertificate_Handler,
},
{ {
MethodName: "GetCertificateStatus", MethodName: "GetCertificateStatus",
Handler: _StorageAuthority_GetCertificateStatus_Handler, Handler: _StorageAuthority_GetCertificateStatus_Handler,

View File

@ -13,6 +13,7 @@ service StorageAuthority {
rpc GetPendingAuthorization(GetPendingAuthorizationRequest) returns (core.Authorization) {} rpc GetPendingAuthorization(GetPendingAuthorizationRequest) returns (core.Authorization) {}
rpc GetValidAuthorizations(GetValidAuthorizationsRequest) returns (ValidAuthorizations) {} rpc GetValidAuthorizations(GetValidAuthorizationsRequest) returns (ValidAuthorizations) {}
rpc GetCertificate(Serial) returns (core.Certificate) {} rpc GetCertificate(Serial) returns (core.Certificate) {}
rpc GetPrecertificate(Serial) returns (core.Certificate) {}
rpc GetCertificateStatus(Serial) returns (CertificateStatus) {} rpc GetCertificateStatus(Serial) returns (CertificateStatus) {}
rpc CountCertificatesByNames(CountCertificatesByNamesRequest) returns (CountByNames) {} rpc CountCertificatesByNames(CountCertificatesByNamesRequest) returns (CountByNames) {}
rpc CountRegistrationsByIP(CountRegistrationsByIPRequest) returns (Count) {} rpc CountRegistrationsByIP(CountRegistrationsByIPRequest) returns (Count) {}

View File

@ -465,6 +465,25 @@ func (ssa *SQLStorageAuthority) GetCertificate(ctx context.Context, serial strin
return cert, err return cert, err
} }
// GetPrecertificate takes a serial number and returns the corresponding
// precertificate, or error if it does not exist.
func (ssa *SQLStorageAuthority) GetPrecertificate(ctx context.Context, reqSerial *sapb.Serial) (*corepb.Certificate, error) {
if !core.ValidSerial(*reqSerial.Serial) {
return nil,
fmt.Errorf("Invalid precertificate serial %q", *reqSerial.Serial)
}
cert, err := selectPrecertificate(ssa.dbMap.WithContext(ctx), *reqSerial.Serial)
if err == sql.ErrNoRows {
return nil,
berrors.NotFoundError("precertificate with serial %q not found", *reqSerial.Serial)
}
if err != nil {
return nil, err
}
return bgrpc.CertToPB(cert), nil
}
// GetCertificateStatus takes a hexadecimal string representing the full 128-bit serial // GetCertificateStatus takes a hexadecimal string representing the full 128-bit serial
// number of a certificate and returns data about that certificate's current // number of a certificate and returns data about that certificate's current
// validity. // validity.

View File

@ -54,7 +54,8 @@
"HeadNonceStatusOK": true, "HeadNonceStatusOK": true,
"NewAuthorizationSchema": true, "NewAuthorizationSchema": true,
"RemoveWFE2AccountID": true, "RemoveWFE2AccountID": true,
"MandatoryPOSTAsGET": true "MandatoryPOSTAsGET": true,
"PrecertificateRevocation": true
} }
}, },

View File

@ -0,0 +1,93 @@
// +build integration
package integration
import (
"bytes"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
berrors "github.com/letsencrypt/boulder/errors"
)
// ctAddRejectHost adds a domain to all of the CT test server's reject-host
// lists. If this fails the test is aborted with a fatal error.
func ctAddRejectHost(domain string) error {
for _, port := range []int{4500, 4501, 4510, 4511} {
url := fmt.Sprintf("http://boulder:%d/add-reject-host", port)
body := []byte(fmt.Sprintf(`{"host": %q}`, domain))
resp, err := http.Post(url, "", bytes.NewBuffer(body))
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("adding reject host: %d", resp.StatusCode)
}
resp.Body.Close()
}
return nil
}
// ctGetRejections returns a slice of base64 encoded certificates that were
// rejected by the CT test server at the specified port or an error.
func ctGetRejections(port int) ([]string, error) {
url := fmt.Sprintf("http://boulder:%d/get-rejections", port)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(
"getting rejections: status %d", resp.StatusCode)
}
var rejections []string
err = json.NewDecoder(resp.Body).Decode(&rejections)
if err != nil {
return nil, err
}
return rejections, nil
}
// ctFindRejection returns a parsed x509.Certificate matching the given domains
// from the base64 certificates the CT test server rejected. If no rejected
// certificate matching the provided domains is found an error is returned.
func ctFindRejection(port int, domains []string) (*x509.Certificate, error) {
// Parse each rejection cert
var cert *x509.Certificate
rejections, err := ctGetRejections(port)
if err != nil {
return nil, err
}
RejectionLoop:
for _, r := range rejections {
precertDER, err := base64.StdEncoding.DecodeString(r)
if err != nil {
return nil, err
}
c, err := x509.ParseCertificate(precertDER)
if err != nil {
return nil, err
}
// If the cert doesn't have the right number of names it won't be a match.
if len(c.DNSNames) != len(domains) {
continue
}
// If any names don't match, it isn't a match
for i, name := range c.DNSNames {
if name != domains[i] {
continue RejectionLoop
}
}
// It's a match!
cert = c
break
}
if cert == nil {
return nil, berrors.NotFoundError("no matching ct-test-srv rejection found")
}
return cert, nil
}

View File

@ -9,6 +9,7 @@ import (
"crypto/rand" "crypto/rand"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/asn1"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"net/http" "net/http"
@ -17,6 +18,10 @@ import (
"github.com/eggsampler/acme/v2" "github.com/eggsampler/acme/v2"
) )
var (
OIDExtensionCTPoison = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}
)
func random_domain() string { func random_domain() string {
var bytes [3]byte var bytes [3]byte
rand.Read(bytes[:]) rand.Read(bytes[:])
@ -80,11 +85,15 @@ type issuanceResult struct {
certs []*x509.Certificate certs []*x509.Certificate
} }
func authAndIssue(domains []string) (*issuanceResult, error) { func authAndIssue(c *client, csrKey *ecdsa.PrivateKey, domains []string) (*issuanceResult, error) {
c, err := makeClient() var err error
if err != nil { if c == nil {
return nil, err c, err = makeClient()
if err != nil {
return nil, err
}
} }
var ids []acme.Identifier var ids []acme.Identifier
for _, domain := range domains { for _, domain := range domains {
ids = append(ids, acme.Identifier{Type: "dns", Value: domain}) ids = append(ids, acme.Identifier{Type: "dns", Value: domain})
@ -116,7 +125,7 @@ func authAndIssue(domains []string) (*issuanceResult, error) {
} }
} }
csr, err := makeCSR(domains) csr, err := makeCSR(csrKey, domains)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -132,19 +141,22 @@ func authAndIssue(domains []string) (*issuanceResult, error) {
return &issuanceResult{order, certs}, nil return &issuanceResult{order, certs}, nil
} }
func makeCSR(domains []string) (*x509.CertificateRequest, error) { func makeCSR(k *ecdsa.PrivateKey, domains []string) (*x509.CertificateRequest, error) {
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) var err error
if err != nil { if k == nil {
return nil, fmt.Errorf("generating certificate key: %s", err) k, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("generating certificate key: %s", err)
}
} }
csrDer, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ csrDer, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
SignatureAlgorithm: x509.ECDSAWithSHA256, SignatureAlgorithm: x509.ECDSAWithSHA256,
PublicKeyAlgorithm: x509.ECDSA, PublicKeyAlgorithm: x509.ECDSA,
PublicKey: certKey.Public(), PublicKey: k.Public(),
Subject: pkix.Name{CommonName: domains[0]}, Subject: pkix.Name{CommonName: domains[0]},
DNSNames: domains, DNSNames: domains,
}, certKey) }, k)
if err != nil { if err != nil {
return nil, fmt.Errorf("making csr: %s", err) return nil, fmt.Errorf("making csr: %s", err)
} }

View File

@ -3,16 +3,13 @@
package integration package integration
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os" "os"
"strings" "strings"
"testing" "testing"
ocsp_helper "github.com/letsencrypt/boulder/test/ocsp/helper" ocsp_helper "github.com/letsencrypt/boulder/test/ocsp/helper"
"golang.org/x/crypto/ocsp"
) )
func TestPrecertificateOCSP(t *testing.T) { func TestPrecertificateOCSP(t *testing.T) {
@ -21,21 +18,13 @@ func TestPrecertificateOCSP(t *testing.T) {
return return
} }
domain := random_domain() domain := random_domain()
for _, port := range []int{4500, 4501, 4510, 4511} { err := ctAddRejectHost(domain)
url := fmt.Sprintf("http://boulder:%d/add-reject-host", port) if err != nil {
body := []byte(fmt.Sprintf(`{"host": "%s"}`, domain)) t.Fatalf("adding ct-test-srv reject host: %s", err)
resp, err := http.Post(url, "", bytes.NewBuffer(body))
if err != nil {
t.Fatalf("adding reject host: %s", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("adding reject host: %d", resp.StatusCode)
}
resp.Body.Close()
} }
os.Setenv("DIRECTORY", "http://boulder:4001/directory") os.Setenv("DIRECTORY", "http://boulder:4001/directory")
_, err := authAndIssue([]string{domain}) _, err = authAndIssue(nil, nil, []string{domain})
if err != nil { if err != nil {
if strings.Contains(err.Error(), "urn:ietf:params:acme:error:serverInternal") && if strings.Contains(err.Error(), "urn:ietf:params:acme:error:serverInternal") &&
strings.Contains(err.Error(), "SCT embedding") { strings.Contains(err.Error(), "SCT embedding") {
@ -47,26 +36,16 @@ func TestPrecertificateOCSP(t *testing.T) {
t.Fatal("expected error issuing for domain rejected by CT servers; got none") t.Fatal("expected error issuing for domain rejected by CT servers; got none")
} }
resp, err := http.Get("http://boulder:4500/get-rejections") rejections, err := ctGetRejections(4500)
if err != nil { if err != nil {
t.Fatalf("getting rejections: %s", err) t.Fatalf("getting ct-test-srv rejections: %s", err)
} }
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("getting rejections: status %d", resp.StatusCode)
}
var rejections []string
err = json.NewDecoder(resp.Body).Decode(&rejections)
if err != nil {
t.Fatalf("parsing rejections: %s", err)
}
for _, r := range rejections { for _, r := range rejections {
rejectedCertBytes, err := base64.StdEncoding.DecodeString(r) rejectedCertBytes, err := base64.StdEncoding.DecodeString(r)
if err != nil { if err != nil {
t.Fatalf("decoding rejected cert: %s", err) t.Fatalf("decoding rejected cert: %s", err)
} }
_, err = ocsp_helper.ReqDER(rejectedCertBytes) _, err = ocsp_helper.ReqDER(rejectedCertBytes, ocsp.Good)
if err != nil { if err != nil {
t.Errorf("requesting OCSP for rejected precertificate: %s", err) t.Errorf("requesting OCSP for rejected precertificate: %s", err)
} }

View File

@ -0,0 +1,136 @@
// +build integration
package integration
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"os"
"strings"
"testing"
"github.com/letsencrypt/boulder/test"
ocsp_helper "github.com/letsencrypt/boulder/test/ocsp/helper"
"golang.org/x/crypto/ocsp"
)
// isPrecert returns true if the provided cert has an extension with the OID
// equal to OIDExtensionCTPoison.
func isPrecert(cert *x509.Certificate) bool {
for _, ext := range cert.Extensions {
if ext.Id.Equal(OIDExtensionCTPoison) {
return true
}
}
return false
}
// TestPrecertificateRevocation tests that a precertificate without a matching
// certificate can be revoked using all of the available RFC 8555 revocation
// authentication mechansims.
func TestPrecertificateRevocation(t *testing.T) {
// This test is gated on the PrecertificateRevocation feature flag.
if !strings.Contains(os.Getenv("BOULDER_CONFIG_DIR"), "test/config-next") {
return
}
// Create a base account to use for revocation tests.
os.Setenv("DIRECTORY", "http://boulder:4001/directory")
c, err := makeClient()
test.AssertNotError(t, err, "creating acme client")
// Create a specific key for CSRs so that it is possible to test revocation
// with the cert key.
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "creating random cert key")
// Create a second account to test revocation with an equally authorized account
otherAccount, err := makeClient()
test.AssertNotError(t, err, "creating second acme client")
// Preauthorize a specific domain with the other account before it has been
// added to the ct-test-srv reject list.
preAuthDomain := random_domain()
_, err = authAndIssue(otherAccount, nil, []string{preAuthDomain})
test.AssertNotError(t, err, "preauthorizing second acme client")
testCases := []struct {
name string
domain string
revokeClient *client
revokeKey crypto.Signer
}{
{
name: "revocation by certificate key",
revokeKey: certKey,
},
{
name: "revocation by owner account key",
revokeKey: c.Account.PrivateKey,
},
{
name: "equivalently authorized account key",
revokeClient: otherAccount,
revokeKey: otherAccount.Account.PrivateKey,
domain: preAuthDomain,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// If the test case didn't specify a domain make one up randomly
if tc.domain == "" {
tc.domain = random_domain()
}
// If the test case didn't specify a different client to use for
// revocation use c.
if tc.revokeClient == nil {
tc.revokeClient = c
}
// Make sure the ct-test-srv will reject issuance for the domain
err := ctAddRejectHost(tc.domain)
test.AssertNotError(t, err, "adding ct-test-srv reject host")
// Issue a certificate for the name using the `c` client. It should fail
// because not enough SCTs can be collected, leaving a precert without
// a matching final cert.
_, err = authAndIssue(c, certKey, []string{tc.domain})
test.AssertError(t, err, "expected error from authAndIssue, was nil")
if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:serverInternal") ||
!strings.Contains(err.Error(), "SCT embedding") {
t.Fatal(err)
}
// Find the rejected precert from the first ct-test-srv for the domains we
// attempted to issue for.
cert, err := ctFindRejection(4500, []string{tc.domain})
test.AssertNotError(t, err, "finding rejected precertificate")
// To be confident that we're testing the right thing also verify that the
// rejection is a poisoned precertificate.
if !isPrecert(cert) {
t.Fatal("precert was missing poison extension")
}
// To start with the precertificate should have a Good OCSP response.
_, err = ocsp_helper.ReqDER(cert.Raw, ocsp.Good)
test.AssertNotError(t, err, "requesting OCSP for precert")
// Revoke the precertificate using the specified key and client
err = tc.revokeClient.RevokeCertificate(
tc.revokeClient.Account,
cert,
tc.revokeKey,
ocsp.KeyCompromise)
test.AssertNotError(t, err, "revoking precert")
// Check the OCSP response for the precertificate again. It should now be
// revoked.
_, err = ocsp_helper.ReqDER(cert.Raw, ocsp.Revoked)
test.AssertNotError(t, err, "requesting OCSP for revoked precert")
})
}
}

View File

@ -96,10 +96,10 @@ func Req(fileName string) (*ocsp.Response, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return ReqDER(contents) return ReqDER(contents, *expectStatus)
} }
func ReqDER(der []byte) (*ocsp.Response, error) { func ReqDER(der []byte, expectStatus int) (*ocsp.Response, error) {
cert, err := parse(der) cert, err := parse(der)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing certificate: %s", err) return nil, fmt.Errorf("parsing certificate: %s", err)
@ -150,7 +150,7 @@ func ReqDER(der []byte) (*ocsp.Response, error) {
if len(respBytes) == 0 { if len(respBytes) == 0 {
return nil, fmt.Errorf("empty reponse body") return nil, fmt.Errorf("empty reponse body")
} }
return parseAndPrint(respBytes, cert, issuer) return parseAndPrint(respBytes, cert, issuer, expectStatus)
} }
func sendHTTPRequest(req []byte, ocspURL *url.URL) (*http.Response, error) { func sendHTTPRequest(req []byte, ocspURL *url.URL) (*http.Response, error) {
@ -194,14 +194,14 @@ func getOCSPURL(cert *x509.Certificate) (*url.URL, error) {
return ocspURL, nil return ocspURL, nil
} }
func parseAndPrint(respBytes []byte, cert, issuer *x509.Certificate) (*ocsp.Response, error) { func parseAndPrint(respBytes []byte, cert, issuer *x509.Certificate, expectStatus int) (*ocsp.Response, error) {
fmt.Printf("\nDecoding body: %s\n", base64.StdEncoding.EncodeToString(respBytes)) fmt.Printf("\nDecoding body: %s\n", base64.StdEncoding.EncodeToString(respBytes))
resp, err := ocsp.ParseResponseForCert(respBytes, cert, issuer) resp, err := ocsp.ParseResponseForCert(respBytes, cert, issuer)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing response: %s", err) return nil, fmt.Errorf("parsing response: %s", err)
} }
if resp.Status != *expectStatus { if resp.Status != expectStatus {
return nil, fmt.Errorf("wrong CertStatus %d, expected %d", resp.Status, *expectStatus) return nil, fmt.Errorf("wrong CertStatus %d, expected %d", resp.Status, expectStatus)
} }
timeTilExpiry := time.Until(resp.NextUpdate) timeTilExpiry := time.Until(resp.NextUpdate)
tooSoonDuration := time.Duration(*tooSoon) * time.Hour tooSoonDuration := time.Duration(*tooSoon) * time.Hour

View File

@ -32,7 +32,7 @@ GRANT SELECT,INSERT,DELETE ON orderFqdnSets TO 'sa'@'localhost';
GRANT SELECT,INSERT,UPDATE ON authz2 TO 'sa'@'localhost'; GRANT SELECT,INSERT,UPDATE ON authz2 TO 'sa'@'localhost';
GRANT SELECT,INSERT ON orderToAuthz2 TO 'sa'@'localhost'; GRANT SELECT,INSERT ON orderToAuthz2 TO 'sa'@'localhost';
GRANT INSERT ON serials TO 'sa'@'localhost'; GRANT INSERT ON serials TO 'sa'@'localhost';
GRANT INSERT ON precertificates TO 'sa'@'localhost'; GRANT SELECT,INSERT ON precertificates TO 'sa'@'localhost';
-- OCSP Responder -- OCSP Responder
GRANT SELECT ON certificateStatus TO 'ocsp_resp'@'localhost'; GRANT SELECT ON certificateStatus TO 'ocsp_resp'@'localhost';

View File

@ -83,6 +83,11 @@ type WebFrontEndImpl struct {
// sorted from leaf to root // sorted from leaf to root
certificateChains map[string][]byte certificateChains map[string][]byte
// issuerCertificates is a slice of known issuer certificates built with the
// first entry from each of the certificateChains. These certificates are used
// to verify the signature of certificates provided in revocation requests.
issuerCertificates []*x509.Certificate
// URL to the current subscriber agreement (should contain some version identifier) // URL to the current subscriber agreement (should contain some version identifier)
SubscriberAgreementURL string SubscriberAgreementURL string
@ -121,6 +126,7 @@ func NewWebFrontEndImpl(
clk clock.Clock, clk clock.Clock,
keyPolicy goodkey.KeyPolicy, keyPolicy goodkey.KeyPolicy,
certificateChains map[string][]byte, certificateChains map[string][]byte,
issuerCertificates []*x509.Certificate,
remoteNonceService noncepb.NonceServiceClient, remoteNonceService noncepb.NonceServiceClient,
noncePrefixMap map[string]noncepb.NonceServiceClient, noncePrefixMap map[string]noncepb.NonceServiceClient,
logger blog.Logger, logger blog.Logger,
@ -130,6 +136,7 @@ func NewWebFrontEndImpl(
clk: clk, clk: clk,
keyPolicy: keyPolicy, keyPolicy: keyPolicy,
certificateChains: certificateChains, certificateChains: certificateChains,
issuerCertificates: issuerCertificates,
stats: initStats(scope), stats: initStats(scope),
scope: scope, scope: scope,
remoteNonceService: remoteNonceService, remoteNonceService: remoteNonceService,
@ -695,22 +702,65 @@ func (wfe *WebFrontEndImpl) processRevocation(
// Compute and record the serial number of the provided certificate // Compute and record the serial number of the provided certificate
serial := core.SerialToString(providedCert.SerialNumber) serial := core.SerialToString(providedCert.SerialNumber)
logEvent.Extra["ProvidedCertificateSerial"] = serial logEvent.Extra["ProvidedCertificateSerial"] = serial
notFoundProb := probs.NotFound("No such certificate")
// Lookup the certificate by the serial. If the certificate wasn't found, or var certDER []byte
// it wasn't a byte-for-byte match to the certificate requested for // If the PrecertificateRevocation feature flag is enabled that means we won't
// revocation, return an error // do a byte-for-byte comparison of the providedCert against the stored cert
cert, err := wfe.SA.GetCertificate(ctx, serial) // returned by SA.GetCertificate because a precert will always fail this
if err != nil || !bytes.Equal(cert.DER, revokeRequest.CertificateDER) { // check. Instead, perform a signature validation of the providedCert using
return probs.NotFound("No such certificate") // the known issuer public keys. If the providedCert signature can not be
// validated with any of the known issuers return a not-found error.
if features.Enabled(features.PrecertificateRevocation) {
// If no issuerCertificates are initialized but the PrecertificateRevocation
// feature flag is enabled then return a runtime server internal error
// rather than fail open.
if len(wfe.issuerCertificates) == 0 {
return probs.ServerInternal(
"unable to verify provided certificate, empty issuerCertificates")
}
// Try to validate the signature on the provided cert using each of the
// known issuer certificates. This is O(n) but we always expect to have
// a small number of configured issuers.
var validIssuerSignature bool
for _, issuer := range wfe.issuerCertificates {
if err := providedCert.CheckSignatureFrom(issuer); err == nil {
validIssuerSignature = true
break
}
}
// If none of the issuers validate the signature on the provided cert then
// return an error.
if !validIssuerSignature {
return notFoundProb
}
// If the signature validates we can use the provided cert's DER for
// revocation safely.
certDER = providedCert.Raw
} else {
// When the precertificate revocation feature flag isn't enabled try to find
// a finalized cert in the DB matching the serial. If there is one, it needs
// to be a byte-for-byte match with the requested cert.
cert, err := wfe.SA.GetCertificate(ctx, serial)
if err != nil {
return notFoundProb
}
// If the certificate in the DB isn't a byte for byte match, return a problem
if !bytes.Equal(cert.DER, revokeRequest.CertificateDER) {
return notFoundProb
}
certDER = cert.DER
} }
// Parse the certificate into memory // Parse the certificate into memory
parsedCertificate, err := x509.ParseCertificate(cert.DER) parsedCertificate, err := x509.ParseCertificate(certDER)
if err != nil { if err != nil {
// InternalServerError because cert.DER came from our own DB. // InternalServerError because certDER came from our own DB, or was
// confirmed issued by one of our own issuers.
return probs.ServerInternal("invalid parse of stored certificate") return probs.ServerInternal("invalid parse of stored certificate")
} }
logEvent.Extra["RetrievedCertificateSerial"] = core.SerialToString(parsedCertificate.SerialNumber) logEvent.Extra["RetrievedCertificateSerial"] = serial
logEvent.Extra["RetrievedCertificateDNSNames"] = parsedCertificate.DNSNames logEvent.Extra["RetrievedCertificateDNSNames"] = parsedCertificate.DNSNames
if parsedCertificate.NotAfter.Before(wfe.clk.Now()) { if parsedCertificate.NotAfter.Before(wfe.clk.Now()) {
@ -779,21 +829,53 @@ func (wfe *WebFrontEndImpl) revokeCertByKeyID(
// certificate by checking that the account has valid authorizations for all // certificate by checking that the account has valid authorizations for all
// of the names in the certificate or was the issuing account // of the names in the certificate or was the issuing account
authorizedToRevoke := func(parsedCertificate *x509.Certificate) *probs.ProblemDetails { authorizedToRevoke := func(parsedCertificate *x509.Certificate) *probs.ProblemDetails {
cert, err := wfe.SA.GetCertificate(ctx, core.SerialToString(parsedCertificate.SerialNumber)) // Try to find a stored final certificate for the serial number
if err != nil { serial := core.SerialToString(parsedCertificate.SerialNumber)
cert, err := wfe.SA.GetCertificate(ctx, serial)
if berrors.Is(err, berrors.NotFound) && features.Enabled(features.PrecertificateRevocation) {
// If there was an error, it was a not found error, and the precertificate
// revocation feature is enabled, then try to find a stored precert.
pbCert, err := wfe.SA.GetPrecertificate(ctx,
&sapb.Serial{Serial: &serial})
if berrors.Is(err, berrors.NotFound) {
// If looking up a precert also returned a not found error then return
// a not found problem.
return probs.NotFound("No such certificate")
} else if err != nil {
// If there was any other error looking up the precert then return
// a server internal problem.
return probs.ServerInternal("Failed to retrieve certificate")
}
cert, err = bgrpc.PBToCert(pbCert)
if err != nil {
return probs.ServerInternal("Failed to unmarshal protobuf certificate")
}
} else if berrors.Is(err, berrors.NotFound) {
// Otherwise if the err was not nil and was a not found error but the
// precertificate revocation feature flag is not enabled, return a not
// found error.
return probs.NotFound("No such certificate")
} else if err != nil {
// Otherwise if the err was not nil and not a not found error, return
// a server internal problem.
return probs.ServerInternal("Failed to retrieve certificate") return probs.ServerInternal("Failed to retrieve certificate")
} }
// If the cert/precert is owned by the requester then return nil, it is an
// authorized revocation.
if cert.RegistrationID == acct.ID { if cert.RegistrationID == acct.ID {
return nil return nil
} }
// Otherwise check if the account, while not the owner, has equivalent authorizations
valid, err := wfe.acctHoldsAuthorizations(ctx, acct.ID, parsedCertificate.DNSNames) valid, err := wfe.acctHoldsAuthorizations(ctx, acct.ID, parsedCertificate.DNSNames)
if err != nil { if err != nil {
return probs.ServerInternal("Failed to retrieve authorizations for names in certificate") return probs.ServerInternal("Failed to retrieve authorizations for names in certificate")
} }
// If it doesn't, return an unauthorized problem.
if !valid { if !valid {
return probs.Unauthorized( return probs.Unauthorized(
"The key ID specified in the revocation request does not hold valid authorizations for all names in the certificate to be revoked") "The key ID specified in the revocation request does not hold valid authorizations for all names in the certificate to be revoked")
} }
// If it does, return nil. It is an an authorized revocation.
return nil return nil
} }
return wfe.processRevocation(ctx, jwsBody, acct.ID, authorizedToRevoke, request, logEvent) return wfe.processRevocation(ctx, jwsBody, acct.ID, authorizedToRevoke, request, logEvent)

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"crypto" "crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
@ -352,12 +353,18 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock) {
chainPEM, err := ioutil.ReadFile("../test/test-ca2.pem") chainPEM, err := ioutil.ReadFile("../test/test-ca2.pem")
test.AssertNotError(t, err, "Unable to read ../test/test-ca2.pem") test.AssertNotError(t, err, "Unable to read ../test/test-ca2.pem")
chainDER, _ := pem.Decode(chainPEM)
certChains := map[string][]byte{ certChains := map[string][]byte{
"http://localhost:4000/acme/issuer-cert": append([]byte{'\n'}, chainPEM...), "http://localhost:4000/acme/issuer-cert": append([]byte{'\n'}, chainPEM...),
} }
issuerCert, err := x509.ParseCertificate(chainDER.Bytes)
test.AssertNotError(t, err, "Unable to parse issuer cert")
issuerCertificates := []*x509.Certificate{
issuerCert,
}
wfe, err := NewWebFrontEndImpl(stats, fc, testKeyPolicy, certChains, nil, nil, blog.NewMock()) wfe, err := NewWebFrontEndImpl(stats, fc, testKeyPolicy, certChains, issuerCertificates, nil, nil, blog.NewMock())
test.AssertNotError(t, err, "Unable to create WFE") test.AssertNotError(t, err, "Unable to create WFE")
wfe.SubscriberAgreementURL = agreementURL wfe.SubscriberAgreementURL = agreementURL
@ -2542,11 +2549,15 @@ func makeRevokeRequestJSON(reason *revocation.Reason) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return makeRevokeRequestJSONForCert(certBlock.Bytes, reason)
}
func makeRevokeRequestJSONForCert(der []byte, reason *revocation.Reason) ([]byte, error) {
revokeRequest := struct { revokeRequest := struct {
CertificateDER core.JSONBuffer `json:"certificate"` CertificateDER core.JSONBuffer `json:"certificate"`
Reason *revocation.Reason `json:"reason"` Reason *revocation.Reason `json:"reason"`
}{ }{
CertificateDER: certBlock.Bytes, CertificateDER: der,
Reason: reason, Reason: reason,
} }
revokeRequestJSON, err := json.Marshal(revokeRequest) revokeRequestJSON, err := json.Marshal(revokeRequest)
@ -2591,6 +2602,60 @@ func TestRevokeCertificateCertKey(t *testing.T) {
test.AssertEquals(t, responseWriter.Body.String(), "") test.AssertEquals(t, responseWriter.Body.String(), "")
} }
// Valid revocation request for existing, non-revoked cert, signed with cert
// key, precertificate revocation feature flag enabled.
func TestRevokePreCertificateFeatureEnabled(t *testing.T) {
_ = features.Set(map[string]bool{"PrecertificateRevocation": true})
defer features.Reset()
wfe, fc := setupWFE(t)
wfe.SA = &mockSANoSuchRegistration{mocks.NewStorageAuthority(fc)}
responseWriter := httptest.NewRecorder()
keyPemBytes, err := ioutil.ReadFile("test/238.key")
test.AssertNotError(t, err, "Failed to load key")
key := loadKey(t, keyPemBytes)
revokeRequestJSON, err := makeRevokeRequestJSON(nil)
test.AssertNotError(t, err, "Failed to make revokeRequestJSON")
_, _, jwsBody := signRequestEmbed(t,
key, "http://localhost/revoke-cert", string(revokeRequestJSON), wfe.nonceService)
// Revoking a certificate that was issued by a known issuer should work
wfe.RevokeCertificate(ctx, newRequestEvent(), responseWriter,
makePostRequestWithPath("revoke-cert", jwsBody))
test.AssertEquals(t, responseWriter.Code, 200)
test.AssertEquals(t, responseWriter.Body.String(), "")
// Make a self-signed junk certificate
k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "unexpected error making random private key")
// Use a known serial from the mocks/mocks.go GetCertificate mock. This
// shouldn't matter for the precertificate revocation feature flag flow but
// will ensure we don't get a 404 response from the feature flag being handled
// incorrectly and the GetCertificate() mock not knowing the serial.
knownSerial, err := core.StringToSerial("0000000000000000000000000000000000ee")
test.AssertNotError(t, err, "Unexpected error converting known serial to bigint")
template := &x509.Certificate{
SerialNumber: knownSerial,
}
certDER, err := x509.CreateCertificate(rand.Reader, template, template, k.Public(), k)
test.AssertNotError(t, err, "Unexpected error creating self-signed junk cert")
revokeRequestJSON, err = makeRevokeRequestJSONForCert(certDER, nil)
test.AssertNotError(t, err, "Failed to make revokeRequestJSON for certDER")
// Revoking a certificate that wasn't signed by a known issuer should fail
responseWriter = httptest.NewRecorder()
_, _, jwsBody = signRequestEmbed(t,
key, "http://localhost/revoke-cert", string(revokeRequestJSON), wfe.nonceService)
wfe.RevokeCertificate(ctx, newRequestEvent(), responseWriter,
makePostRequestWithPath("revoke-cert", jwsBody))
// It should result in a 404 response with a problem body
test.AssertEquals(t, responseWriter.Code, 404)
test.AssertEquals(t, responseWriter.Body.String(), "{\n \"type\": \"urn:ietf:params:acme:error:malformed\",\n \"detail\": \"No such certificate\",\n \"status\": 404\n}")
}
func TestRevokeCertificateReasons(t *testing.T) { func TestRevokeCertificateReasons(t *testing.T) {
wfe, fc := setupWFE(t) wfe, fc := setupWFE(t)
ra := wfe.RA.(*MockRegistrationAuthority) ra := wfe.RA.(*MockRegistrationAuthority)