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:
parent
76afea15d8
commit
1cd9733c24
|
@ -94,19 +94,19 @@ type config struct {
|
|||
// loadCertificateFile loads a PEM certificate from the certFile provided. It
|
||||
// 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
|
||||
// requirements the PEM bytes from the file are returned, otherwise an error is
|
||||
// returned. If the PEM contents of a certFile do not have a trailing newline
|
||||
// one is added.
|
||||
func loadCertificateFile(aiaIssuerURL, certFile string) ([]byte, error) {
|
||||
// requirements the PEM bytes from the file are returned along with the parsed
|
||||
// certificate, otherwise an error is returned. If the PEM contents of
|
||||
// a certFile do not have a trailing newline one is added.
|
||||
func loadCertificateFile(aiaIssuerURL, certFile string) ([]byte, *x509.Certificate, error) {
|
||||
pemBytes, err := ioutil.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
return nil, nil, fmt.Errorf(
|
||||
"CertificateChain entry for AIA issuer url %q has an "+
|
||||
"invalid chain file: %q - error reading contents: %s",
|
||||
aiaIssuerURL, certFile, err)
|
||||
}
|
||||
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 "+
|
||||
"invalid chain file: %q - contents had CRLF line endings",
|
||||
aiaIssuerURL, certFile)
|
||||
|
@ -114,22 +114,23 @@ func loadCertificateFile(aiaIssuerURL, certFile string) ([]byte, error) {
|
|||
// Try to decode the contents as PEM
|
||||
certBlock, rest := pem.Decode(pemBytes)
|
||||
if certBlock == nil {
|
||||
return nil, fmt.Errorf(
|
||||
return nil, nil, fmt.Errorf(
|
||||
"CertificateChain entry for AIA issuer url %q has an "+
|
||||
"invalid chain file: %q - contents did not decode as PEM",
|
||||
aiaIssuerURL, certFile)
|
||||
}
|
||||
// The PEM contents must be a CERTIFICATE
|
||||
if certBlock.Type != "CERTIFICATE" {
|
||||
return nil, fmt.Errorf(
|
||||
return nil, nil, fmt.Errorf(
|
||||
"CertificateChain entry for AIA issuer url %q has an "+
|
||||
"invalid chain file: %q - PEM block type incorrect, found "+
|
||||
"%q, expected \"CERTIFICATE\"",
|
||||
aiaIssuerURL, certFile, certBlock.Type)
|
||||
}
|
||||
// The PEM Certificate must successfully parse
|
||||
if _, err := x509.ParseCertificate(certBlock.Bytes); err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
var cert *x509.Certificate
|
||||
if cert, err = x509.ParseCertificate(certBlock.Bytes); err != nil {
|
||||
return nil, nil, fmt.Errorf(
|
||||
"CertificateChain entry for AIA issuer url %q has an "+
|
||||
"invalid chain file: %q - certificate bytes failed to parse: %s",
|
||||
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
|
||||
// leftover bytes will end up in a served certificate chain.
|
||||
if len(rest) != 0 {
|
||||
return nil, fmt.Errorf(
|
||||
return nil, nil, fmt.Errorf(
|
||||
"CertificateChain entry for AIA issuer url %q has an "+
|
||||
"invalid chain file: %q - PEM contents had unused remainder "+
|
||||
"input (%d bytes)",
|
||||
|
@ -147,16 +148,19 @@ func loadCertificateFile(aiaIssuerURL, certFile string) ([]byte, error) {
|
|||
if pemBytes[len(pemBytes)-1] != '\n' {
|
||||
pemBytes = append(pemBytes, '\n')
|
||||
}
|
||||
return pemBytes, nil
|
||||
return pemBytes, cert, nil
|
||||
}
|
||||
|
||||
// loadCertificateChains processes the provided chainConfig of AIA Issuer URLs
|
||||
// and cert filenames. For each AIA issuer URL all of its cert filenames are
|
||||
// read, validated as PEM certificates, and concatenated together separated by
|
||||
// newlines. The combined PEM certificate chain contents for each are returned
|
||||
// in the results map, keyed by the AIA Issuer URL.
|
||||
func loadCertificateChains(chainConfig map[string][]string) (map[string][]byte, error) {
|
||||
// in the results map, keyed by the AIA Issuer URL. Additionally the first
|
||||
// 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))
|
||||
var issuerCerts []*x509.Certificate
|
||||
|
||||
// For each AIA Issuer URL we need to read the chain cert files
|
||||
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
|
||||
if len(certFiles) == 0 {
|
||||
return nil, fmt.Errorf(
|
||||
return nil, nil, fmt.Errorf(
|
||||
"CertificateChain entry for AIA issuer url %q has no chain "+
|
||||
"file names configured",
|
||||
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
|
||||
// configuration
|
||||
for _, c := range certFiles {
|
||||
for i, c := range certFiles {
|
||||
// Prepend a newline before each chain entry
|
||||
buffer.Write([]byte("\n"))
|
||||
|
||||
// Read and validate the chain file contents
|
||||
pemBytes, err := loadCertificateFile(aiaIssuerURL, c)
|
||||
pemBytes, cert, err := loadCertificateFile(aiaIssuerURL, c)
|
||||
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
|
||||
|
@ -189,7 +198,8 @@ func loadCertificateChains(chainConfig map[string][]string) (map[string][]byte,
|
|||
// Save the full PEM chain contents
|
||||
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) {
|
||||
|
@ -232,7 +242,7 @@ func main() {
|
|||
err := cmd.ReadConfigFile(*configFile, &c)
|
||||
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")
|
||||
|
||||
err = features.Set(c.WFE.Features)
|
||||
|
@ -248,7 +258,7 @@ func main() {
|
|||
kp, err := goodkey.NewKeyPolicy("", c.WFE.BlockedKeyFile)
|
||||
cmd.FailOnError(err, "Unable to create key policy")
|
||||
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")
|
||||
wfe.RA = rac
|
||||
wfe.SA = sac
|
||||
|
|
|
@ -43,23 +43,20 @@ func TestLoadCertificateChains(t *testing.T) {
|
|||
test.AssertNotError(t, err, "ioutil.WriteFile failed")
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Input map[string][]string
|
||||
ExpectedResult map[string][]byte
|
||||
ExpectedError error
|
||||
Name string
|
||||
Input map[string][]string
|
||||
ExpectedMap map[string][]byte
|
||||
ExpectedError error
|
||||
}{
|
||||
{
|
||||
Name: "No input",
|
||||
Input: nil,
|
||||
ExpectedResult: nil,
|
||||
ExpectedError: nil,
|
||||
Name: "No input",
|
||||
Input: nil,
|
||||
},
|
||||
{
|
||||
Name: "AIA Issuer without chain files",
|
||||
Input: map[string][]string{
|
||||
"http://break.the.chain.com": []string{},
|
||||
},
|
||||
ExpectedResult: nil,
|
||||
ExpectedError: fmt.Errorf(
|
||||
"CertificateChain entry for AIA issuer url \"http://break.the.chain.com\" " +
|
||||
"has no chain file names configured"),
|
||||
|
@ -69,7 +66,6 @@ func TestLoadCertificateChains(t *testing.T) {
|
|||
Input: map[string][]string{
|
||||
"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\" " +
|
||||
"has an invalid chain file: \"/tmp/does.not.exist.pem\" - error reading " +
|
||||
"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{
|
||||
"http://windows.sad.zone": []string{crlfPEM.Name()},
|
||||
},
|
||||
ExpectedResult: nil,
|
||||
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()),
|
||||
},
|
||||
|
@ -88,7 +83,6 @@ func TestLoadCertificateChains(t *testing.T) {
|
|||
Input: map[string][]string{
|
||||
"http://ok.go": []string{invalidPEMFile.Name()},
|
||||
},
|
||||
ExpectedResult: nil,
|
||||
ExpectedError: fmt.Errorf(
|
||||
"CertificateChain entry for AIA issuer url \"http://ok.go\" has an "+
|
||||
"invalid chain file: %q - contents did not decode as PEM",
|
||||
|
@ -99,7 +93,6 @@ func TestLoadCertificateChains(t *testing.T) {
|
|||
Input: map[string][]string{
|
||||
"http://not-a-cert.com": []string{"../../test/test-root.key"},
|
||||
},
|
||||
ExpectedResult: nil,
|
||||
ExpectedError: fmt.Errorf(
|
||||
"CertificateChain entry for AIA issuer url \"http://not-a-cert.com\" has " +
|
||||
"an invalid chain file: \"../../test/test-root.key\" - PEM block type " +
|
||||
|
@ -110,7 +103,6 @@ func TestLoadCertificateChains(t *testing.T) {
|
|||
Input: map[string][]string{
|
||||
"http://tasty.leftovers.com": []string{leftoverPEMFile.Name()},
|
||||
},
|
||||
ExpectedResult: nil,
|
||||
ExpectedError: fmt.Errorf(
|
||||
"CertificateChain entry for AIA issuer url \"http://tasty.leftovers.com\" "+
|
||||
"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{
|
||||
"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))),
|
||||
},
|
||||
ExpectedError: nil,
|
||||
},
|
||||
{
|
||||
Name: "Two PEM file chain",
|
||||
Input: map[string][]string{
|
||||
"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))),
|
||||
},
|
||||
ExpectedError: nil,
|
||||
},
|
||||
{
|
||||
Name: "One PEM file chain, no trailing newline",
|
||||
Input: map[string][]string{
|
||||
"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
|
||||
// expect in the format specifier below.
|
||||
"http://single-cert-chain.nonewline.com": []byte(fmt.Sprintf("\n%s\n", string(abruptPEMBytes))),
|
||||
},
|
||||
ExpectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
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 {
|
||||
t.Errorf("Expected nil error, got %#v\n", err)
|
||||
} else if tc.ExpectedError != nil && err == nil {
|
||||
|
@ -163,9 +152,10 @@ func TestLoadCertificateChains(t *testing.T) {
|
|||
} else if tc.ExpectedError != nil {
|
||||
test.AssertEquals(t, err.Error(), tc.ExpectedError.Error())
|
||||
}
|
||||
test.AssertEquals(t, len(result), len(tc.ExpectedResult))
|
||||
for url, chain := range result {
|
||||
test.Assert(t, bytes.Compare(chain, tc.ExpectedResult[url]) == 0, "Chain bytes did not match expected")
|
||||
test.AssertEquals(t, len(resultMap), len(tc.ExpectedMap))
|
||||
test.AssertEquals(t, len(issuers), len(tc.ExpectedMap))
|
||||
for url, chain := range resultMap {
|
||||
test.Assert(t, bytes.Compare(chain, tc.ExpectedMap[url]) == 0, "Chain bytes did not match expected")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -118,6 +118,7 @@ type StorageGetter interface {
|
|||
GetValidAuthorizations(ctx context.Context, regID int64, domains []string, now time.Time) (map[string]*Authorization, error)
|
||||
GetPendingAuthorization(ctx context.Context, req *sapb.GetPendingAuthorizationRequest) (*Authorization, 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)
|
||||
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)
|
||||
|
|
|
@ -35,11 +35,12 @@ func _() {
|
|||
_ = x[DeleteUnusedChallenges-24]
|
||||
_ = x[V1DisableNewValidations-25]
|
||||
_ = 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 {
|
||||
if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) {
|
||||
|
|
|
@ -72,6 +72,9 @@ const (
|
|||
// generating a precertificate. This also changes the issuance / storage flow,
|
||||
// adding two new calls from CA to SA: AddSerial and AddPrecertificate.
|
||||
PrecertificateOCSP
|
||||
// PrecertificateRevocation allows revocation of precertificates with the
|
||||
// ACMEv2 interface.
|
||||
PrecertificateRevocation
|
||||
)
|
||||
|
||||
// List of features and their default value, protected by fMu
|
||||
|
@ -103,6 +106,7 @@ var features = map[FeatureFlag]bool{
|
|||
DeleteUnusedChallenges: false,
|
||||
V1DisableNewValidations: false,
|
||||
PrecertificateOCSP: false,
|
||||
PrecertificateRevocation: false,
|
||||
}
|
||||
|
||||
var fMu = new(sync.RWMutex)
|
||||
|
|
|
@ -56,7 +56,7 @@ func (cac CertificateAuthorityClientWrapper) IssueCertificateForPrecertificate(c
|
|||
if err != nil {
|
||||
return core.Certificate{}, err
|
||||
}
|
||||
return pbToCert(res)
|
||||
return PBToCert(res)
|
||||
}
|
||||
|
||||
func (cac CertificateAuthorityClientWrapper) GenerateOCSP(ctx context.Context, ocspReq core.OCSPSigningRequest) ([]byte, error) {
|
||||
|
@ -113,7 +113,7 @@ func (cas *CertificateAuthorityServerWrapper) IssueCertificateForPrecertificate(
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return certToPB(cert), nil
|
||||
return CertToPB(cert), nil
|
||||
}
|
||||
|
||||
func (cas *CertificateAuthorityServerWrapper) GenerateOCSP(ctx context.Context, request *caPB.GenerateOCSPRequest) (*caPB.OCSPResponse, error) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
func certToPB(cert core.Certificate) *corepb.Certificate {
|
||||
func CertToPB(cert core.Certificate) *corepb.Certificate {
|
||||
issued, expires := cert.Issued.UnixNano(), cert.Expires.UnixNano()
|
||||
return &corepb.Certificate{
|
||||
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 {
|
||||
return core.Certificate{}, errIncompleteResponse
|
||||
}
|
||||
|
|
|
@ -306,8 +306,8 @@ func TestCert(t *testing.T) {
|
|||
Expires: now.Add(time.Hour),
|
||||
}
|
||||
|
||||
certPB := certToPB(cert)
|
||||
outCert, _ := pbToCert(certPB)
|
||||
certPB := CertToPB(cert)
|
||||
outCert, _ := PBToCert(certPB)
|
||||
|
||||
test.AssertDeepEquals(t, cert, outCert)
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ func (rac RegistrationAuthorityClientWrapper) NewCertificate(ctx context.Context
|
|||
return core.Certificate{}, err
|
||||
}
|
||||
|
||||
return pbToCert(response)
|
||||
return PBToCert(response)
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
return certToPB(cert), nil
|
||||
return CertToPB(cert), nil
|
||||
}
|
||||
|
||||
func (ras *RegistrationAuthorityServerWrapper) UpdateRegistration(ctx context.Context, request *rapb.UpdateRegistrationRequest) (*corepb.Registration, error) {
|
||||
|
|
|
@ -108,7 +108,18 @@ func (sac StorageAuthorityClientWrapper) GetCertificate(ctx context.Context, ser
|
|||
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) {
|
||||
|
@ -729,7 +740,14 @@ func (sas StorageAuthorityServerWrapper) GetCertificate(ctx context.Context, req
|
|||
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) {
|
||||
|
|
|
@ -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
|
||||
func (sa *StorageAuthority) GetCertificateStatus(_ context.Context, serial string) (core.CertificateStatus, error) {
|
||||
// Serial ee == 238.crt
|
||||
|
|
|
@ -42,6 +42,10 @@ func (sa *mockInvalidAuthorizationsAuthority) GetCertificate(ctx context.Context
|
|||
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) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
19
sa/model.go
19
sa/model.go
|
@ -132,6 +132,25 @@ func SelectCertificate(s dbOneSelector, q string, args ...interface{}) (core.Cer
|
|||
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 {
|
||||
ID int64
|
||||
core.Certificate
|
||||
|
|
|
@ -1838,122 +1838,122 @@ func init() {
|
|||
func init() { proto.RegisterFile("sa/proto/sa.proto", fileDescriptor_099fb35e782a48a6) }
|
||||
|
||||
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,
|
||||
0x11, 0xe7, 0x1f, 0x53, 0x26, 0x57, 0xb2, 0xfe, 0x9c, 0x25, 0x0a, 0x81, 0x65, 0x9b, 0xbe, 0x3a,
|
||||
0x1e, 0x65, 0x3a, 0xa3, 0xb8, 0x68, 0x27, 0xe9, 0x8c, 0x1a, 0xa7, 0x72, 0x24, 0xcb, 0x4a, 0x1d,
|
||||
0x11, 0xe7, 0x1f, 0x53, 0x26, 0x57, 0xb2, 0xfe, 0x9c, 0x25, 0x1a, 0x81, 0x65, 0x9b, 0xbe, 0x3a,
|
||||
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,
|
||||
0xa4, 0xe7, 0xce, 0xb4, 0x9f, 0xa0, 0xd3, 0xc7, 0x7e, 0x8e, 0x7e, 0x89, 0x7e, 0xa1, 0x3e, 0x74,
|
||||
0x6e, 0xef, 0x00, 0x02, 0xe0, 0x81, 0x72, 0xec, 0x4e, 0xdf, 0xb0, 0x7b, 0xbb, 0x7b, 0x7b, 0x77,
|
||||
0xfb, 0xe7, 0xb7, 0x24, 0xac, 0x89, 0xe0, 0xe3, 0x31, 0x4f, 0x64, 0xf2, 0xb1, 0x08, 0x76, 0xf0,
|
||||
0xa4, 0xe7, 0xce, 0xb4, 0x9f, 0xa0, 0xd3, 0xc7, 0x7e, 0x86, 0x3e, 0xf6, 0x4b, 0xf4, 0x2b, 0x75,
|
||||
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, 0xb6, 0xf1,
|
||||
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,
|
||||
0x7a, 0xaf, 0xbe, 0xdd, 0xf4, 0x1b, 0x51, 0x48, 0xef, 0x01, 0x7c, 0x39, 0x78, 0x79, 0xfc, 0x2d,
|
||||
0xfb, 0xee, 0x77, 0xec, 0x92, 0xac, 0x42, 0xf3, 0xcf, 0x17, 0x6f, 0x70, 0x79, 0xc9, 0x57, 0x9f,
|
||||
0xf4, 0x01, 0xac, 0xec, 0x4d, 0xe4, 0xeb, 0x84, 0x47, 0x57, 0xb3, 0x26, 0x3a, 0x68, 0xe2, 0x5f,
|
||||
0x75, 0xb8, 0x77, 0xc8, 0x64, 0x9f, 0xc5, 0x61, 0x14, 0x9f, 0x16, 0xa4, 0x7d, 0xf6, 0xc3, 0x84,
|
||||
0x09, 0x49, 0x1e, 0xc1, 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,
|
||||
0x4c, 0x39, 0x27, 0xc1, 0x68, 0xc2, 0x9c, 0x26, 0x0a, 0x96, 0xd9, 0xe4, 0x1e, 0xc0, 0x79, 0x30,
|
||||
0x8a, 0xc2, 0x57, 0xb1, 0x8c, 0x46, 0xce, 0x0d, 0xdc, 0x35, 0xc7, 0xa1, 0x02, 0xee, 0x1e, 0x32,
|
||||
0x79, 0xa2, 0x18, 0x05, 0xcf, 0xc5, 0x4f, 0x75, 0xdd, 0x81, 0x9b, 0x61, 0x72, 0x16, 0x44, 0xb1,
|
||||
0x70, 0x1a, 0xbd, 0xe6, 0x76, 0xc7, 0x4f, 0x49, 0x75, 0xa9, 0x71, 0x72, 0x81, 0x0e, 0x36, 0x7d,
|
||||
0xf5, 0x49, 0xff, 0x59, 0x87, 0xdb, 0x96, 0x2d, 0xc9, 0xaf, 0xa1, 0x85, 0xae, 0x39, 0xf5, 0x5e,
|
||||
0x73, 0x7b, 0xd1, 0xa3, 0x3b, 0x22, 0xd8, 0xb1, 0xc8, 0xed, 0x7c, 0x15, 0x8c, 0x0f, 0x46, 0xec,
|
||||
0x8c, 0xc5, 0xd2, 0xd7, 0x0a, 0xee, 0x4b, 0x80, 0x29, 0x93, 0x74, 0x61, 0x41, 0x6f, 0x6e, 0x5e,
|
||||
0xc9, 0x50, 0xe4, 0x23, 0x68, 0x05, 0x13, 0xf9, 0xfa, 0x0a, 0x6f, 0x75, 0xd1, 0xbb, 0xbd, 0x83,
|
||||
0xa1, 0x52, 0x7c, 0x31, 0x2d, 0x41, 0xff, 0xdd, 0x80, 0xb5, 0x2f, 0x18, 0x57, 0x57, 0x39, 0x0c,
|
||||
0x24, 0x1b, 0xc8, 0x40, 0x4e, 0x84, 0x32, 0x2c, 0x18, 0x8f, 0x82, 0x51, 0x6a, 0x58, 0x53, 0xc8,
|
||||
0x47, 0x09, 0xf3, 0x0c, 0x86, 0x52, 0xef, 0x94, 0x0c, 0xc5, 0xf8, 0x45, 0x20, 0xe4, 0xab, 0x71,
|
||||
0x18, 0x48, 0x16, 0x9a, 0x27, 0x28, 0xb3, 0x49, 0x0f, 0x16, 0x39, 0x3b, 0x4f, 0xde, 0xb0, 0x70,
|
||||
0x3f, 0x90, 0xcc, 0x69, 0xa1, 0x54, 0x9e, 0x45, 0x1e, 0xc2, 0x2d, 0x43, 0xfa, 0x2c, 0x10, 0x49,
|
||||
0xec, 0x2c, 0xa0, 0x4c, 0x91, 0x49, 0x7e, 0x05, 0x1b, 0xa3, 0x40, 0xc8, 0x83, 0x1f, 0xc7, 0x91,
|
||||
0x7e, 0x9a, 0xe3, 0xe0, 0x74, 0xc0, 0x62, 0xe9, 0xdc, 0x44, 0x69, 0xfb, 0x22, 0xa1, 0xb0, 0xa4,
|
||||
0x1c, 0xf2, 0x99, 0x18, 0x27, 0xb1, 0x60, 0x4e, 0x1b, 0x13, 0xa0, 0xc0, 0x23, 0x2e, 0xb4, 0xe3,
|
||||
0x44, 0xee, 0x7d, 0x2f, 0x19, 0x77, 0x3a, 0x68, 0x2c, 0xa3, 0xc9, 0x16, 0x74, 0x22, 0x81, 0x66,
|
||||
0x59, 0xe8, 0x40, 0xaf, 0xbe, 0xdd, 0xf6, 0xa7, 0x8c, 0x2f, 0x6f, 0xb4, 0x1b, 0xab, 0x4d, 0xda,
|
||||
0x83, 0x85, 0xc1, 0xf4, 0xb6, 0x2c, 0xb7, 0x48, 0x77, 0xa1, 0xe5, 0x07, 0xf1, 0x29, 0x6e, 0xc5,
|
||||
0x02, 0x3e, 0x8a, 0x98, 0x90, 0x26, 0xda, 0x32, 0x5a, 0x29, 0x8f, 0x02, 0xa9, 0x56, 0x1a, 0xb8,
|
||||
0x62, 0x28, 0x7a, 0x17, 0x5a, 0x5f, 0x24, 0x93, 0x58, 0x92, 0x75, 0x68, 0x0d, 0xd5, 0x87, 0xd1,
|
||||
0xd4, 0x04, 0xfd, 0x3d, 0xdc, 0xc7, 0xe5, 0xdc, 0x9b, 0x8a, 0xa7, 0x97, 0xc7, 0xc1, 0x19, 0xcb,
|
||||
0x22, 0xfd, 0x3e, 0xb4, 0xb8, 0xda, 0x1e, 0x15, 0x17, 0xbd, 0x8e, 0x8a, 0x3e, 0xf4, 0xc7, 0xd7,
|
||||
0x7c, 0x65, 0x39, 0x56, 0x0a, 0x26, 0xc0, 0x35, 0x41, 0xff, 0x5a, 0x87, 0x25, 0x34, 0x6d, 0xcc,
|
||||
0x91, 0xcf, 0x61, 0x69, 0x98, 0xa3, 0x4d, 0x30, 0xdf, 0x51, 0xe6, 0xf2, 0x72, 0xf9, 0x28, 0x2e,
|
||||
0x28, 0xb8, 0x9f, 0x14, 0x82, 0x99, 0xc0, 0x0d, 0xb5, 0x91, 0xb9, 0x2b, 0xfc, 0x9e, 0x9e, 0xb1,
|
||||
0x91, 0x3f, 0x63, 0x1f, 0xee, 0xe2, 0x06, 0xf9, 0x92, 0x27, 0x9e, 0x5e, 0x1e, 0xf5, 0xd3, 0x13,
|
||||
0xaa, 0xca, 0x35, 0x36, 0xd5, 0xad, 0x11, 0x8d, 0xa7, 0x27, 0x6e, 0xd8, 0x4f, 0x4c, 0xff, 0x56,
|
||||
0x87, 0x07, 0x68, 0xf2, 0x28, 0x3e, 0x7f, 0xff, 0x12, 0xe1, 0x42, 0xfb, 0x75, 0x22, 0x24, 0x9e,
|
||||
0x46, 0xd7, 0xb5, 0x8c, 0x9e, 0xba, 0xd2, 0xac, 0x70, 0x65, 0x00, 0x04, 0x3d, 0x79, 0xc9, 0x43,
|
||||
0xc6, 0xb3, 0xad, 0xb7, 0xa0, 0x13, 0x0c, 0xf1, 0xf4, 0xd9, 0xae, 0x53, 0xc6, 0xf5, 0xe7, 0x7b,
|
||||
0x0e, 0xeb, 0x68, 0xf4, 0xd9, 0xd7, 0xfb, 0xc7, 0x03, 0x26, 0x33, 0xb3, 0x5d, 0x58, 0xb8, 0x88,
|
||||
0xe2, 0x30, 0xb9, 0x30, 0x36, 0x0d, 0x55, 0x5d, 0xe4, 0xe8, 0x63, 0x58, 0x37, 0x46, 0x0e, 0x7e,
|
||||
0x8c, 0xc4, 0xd4, 0x52, 0x4e, 0xa3, 0x5e, 0xd4, 0xe8, 0x43, 0xaf, 0xcf, 0xd9, 0x79, 0x94, 0x4c,
|
||||
0x44, 0x2e, 0x28, 0x8b, 0xda, 0x55, 0x85, 0x6c, 0x1d, 0x5a, 0x9c, 0x9d, 0x1e, 0xed, 0xa7, 0xef,
|
||||
0x8f, 0x84, 0xca, 0x30, 0xad, 0xae, 0xf4, 0x18, 0x7e, 0xa1, 0x5e, 0xdb, 0x37, 0x14, 0x95, 0xb0,
|
||||
0xba, 0x17, 0x86, 0x3a, 0x0d, 0xd3, 0x3d, 0x32, 0x5b, 0xf5, 0x9c, 0xad, 0x5c, 0x8e, 0x36, 0x0a,
|
||||
0x95, 0xce, 0x81, 0x9b, 0x43, 0xce, 0xb0, 0x92, 0xe9, 0x82, 0x9e, 0x92, 0x6a, 0x85, 0x61, 0xc2,
|
||||
0x0b, 0x53, 0xe3, 0x52, 0x92, 0xbe, 0x81, 0x8d, 0xbd, 0x30, 0xcc, 0x1d, 0x32, 0xdd, 0x7a, 0x15,
|
||||
0x9a, 0x21, 0xe3, 0x69, 0xbb, 0x0d, 0x19, 0xb7, 0x1f, 0x4c, 0xa5, 0x80, 0x2a, 0x45, 0xb8, 0xe3,
|
||||
0x92, 0x8f, 0xdf, 0xca, 0xc1, 0x48, 0x88, 0x49, 0x56, 0x51, 0x0d, 0x45, 0x1f, 0x43, 0xb7, 0xbc,
|
||||
0x99, 0x29, 0x60, 0xea, 0x32, 0xa3, 0xd3, 0xb4, 0xa6, 0xa8, 0xcb, 0x44, 0x8a, 0xf6, 0x61, 0x09,
|
||||
0x83, 0x2a, 0x9f, 0x25, 0x39, 0x88, 0x40, 0x1e, 0xc3, 0xed, 0x89, 0x60, 0x27, 0x5e, 0x31, 0xf8,
|
||||
0xd1, 0xc3, 0xb6, 0x6f, 0x5b, 0xa2, 0x2f, 0x80, 0xa6, 0x4d, 0x15, 0x2d, 0xdb, 0xd3, 0xa6, 0xbc,
|
||||
0x4f, 0x17, 0x16, 0x82, 0xe1, 0x50, 0x66, 0x87, 0x37, 0x14, 0xbd, 0x84, 0xcd, 0x43, 0xa6, 0xe3,
|
||||
0xfe, 0x59, 0xc2, 0x0b, 0x25, 0x6b, 0xaa, 0x52, 0xcf, 0xab, 0xd8, 0x2b, 0x55, 0xd5, 0x41, 0x9a,
|
||||
0xd5, 0x07, 0xf9, 0x47, 0x1d, 0x9c, 0x43, 0x26, 0xff, 0x6f, 0xc8, 0x40, 0x35, 0x4c, 0xce, 0x7e,
|
||||
0x98, 0x44, 0xdc, 0xf8, 0x72, 0xa5, 0x83, 0xa9, 0xed, 0x97, 0xd9, 0xf4, 0xef, 0x75, 0x58, 0x2e,
|
||||
0xc1, 0x87, 0x5f, 0xa6, 0xed, 0x5d, 0x57, 0xdc, 0xbb, 0x2a, 0xdd, 0xe7, 0x20, 0x07, 0x94, 0xfd,
|
||||
0xdf, 0x23, 0x87, 0x17, 0x70, 0x7f, 0x2f, 0x0c, 0x6d, 0x68, 0x30, 0xbb, 0xb9, 0x8f, 0x8a, 0x8e,
|
||||
0xce, 0xb3, 0xf6, 0x10, 0x56, 0x4b, 0xf8, 0x13, 0xaf, 0x2d, 0x0a, 0xd3, 0x7a, 0xa2, 0x3e, 0x29,
|
||||
0x9d, 0x91, 0xf2, 0x66, 0x90, 0xee, 0x87, 0xb0, 0x56, 0x90, 0xf1, 0x4a, 0xa6, 0x9a, 0xda, 0xd4,
|
||||
0x15, 0x38, 0x3e, 0x22, 0x0a, 0x4b, 0xbe, 0xce, 0x81, 0x3f, 0x5c, 0x63, 0x12, 0x13, 0xb9, 0x9a,
|
||||
0x52, 0x79, 0xab, 0xd0, 0x8d, 0x79, 0x60, 0xfc, 0x56, 0x4d, 0x80, 0xa7, 0x30, 0xe3, 0x06, 0xe6,
|
||||
0x73, 0x46, 0xd3, 0xbf, 0x34, 0x60, 0xeb, 0x59, 0x14, 0x07, 0xa3, 0xe8, 0x8a, 0x59, 0x71, 0xb4,
|
||||
0x25, 0x65, 0x0c, 0xee, 0x6a, 0x14, 0x70, 0x57, 0xae, 0x16, 0x35, 0x0b, 0xb5, 0x08, 0x1b, 0x86,
|
||||
0x94, 0xec, 0x6c, 0x9c, 0x62, 0xb1, 0x8e, 0x3f, 0x65, 0x90, 0x7d, 0x58, 0xc3, 0x3e, 0x67, 0x36,
|
||||
0x1d, 0x26, 0x3c, 0x14, 0x4e, 0x0b, 0x1f, 0xa9, 0xab, 0x1f, 0xe9, 0xa4, 0xb4, 0xec, 0xcf, 0x2a,
|
||||
0x90, 0x27, 0xb0, 0x32, 0x65, 0x1e, 0x70, 0x9e, 0x70, 0xc4, 0x6a, 0x8b, 0xde, 0xba, 0xb6, 0xd1,
|
||||
0xe7, 0xc9, 0x77, 0x23, 0x76, 0xb6, 0xcf, 0x64, 0x10, 0x8d, 0x84, 0x5f, 0x16, 0xf6, 0xfe, 0xb3,
|
||||
0x09, 0xab, 0x03, 0x99, 0xf0, 0xe0, 0x34, 0xbd, 0x05, 0x79, 0x49, 0x76, 0x61, 0xe5, 0x90, 0x15,
|
||||
0x5a, 0x3b, 0x21, 0xd8, 0xcf, 0x0a, 0xc9, 0xe6, 0x12, 0xbd, 0x45, 0x9e, 0x4b, 0x6b, 0xe4, 0x37,
|
||||
0xb0, 0x5e, 0x52, 0x7e, 0x7a, 0xa9, 0xe6, 0x9d, 0x65, 0x65, 0x61, 0x3a, 0xff, 0x54, 0x68, 0x3f,
|
||||
0x81, 0xd5, 0x72, 0x11, 0x20, 0xb7, 0x67, 0x92, 0xeb, 0x68, 0xdf, 0xb5, 0x05, 0x32, 0xad, 0x91,
|
||||
0x6f, 0xb0, 0x80, 0xd9, 0x32, 0x82, 0x20, 0xc4, 0x9f, 0x3f, 0x3c, 0x55, 0x59, 0x3d, 0x81, 0xae,
|
||||
0x7d, 0x72, 0x21, 0x0f, 0x8c, 0xd1, 0xea, 0xa9, 0xc6, 0xdd, 0xac, 0x18, 0x2d, 0x68, 0x8d, 0xfc,
|
||||
0x02, 0x96, 0x0f, 0x59, 0x1e, 0x27, 0x12, 0x50, 0xc2, 0xba, 0x69, 0xba, 0x6b, 0xda, 0x99, 0xdc,
|
||||
0x32, 0xad, 0x91, 0x5d, 0xbc, 0xde, 0xd9, 0x71, 0x21, 0xaf, 0xb8, 0x81, 0xf8, 0xaf, 0x2c, 0x42,
|
||||
0x6b, 0x64, 0x00, 0x4e, 0x15, 0x32, 0x25, 0x3f, 0xcb, 0x40, 0x63, 0x35, 0x6e, 0x75, 0x57, 0xcb,
|
||||
0xc8, 0x92, 0xd6, 0xc8, 0x73, 0xe8, 0xda, 0xa1, 0xa0, 0xbe, 0x9c, 0xb9, 0x30, 0xd1, 0xed, 0x64,
|
||||
0x22, 0xb4, 0x46, 0xbe, 0x82, 0x3b, 0x15, 0xd2, 0x88, 0x89, 0x7f, 0xaa, 0xb9, 0xcf, 0xc0, 0xc5,
|
||||
0x4f, 0x6b, 0x7d, 0xb4, 0x46, 0x74, 0x41, 0xdd, 0x83, 0xc5, 0x1c, 0x0a, 0x24, 0xdd, 0x6c, 0xad,
|
||||
0x00, 0x0b, 0x8b, 0x3a, 0x7d, 0xb3, 0xa5, 0x15, 0xc3, 0x92, 0x0f, 0x33, 0xd1, 0x79, 0x18, 0xb7,
|
||||
0x68, 0xf1, 0x13, 0xb8, 0x55, 0x80, 0x8d, 0xc4, 0xc9, 0x56, 0x4b, 0x48, 0xb2, 0xa8, 0xf7, 0x29,
|
||||
0xdc, 0x2a, 0x80, 0x44, 0xad, 0x67, 0xc3, 0x8d, 0x2e, 0x86, 0x8e, 0x66, 0xd1, 0x1a, 0x79, 0x09,
|
||||
0x1f, 0x54, 0x62, 0x45, 0xf2, 0x50, 0x89, 0x5e, 0x07, 0x25, 0x4b, 0x06, 0x7f, 0x0b, 0x6b, 0xe5,
|
||||
0x94, 0xf6, 0xc8, 0xba, 0x25, 0xa7, 0xbd, 0xaa, 0xf4, 0x7b, 0x0e, 0x64, 0x06, 0x19, 0x78, 0x64,
|
||||
0xcb, 0xa4, 0x9e, 0xfd, 0x12, 0xc9, 0x6c, 0x47, 0xa6, 0x35, 0xf2, 0x0a, 0x31, 0x86, 0x2d, 0x20,
|
||||
0xbc, 0xf7, 0xa9, 0x0f, 0x4f, 0x4c, 0xe0, 0x5a, 0x23, 0xcd, 0xbb, 0x3e, 0xd4, 0xfe, 0x04, 0x5b,
|
||||
0x73, 0x40, 0x9c, 0x47, 0x1e, 0xe5, 0xab, 0x4c, 0x35, 0xcc, 0xab, 0x38, 0xf4, 0xd7, 0xc6, 0x3b,
|
||||
0x6b, 0xd0, 0x79, 0xef, 0x14, 0x95, 0x3e, 0x96, 0xd9, 0x13, 0x9b, 0xb9, 0xb7, 0xa8, 0x88, 0x76,
|
||||
0x37, 0x77, 0x61, 0xe5, 0x98, 0x5d, 0x94, 0xba, 0xce, 0x4c, 0x8f, 0xa8, 0xe8, 0x1b, 0x9f, 0x02,
|
||||
0xd1, 0x3f, 0x6f, 0x5c, 0xab, 0xbf, 0xa8, 0x79, 0x07, 0x67, 0x63, 0x79, 0x49, 0x6b, 0xe4, 0x00,
|
||||
0x36, 0x8f, 0xd9, 0x85, 0xb5, 0x61, 0xd8, 0x1e, 0xbb, 0x2a, 0x02, 0x76, 0x61, 0xc3, 0x8a, 0x26,
|
||||
0xec, 0x46, 0x4a, 0x3e, 0x1c, 0xc1, 0x72, 0x71, 0x8e, 0x20, 0x1f, 0xe0, 0x0d, 0xd9, 0x06, 0x19,
|
||||
0xd7, 0xb5, 0x2d, 0x19, 0x50, 0xa3, 0x6a, 0xde, 0x9a, 0x42, 0x84, 0x9c, 0x0d, 0xdf, 0xce, 0x5a,
|
||||
0xc9, 0x93, 0xc7, 0xd0, 0xc9, 0x86, 0x36, 0x93, 0xa3, 0xa5, 0x19, 0xae, 0xac, 0xb1, 0x0b, 0xdd,
|
||||
0x7d, 0x16, 0x0c, 0x65, 0x74, 0x3e, 0x7b, 0xf9, 0xb3, 0x51, 0x5f, 0x52, 0xfe, 0x0c, 0x36, 0xa7,
|
||||
0xca, 0x6f, 0xd1, 0xf4, 0x4b, 0xea, 0x8f, 0xa0, 0x7d, 0xcc, 0x2e, 0x30, 0x1f, 0x88, 0x59, 0x42,
|
||||
0xc2, 0xcd, 0x13, 0x78, 0x2a, 0x32, 0x30, 0x53, 0x4d, 0x9f, 0x27, 0x43, 0x26, 0x44, 0x14, 0x9f,
|
||||
0x5a, 0x35, 0x52, 0xcb, 0x3f, 0x87, 0x5b, 0xa9, 0x06, 0xe2, 0xa4, 0xeb, 0x84, 0xd3, 0xb7, 0xaf,
|
||||
0xf6, 0x65, 0x2a, 0xdc, 0x4e, 0x27, 0x2c, 0x82, 0xdd, 0x34, 0x3f, 0x0f, 0x96, 0x1d, 0xff, 0x23,
|
||||
0xdc, 0x99, 0x93, 0xf5, 0xef, 0x59, 0x16, 0x34, 0xd4, 0x2a, 0xcc, 0x7a, 0xe4, 0x8e, 0xb1, 0x68,
|
||||
0x9b, 0x00, 0xcb, 0xce, 0x1d, 0xce, 0xd6, 0x75, 0xf1, 0x4e, 0x45, 0xf9, 0x5b, 0x70, 0xaa, 0xa6,
|
||||
0x18, 0x8d, 0x4a, 0xae, 0x99, 0x71, 0x5c, 0x5b, 0x33, 0x31, 0x9d, 0x67, 0x66, 0xbe, 0xd0, 0x1e,
|
||||
0x56, 0x8d, 0x1d, 0xe5, 0xd7, 0xf2, 0x81, 0x1c, 0xb3, 0x8b, 0x72, 0x89, 0x7b, 0x2b, 0xa7, 0x36,
|
||||
0x66, 0x9c, 0xf2, 0xb4, 0x57, 0x47, 0xd0, 0xb5, 0x96, 0x0a, 0x8f, 0xf4, 0xb0, 0x45, 0xcf, 0x19,
|
||||
0x4a, 0xca, 0xee, 0x7d, 0x0e, 0x4e, 0x45, 0xfe, 0x54, 0x75, 0xd8, 0xa2, 0x81, 0xa7, 0x37, 0xff,
|
||||
0xd0, 0xc2, 0x7f, 0x2f, 0xfe, 0x1b, 0x00, 0x00, 0xff, 0xff, 0x6f, 0x39, 0x5a, 0x2e, 0xec, 0x18,
|
||||
0x00, 0x00,
|
||||
0x3e, 0x3b, 0x89, 0x84, 0xe4, 0x81, 0x8c, 0x92, 0xf8, 0x70, 0x8f, 0x2c, 0x43, 0x23, 0x0a, 0x9d,
|
||||
0x7a, 0xaf, 0xbe, 0xd5, 0xf4, 0x1b, 0x51, 0x48, 0xef, 0x02, 0x7c, 0x35, 0x78, 0x71, 0xf4, 0x1d,
|
||||
0xfb, 0xfe, 0xf7, 0xec, 0x82, 0xac, 0x42, 0xf3, 0x2f, 0xe7, 0xaf, 0x71, 0x79, 0xc9, 0x57, 0x9f,
|
||||
0xf4, 0x3e, 0xac, 0xec, 0x4e, 0xe4, 0xab, 0x84, 0x47, 0x97, 0xb3, 0x26, 0x3a, 0x68, 0xe2, 0x3f,
|
||||
0x75, 0xb8, 0x7b, 0xc0, 0x64, 0x9f, 0xc5, 0x61, 0x14, 0x9f, 0x14, 0xa4, 0x7d, 0xf6, 0xe3, 0x84,
|
||||
0x09, 0x49, 0x1e, 0xc2, 0x32, 0x2f, 0xf8, 0x61, 0x3c, 0x28, 0x71, 0x95, 0x5c, 0x14, 0xb2, 0x58,
|
||||
0x46, 0x3f, 0x44, 0x8c, 0x7f, 0x7b, 0x31, 0x66, 0x4e, 0x03, 0xb7, 0x29, 0x71, 0xc9, 0x16, 0xac,
|
||||
0x4c, 0x39, 0xc7, 0xc1, 0x68, 0xc2, 0x9c, 0x26, 0x0a, 0x96, 0xd9, 0xe4, 0x2e, 0xc0, 0x59, 0x30,
|
||||
0x8a, 0xc2, 0x97, 0xb1, 0x8c, 0x46, 0xce, 0x35, 0xdc, 0x35, 0xc7, 0xa1, 0x02, 0xee, 0x1c, 0x30,
|
||||
0x79, 0xac, 0x18, 0x05, 0xcf, 0xc5, 0xdb, 0xba, 0xee, 0xc0, 0xf5, 0x30, 0x39, 0x0d, 0xa2, 0x58,
|
||||
0x38, 0x8d, 0x5e, 0x73, 0xab, 0xe3, 0xa7, 0xa4, 0xba, 0xd4, 0x38, 0x39, 0x47, 0x07, 0x9b, 0xbe,
|
||||
0xfa, 0xa4, 0xff, 0xaa, 0xc3, 0x4d, 0xcb, 0x96, 0xe4, 0x37, 0xd0, 0x42, 0xd7, 0x9c, 0x7a, 0xaf,
|
||||
0xb9, 0xb5, 0xe8, 0xd1, 0x6d, 0x11, 0x6c, 0x5b, 0xe4, 0xb6, 0xbf, 0x0e, 0xc6, 0xfb, 0x23, 0x76,
|
||||
0xca, 0x62, 0xe9, 0x6b, 0x05, 0xf7, 0x05, 0xc0, 0x94, 0x49, 0xba, 0xb0, 0xa0, 0x37, 0x37, 0xaf,
|
||||
0x64, 0x28, 0xf2, 0x11, 0xb4, 0x82, 0x89, 0x7c, 0x75, 0x89, 0xb7, 0xba, 0xe8, 0xdd, 0xdc, 0xc6,
|
||||
0x50, 0x29, 0xbe, 0x98, 0x96, 0xa0, 0xff, 0x6d, 0xc0, 0xda, 0x97, 0x8c, 0xab, 0xab, 0x1c, 0x06,
|
||||
0x92, 0x0d, 0x64, 0x20, 0x27, 0x42, 0x19, 0x16, 0x8c, 0x47, 0xc1, 0x28, 0x35, 0xac, 0x29, 0xe4,
|
||||
0xa3, 0x84, 0x79, 0x06, 0x43, 0xa9, 0x77, 0x4a, 0x86, 0x62, 0xfc, 0x3c, 0x10, 0xf2, 0xe5, 0x38,
|
||||
0x0c, 0x24, 0x0b, 0xcd, 0x13, 0x94, 0xd9, 0xa4, 0x07, 0x8b, 0x9c, 0x9d, 0x25, 0xaf, 0x59, 0xb8,
|
||||
0x17, 0x48, 0xe6, 0xb4, 0x50, 0x2a, 0xcf, 0x22, 0x0f, 0xe0, 0x86, 0x21, 0x7d, 0x16, 0x88, 0x24,
|
||||
0x76, 0x16, 0x50, 0xa6, 0xc8, 0x24, 0x9f, 0xc0, 0xc6, 0x28, 0x10, 0x72, 0xff, 0xa7, 0x71, 0xa4,
|
||||
0x9f, 0xe6, 0x28, 0x38, 0x19, 0xb0, 0x58, 0x3a, 0xd7, 0x51, 0xda, 0xbe, 0x48, 0x28, 0x2c, 0x29,
|
||||
0x87, 0x7c, 0x26, 0xc6, 0x49, 0x2c, 0x98, 0xd3, 0xc6, 0x04, 0x28, 0xf0, 0x88, 0x0b, 0xed, 0x38,
|
||||
0x91, 0xbb, 0x3f, 0x48, 0xc6, 0x9d, 0x0e, 0x1a, 0xcb, 0x68, 0xb2, 0x09, 0x9d, 0x48, 0xa0, 0x59,
|
||||
0x16, 0x3a, 0xd0, 0xab, 0x6f, 0xb5, 0xfd, 0x29, 0xe3, 0xab, 0x6b, 0xed, 0xc6, 0x6a, 0x93, 0xf6,
|
||||
0x60, 0x61, 0x30, 0xbd, 0x2d, 0xcb, 0x2d, 0xd2, 0x1d, 0x68, 0xf9, 0x41, 0x7c, 0x82, 0x5b, 0xb1,
|
||||
0x80, 0x8f, 0x22, 0x26, 0xa4, 0x89, 0xb6, 0x8c, 0x56, 0xca, 0xa3, 0x40, 0xaa, 0x95, 0x06, 0xae,
|
||||
0x18, 0x8a, 0xde, 0x81, 0xd6, 0x97, 0xc9, 0x24, 0x96, 0x64, 0x1d, 0x5a, 0x43, 0xf5, 0x61, 0x34,
|
||||
0x35, 0x41, 0xff, 0x00, 0xf7, 0x70, 0x39, 0xf7, 0xa6, 0xe2, 0xc9, 0xc5, 0x51, 0x70, 0xca, 0xb2,
|
||||
0x48, 0xbf, 0x07, 0x2d, 0xae, 0xb6, 0x47, 0xc5, 0x45, 0xaf, 0xa3, 0xa2, 0x0f, 0xfd, 0xf1, 0x35,
|
||||
0x5f, 0x59, 0x8e, 0x95, 0x82, 0x09, 0x70, 0x4d, 0xd0, 0xbf, 0xd5, 0x61, 0x09, 0x4d, 0x1b, 0x73,
|
||||
0xe4, 0x0b, 0x58, 0x1a, 0xe6, 0x68, 0x13, 0xcc, 0xb7, 0x95, 0xb9, 0xbc, 0x5c, 0x3e, 0x8a, 0x0b,
|
||||
0x0a, 0xee, 0xa7, 0x85, 0x60, 0x26, 0x70, 0x4d, 0x6d, 0x64, 0xee, 0x0a, 0xbf, 0xa7, 0x67, 0x6c,
|
||||
0xe4, 0xcf, 0xd8, 0x87, 0x3b, 0xb8, 0x41, 0xbe, 0xe4, 0x89, 0x27, 0x17, 0x87, 0xfd, 0xf4, 0x84,
|
||||
0xaa, 0x72, 0x8d, 0x4d, 0x75, 0x6b, 0x44, 0xe3, 0xe9, 0x89, 0x1b, 0xf6, 0x13, 0xd3, 0xbf, 0xd7,
|
||||
0xe1, 0x3e, 0x9a, 0x3c, 0x8c, 0xcf, 0xde, 0xbf, 0x44, 0xb8, 0xd0, 0x7e, 0x95, 0x08, 0x89, 0xa7,
|
||||
0xd1, 0x75, 0x2d, 0xa3, 0xa7, 0xae, 0x34, 0x2b, 0x5c, 0x19, 0x00, 0x41, 0x4f, 0x5e, 0xf0, 0x90,
|
||||
0xf1, 0x6c, 0xeb, 0x4d, 0xe8, 0x04, 0x43, 0x3c, 0x7d, 0xb6, 0xeb, 0x94, 0x71, 0xf5, 0xf9, 0x9e,
|
||||
0xc1, 0x3a, 0x1a, 0x7d, 0xfa, 0xcd, 0xde, 0xd1, 0x80, 0xc9, 0xcc, 0x6c, 0x17, 0x16, 0xce, 0xa3,
|
||||
0x38, 0x4c, 0xce, 0x8d, 0x4d, 0x43, 0x55, 0x17, 0x39, 0xfa, 0x08, 0xd6, 0x8d, 0x91, 0xfd, 0x9f,
|
||||
0x22, 0x31, 0xb5, 0x94, 0xd3, 0xa8, 0x17, 0x35, 0xfa, 0xd0, 0xeb, 0x73, 0x76, 0x16, 0x25, 0x13,
|
||||
0x91, 0x0b, 0xca, 0xa2, 0x76, 0x55, 0x21, 0x5b, 0x87, 0x16, 0x67, 0x27, 0x87, 0x7b, 0xe9, 0xfb,
|
||||
0x23, 0xa1, 0x32, 0x4c, 0xab, 0x2b, 0x3d, 0x86, 0x5f, 0xa8, 0xd7, 0xf6, 0x0d, 0x45, 0x25, 0xac,
|
||||
0xee, 0x86, 0xa1, 0x4e, 0xc3, 0x74, 0x8f, 0xcc, 0x56, 0x3d, 0x67, 0x2b, 0x97, 0xa3, 0x8d, 0x42,
|
||||
0xa5, 0x73, 0xe0, 0xfa, 0x90, 0x33, 0xac, 0x64, 0xba, 0xa0, 0xa7, 0xa4, 0x5a, 0x61, 0x98, 0xf0,
|
||||
0xc2, 0xd4, 0xb8, 0x94, 0xa4, 0xaf, 0x61, 0x63, 0x37, 0x0c, 0x73, 0x87, 0x4c, 0xb7, 0x5e, 0x85,
|
||||
0x66, 0xc8, 0x78, 0xda, 0x6e, 0x43, 0xc6, 0xed, 0x07, 0x53, 0x29, 0xa0, 0x4a, 0x11, 0xee, 0xb8,
|
||||
0xe4, 0xe3, 0xb7, 0x72, 0x30, 0x12, 0x62, 0x92, 0x55, 0x54, 0x43, 0xd1, 0x47, 0xd0, 0x2d, 0x6f,
|
||||
0x66, 0x0a, 0x98, 0xba, 0xcc, 0xe8, 0x24, 0xad, 0x29, 0xea, 0x32, 0x91, 0xa2, 0x7d, 0x58, 0xc2,
|
||||
0xa0, 0xca, 0x67, 0x49, 0x0e, 0x22, 0x90, 0x47, 0x70, 0x73, 0x22, 0xd8, 0xb1, 0x57, 0x0c, 0x7e,
|
||||
0xf4, 0xb0, 0xed, 0xdb, 0x96, 0xe8, 0x73, 0xa0, 0x69, 0x53, 0x45, 0xcb, 0xf6, 0xb4, 0x29, 0xef,
|
||||
0xd3, 0x85, 0x85, 0x60, 0x38, 0x94, 0xd9, 0xe1, 0x0d, 0x45, 0x2f, 0xe0, 0xd6, 0x01, 0xd3, 0x71,
|
||||
0xff, 0x34, 0xe1, 0x85, 0x92, 0x35, 0x55, 0xa9, 0xe7, 0x55, 0xec, 0x95, 0xaa, 0xea, 0x20, 0xcd,
|
||||
0xea, 0x83, 0xfc, 0xb3, 0x0e, 0xce, 0x01, 0x93, 0x3f, 0x1b, 0x32, 0x50, 0x0d, 0x93, 0xb3, 0x1f,
|
||||
0x27, 0x11, 0x37, 0xbe, 0x5c, 0xea, 0x60, 0x6a, 0xfb, 0x65, 0x36, 0xfd, 0x47, 0x1d, 0x96, 0x4b,
|
||||
0xf0, 0xe1, 0xd7, 0x69, 0x7b, 0xd7, 0x15, 0xf7, 0x8e, 0x4a, 0xf7, 0x39, 0xc8, 0x01, 0x65, 0xff,
|
||||
0xff, 0xc8, 0xe1, 0x39, 0xdc, 0xdb, 0x0d, 0x43, 0x1b, 0x1a, 0xcc, 0x6e, 0xee, 0xa3, 0xa2, 0xa3,
|
||||
0xf3, 0xac, 0x3d, 0x80, 0xd5, 0x12, 0xfe, 0xc4, 0x6b, 0x8b, 0xc2, 0xb4, 0x9e, 0xa8, 0x4f, 0x4a,
|
||||
0x67, 0xa4, 0xbc, 0x19, 0xa4, 0xfb, 0x21, 0xac, 0x15, 0x64, 0xbc, 0x92, 0xa9, 0xa6, 0x36, 0x75,
|
||||
0x09, 0x8e, 0x8f, 0x88, 0xc2, 0x92, 0xaf, 0x73, 0xe0, 0x0f, 0xd7, 0x98, 0xc4, 0x44, 0xae, 0xa6,
|
||||
0x54, 0xde, 0x2a, 0x74, 0x63, 0x1e, 0x18, 0xbf, 0x55, 0x13, 0xe0, 0x29, 0xcc, 0xb8, 0x86, 0xf9,
|
||||
0x9c, 0xd1, 0xf4, 0xaf, 0x0d, 0xd8, 0x7c, 0x1a, 0xc5, 0xc1, 0x28, 0xba, 0x64, 0x56, 0x1c, 0x6d,
|
||||
0x49, 0x19, 0x83, 0xbb, 0x1a, 0x05, 0xdc, 0x95, 0xab, 0x45, 0xcd, 0x42, 0x2d, 0xc2, 0x86, 0x21,
|
||||
0x25, 0x3b, 0x1d, 0xa7, 0x58, 0xac, 0xe3, 0x4f, 0x19, 0x64, 0x0f, 0xd6, 0xb0, 0xcf, 0x99, 0x4d,
|
||||
0x87, 0x09, 0x0f, 0x85, 0xd3, 0xc2, 0x47, 0xea, 0xea, 0x47, 0x3a, 0x2e, 0x2d, 0xfb, 0xb3, 0x0a,
|
||||
0xe4, 0x31, 0xac, 0x4c, 0x99, 0xfb, 0x9c, 0x27, 0x1c, 0xb1, 0xda, 0xa2, 0xb7, 0xae, 0x6d, 0xf4,
|
||||
0x79, 0xf2, 0xfd, 0x88, 0x9d, 0xee, 0x31, 0x19, 0x44, 0x23, 0xe1, 0x97, 0x85, 0xbd, 0x7f, 0x3b,
|
||||
0xb0, 0x3a, 0x90, 0x09, 0x0f, 0x4e, 0xd2, 0x5b, 0x90, 0x17, 0x64, 0x07, 0x56, 0x0e, 0x58, 0xa1,
|
||||
0xb5, 0x13, 0x82, 0xfd, 0xac, 0x90, 0x6c, 0x2e, 0xd1, 0x5b, 0xe4, 0xb9, 0xb4, 0x46, 0x7e, 0x0b,
|
||||
0xeb, 0x25, 0xe5, 0x27, 0x17, 0x6a, 0xde, 0x59, 0x56, 0x16, 0xa6, 0xf3, 0x4f, 0x85, 0xf6, 0x63,
|
||||
0x58, 0x2d, 0x17, 0x01, 0x72, 0x73, 0x26, 0xb9, 0x0e, 0xf7, 0x5c, 0x5b, 0x20, 0xd3, 0x1a, 0xf9,
|
||||
0x16, 0x0b, 0x98, 0x2d, 0x23, 0x08, 0x42, 0xfc, 0xf9, 0xc3, 0x53, 0x95, 0xd5, 0x63, 0xe8, 0xda,
|
||||
0x27, 0x17, 0x72, 0xdf, 0x18, 0xad, 0x9e, 0x6a, 0xdc, 0x5b, 0x15, 0xa3, 0x05, 0xad, 0x91, 0x5f,
|
||||
0xc1, 0xf2, 0x01, 0xcb, 0xe3, 0x44, 0x02, 0x4a, 0x58, 0x37, 0x4d, 0x77, 0x4d, 0x3b, 0x93, 0x5b,
|
||||
0xa6, 0x35, 0xf2, 0x09, 0xac, 0xa9, 0x33, 0x70, 0x36, 0x7c, 0x1b, 0xad, 0x1d, 0x7c, 0x94, 0xd9,
|
||||
0x21, 0x23, 0xaf, 0xb8, 0x81, 0xa8, 0xb1, 0x2c, 0x42, 0x6b, 0x64, 0x00, 0x4e, 0x15, 0x9e, 0x25,
|
||||
0xbf, 0xc8, 0xa0, 0x66, 0x35, 0xda, 0x75, 0x57, 0xcb, 0x78, 0x94, 0xd6, 0xc8, 0x33, 0xe8, 0xda,
|
||||
0x01, 0xa4, 0xbe, 0xd2, 0xb9, 0xe0, 0xd2, 0xed, 0x64, 0x22, 0xb4, 0x46, 0xbe, 0x86, 0xdb, 0x15,
|
||||
0xd2, 0x88, 0xa4, 0xdf, 0xd6, 0xdc, 0xe7, 0xe0, 0xe2, 0xa7, 0xb5, 0xaa, 0x5a, 0xf3, 0xa0, 0xa0,
|
||||
0xee, 0xc1, 0x62, 0x0e, 0x3b, 0x92, 0x6e, 0xb6, 0x56, 0x00, 0x93, 0x45, 0x9d, 0xbe, 0xd9, 0xd2,
|
||||
0x8a, 0x7c, 0xc9, 0x87, 0x99, 0xe8, 0x3c, 0x64, 0x5c, 0xb4, 0xf8, 0x29, 0xdc, 0x28, 0x80, 0x4d,
|
||||
0xe2, 0x64, 0xab, 0x25, 0xfc, 0x59, 0xd4, 0xfb, 0x0c, 0x6e, 0x14, 0xa0, 0xa5, 0xd6, 0xb3, 0xa1,
|
||||
0x4d, 0x17, 0x43, 0x47, 0xb3, 0x68, 0x8d, 0xbc, 0x80, 0x0f, 0x2a, 0x11, 0x26, 0x79, 0xa0, 0x44,
|
||||
0xaf, 0x02, 0xa0, 0x25, 0x83, 0xbf, 0xc3, 0x38, 0x2f, 0x76, 0x11, 0xb2, 0x6e, 0xa9, 0x04, 0x5e,
|
||||
0x55, 0xd2, 0x3e, 0x03, 0x32, 0x83, 0x27, 0x3c, 0xb2, 0x69, 0x12, 0xd6, 0x7e, 0x89, 0x64, 0xb6,
|
||||
0x8f, 0xd3, 0x1a, 0x79, 0x89, 0xc8, 0xc4, 0x16, 0x10, 0xde, 0xfb, 0x54, 0x95, 0xc7, 0x26, 0x70,
|
||||
0xad, 0x91, 0xe6, 0x5d, 0x1d, 0x6a, 0x7f, 0x86, 0xcd, 0x39, 0xd0, 0xcf, 0x23, 0x0f, 0xf3, 0xb5,
|
||||
0xa9, 0x1a, 0x1c, 0x56, 0x1c, 0xfa, 0x1b, 0xe3, 0x9d, 0x35, 0xe8, 0xbc, 0x77, 0x8a, 0x4a, 0x1f,
|
||||
0x8b, 0xf3, 0xb1, 0xcd, 0xdc, 0x1b, 0xd4, 0x51, 0xbb, 0x9b, 0x3b, 0xb0, 0x72, 0xc4, 0xce, 0x4b,
|
||||
0xbd, 0x6a, 0xa6, 0xb3, 0x54, 0x74, 0x9b, 0xcf, 0x80, 0xe8, 0x1f, 0x45, 0xae, 0xd4, 0x5f, 0xd4,
|
||||
0xbc, 0xfd, 0xd3, 0xb1, 0xbc, 0xa0, 0x35, 0xb2, 0x0f, 0xb7, 0x8e, 0xd8, 0xb9, 0xb5, 0xcd, 0xd8,
|
||||
0x1e, 0xbb, 0x2a, 0x02, 0x76, 0x60, 0xc3, 0x8a, 0x41, 0xec, 0x46, 0x4a, 0x3e, 0x1c, 0xc2, 0x72,
|
||||
0x71, 0xfa, 0x20, 0x1f, 0xe0, 0x0d, 0xd9, 0xc6, 0x1f, 0xd7, 0xb5, 0x2d, 0x19, 0x28, 0xa4, 0x6a,
|
||||
0xde, 0x9a, 0xc2, 0x91, 0xc5, 0xa6, 0x32, 0xc7, 0x5a, 0xc9, 0x93, 0x47, 0xd0, 0xc9, 0x46, 0x3d,
|
||||
0x93, 0xa3, 0xa5, 0xc9, 0xaf, 0xac, 0xb1, 0x03, 0xdd, 0x3d, 0x16, 0x0c, 0x65, 0x74, 0x36, 0x7b,
|
||||
0xf9, 0xb3, 0x51, 0x5f, 0x52, 0xfe, 0x1c, 0x6e, 0x4d, 0x95, 0xdf, 0x00, 0x2a, 0x94, 0xd4, 0x1f,
|
||||
0x42, 0xfb, 0x88, 0x9d, 0x63, 0x3e, 0x10, 0xb3, 0x84, 0x84, 0x9b, 0x27, 0xf0, 0x54, 0x64, 0x60,
|
||||
0x66, 0xa1, 0x3e, 0x4f, 0x86, 0x4c, 0x88, 0x28, 0x3e, 0xb1, 0x6a, 0xa4, 0x96, 0x7f, 0x09, 0x37,
|
||||
0x52, 0x0d, 0x44, 0x57, 0x57, 0x09, 0xa7, 0x6f, 0x5f, 0xed, 0xcb, 0x54, 0xb8, 0x9d, 0xce, 0x65,
|
||||
0x04, 0xbb, 0x69, 0x7e, 0x8a, 0x2c, 0x3b, 0xfe, 0x27, 0xb8, 0x3d, 0x27, 0xeb, 0xdf, 0xb3, 0x2c,
|
||||
0x68, 0x80, 0x56, 0x98, 0x10, 0xc9, 0x6d, 0x63, 0xd1, 0x36, 0x37, 0x96, 0x9d, 0x3b, 0x98, 0xad,
|
||||
0xeb, 0xe2, 0x9d, 0x8a, 0xf2, 0x77, 0xe0, 0x54, 0xcd, 0x3e, 0x1a, 0x95, 0x5c, 0x31, 0x19, 0xb9,
|
||||
0xb6, 0x66, 0x62, 0x3a, 0xcf, 0xcc, 0x54, 0xa2, 0x3d, 0xac, 0x1a, 0x56, 0xca, 0xaf, 0xe5, 0x03,
|
||||
0x39, 0x62, 0xe7, 0xe5, 0x12, 0xf7, 0x46, 0x4e, 0x6d, 0xcc, 0x38, 0xe5, 0x69, 0xaf, 0x0e, 0xa1,
|
||||
0x6b, 0x2d, 0x15, 0x1e, 0xe9, 0x61, 0x8b, 0x9e, 0x33, 0xca, 0x94, 0xdd, 0xfb, 0x02, 0x9c, 0x8a,
|
||||
0xfc, 0xa9, 0xea, 0xb0, 0x45, 0x03, 0x4f, 0xae, 0xff, 0xb1, 0x85, 0xff, 0x79, 0xfc, 0x2f, 0x00,
|
||||
0x00, 0xff, 0xff, 0x50, 0xdd, 0x35, 0xd0, 0x22, 0x19, 0x00, 0x00,
|
||||
}
|
||||
|
||||
// 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)
|
||||
GetValidAuthorizations(ctx context.Context, in *GetValidAuthorizationsRequest, opts ...grpc.CallOption) (*ValidAuthorizations, 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)
|
||||
CountCertificatesByNames(ctx context.Context, in *CountCertificatesByNamesRequest, opts ...grpc.CallOption) (*CountByNames, 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
|
||||
}
|
||||
|
||||
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) {
|
||||
out := new(CertificateStatus)
|
||||
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)
|
||||
GetValidAuthorizations(context.Context, *GetValidAuthorizationsRequest) (*ValidAuthorizations, error)
|
||||
GetCertificate(context.Context, *Serial) (*proto1.Certificate, error)
|
||||
GetPrecertificate(context.Context, *Serial) (*proto1.Certificate, error)
|
||||
GetCertificateStatus(context.Context, *Serial) (*CertificateStatus, error)
|
||||
CountCertificatesByNames(context.Context, *CountCertificatesByNamesRequest) (*CountByNames, 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) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
in := new(Serial)
|
||||
if err := dec(in); err != nil {
|
||||
|
@ -3467,6 +3499,10 @@ var _StorageAuthority_serviceDesc = grpc.ServiceDesc{
|
|||
MethodName: "GetCertificate",
|
||||
Handler: _StorageAuthority_GetCertificate_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetPrecertificate",
|
||||
Handler: _StorageAuthority_GetPrecertificate_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetCertificateStatus",
|
||||
Handler: _StorageAuthority_GetCertificateStatus_Handler,
|
||||
|
|
|
@ -13,6 +13,7 @@ service StorageAuthority {
|
|||
rpc GetPendingAuthorization(GetPendingAuthorizationRequest) returns (core.Authorization) {}
|
||||
rpc GetValidAuthorizations(GetValidAuthorizationsRequest) returns (ValidAuthorizations) {}
|
||||
rpc GetCertificate(Serial) returns (core.Certificate) {}
|
||||
rpc GetPrecertificate(Serial) returns (core.Certificate) {}
|
||||
rpc GetCertificateStatus(Serial) returns (CertificateStatus) {}
|
||||
rpc CountCertificatesByNames(CountCertificatesByNamesRequest) returns (CountByNames) {}
|
||||
rpc CountRegistrationsByIP(CountRegistrationsByIPRequest) returns (Count) {}
|
||||
|
|
19
sa/sa.go
19
sa/sa.go
|
@ -465,6 +465,25 @@ func (ssa *SQLStorageAuthority) GetCertificate(ctx context.Context, serial strin
|
|||
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
|
||||
// number of a certificate and returns data about that certificate's current
|
||||
// validity.
|
||||
|
|
|
@ -54,7 +54,8 @@
|
|||
"HeadNonceStatusOK": true,
|
||||
"NewAuthorizationSchema": true,
|
||||
"RemoveWFE2AccountID": true,
|
||||
"MandatoryPOSTAsGET": true
|
||||
"MandatoryPOSTAsGET": true,
|
||||
"PrecertificateRevocation": true
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -9,6 +9,7 @@ import (
|
|||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -17,6 +18,10 @@ import (
|
|||
"github.com/eggsampler/acme/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
OIDExtensionCTPoison = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}
|
||||
)
|
||||
|
||||
func random_domain() string {
|
||||
var bytes [3]byte
|
||||
rand.Read(bytes[:])
|
||||
|
@ -80,11 +85,15 @@ type issuanceResult struct {
|
|||
certs []*x509.Certificate
|
||||
}
|
||||
|
||||
func authAndIssue(domains []string) (*issuanceResult, error) {
|
||||
c, err := makeClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func authAndIssue(c *client, csrKey *ecdsa.PrivateKey, domains []string) (*issuanceResult, error) {
|
||||
var err error
|
||||
if c == nil {
|
||||
c, err = makeClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var ids []acme.Identifier
|
||||
for _, domain := range domains {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -132,19 +141,22 @@ func authAndIssue(domains []string) (*issuanceResult, error) {
|
|||
return &issuanceResult{order, certs}, nil
|
||||
}
|
||||
|
||||
func makeCSR(domains []string) (*x509.CertificateRequest, error) {
|
||||
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generating certificate key: %s", err)
|
||||
func makeCSR(k *ecdsa.PrivateKey, domains []string) (*x509.CertificateRequest, error) {
|
||||
var err error
|
||||
if k == nil {
|
||||
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{
|
||||
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||
PublicKeyAlgorithm: x509.ECDSA,
|
||||
PublicKey: certKey.Public(),
|
||||
PublicKey: k.Public(),
|
||||
Subject: pkix.Name{CommonName: domains[0]},
|
||||
DNSNames: domains,
|
||||
}, certKey)
|
||||
}, k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making csr: %s", err)
|
||||
}
|
||||
|
|
|
@ -3,16 +3,13 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
ocsp_helper "github.com/letsencrypt/boulder/test/ocsp/helper"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
func TestPrecertificateOCSP(t *testing.T) {
|
||||
|
@ -21,21 +18,13 @@ func TestPrecertificateOCSP(t *testing.T) {
|
|||
return
|
||||
}
|
||||
domain := random_domain()
|
||||
for _, port := range []int{4500, 4501, 4510, 4511} {
|
||||
url := fmt.Sprintf("http://boulder:%d/add-reject-host", port)
|
||||
body := []byte(fmt.Sprintf(`{"host": "%s"}`, domain))
|
||||
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()
|
||||
err := ctAddRejectHost(domain)
|
||||
if err != nil {
|
||||
t.Fatalf("adding ct-test-srv reject host: %s", err)
|
||||
}
|
||||
|
||||
os.Setenv("DIRECTORY", "http://boulder:4001/directory")
|
||||
_, err := authAndIssue([]string{domain})
|
||||
_, err = authAndIssue(nil, nil, []string{domain})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "urn:ietf:params:acme:error:serverInternal") &&
|
||||
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")
|
||||
}
|
||||
|
||||
resp, err := http.Get("http://boulder:4500/get-rejections")
|
||||
rejections, err := ctGetRejections(4500)
|
||||
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 {
|
||||
rejectedCertBytes, err := base64.StdEncoding.DecodeString(r)
|
||||
if err != nil {
|
||||
t.Fatalf("decoding rejected cert: %s", err)
|
||||
}
|
||||
_, err = ocsp_helper.ReqDER(rejectedCertBytes)
|
||||
_, err = ocsp_helper.ReqDER(rejectedCertBytes, ocsp.Good)
|
||||
if err != nil {
|
||||
t.Errorf("requesting OCSP for rejected precertificate: %s", err)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
|
@ -96,10 +96,10 @@ func Req(fileName string) (*ocsp.Response, error) {
|
|||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing certificate: %s", err)
|
||||
|
@ -150,7 +150,7 @@ func ReqDER(der []byte) (*ocsp.Response, error) {
|
|||
if len(respBytes) == 0 {
|
||||
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) {
|
||||
|
@ -194,14 +194,14 @@ func getOCSPURL(cert *x509.Certificate) (*url.URL, error) {
|
|||
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))
|
||||
resp, err := ocsp.ParseResponseForCert(respBytes, cert, issuer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing response: %s", err)
|
||||
}
|
||||
if resp.Status != *expectStatus {
|
||||
return nil, fmt.Errorf("wrong CertStatus %d, expected %d", resp.Status, *expectStatus)
|
||||
if resp.Status != expectStatus {
|
||||
return nil, fmt.Errorf("wrong CertStatus %d, expected %d", resp.Status, expectStatus)
|
||||
}
|
||||
timeTilExpiry := time.Until(resp.NextUpdate)
|
||||
tooSoonDuration := time.Duration(*tooSoon) * time.Hour
|
||||
|
|
|
@ -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 ON orderToAuthz2 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
|
||||
GRANT SELECT ON certificateStatus TO 'ocsp_resp'@'localhost';
|
||||
|
|
104
wfe2/wfe.go
104
wfe2/wfe.go
|
@ -83,6 +83,11 @@ type WebFrontEndImpl struct {
|
|||
// sorted from leaf to root
|
||||
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)
|
||||
SubscriberAgreementURL string
|
||||
|
||||
|
@ -121,6 +126,7 @@ func NewWebFrontEndImpl(
|
|||
clk clock.Clock,
|
||||
keyPolicy goodkey.KeyPolicy,
|
||||
certificateChains map[string][]byte,
|
||||
issuerCertificates []*x509.Certificate,
|
||||
remoteNonceService noncepb.NonceServiceClient,
|
||||
noncePrefixMap map[string]noncepb.NonceServiceClient,
|
||||
logger blog.Logger,
|
||||
|
@ -130,6 +136,7 @@ func NewWebFrontEndImpl(
|
|||
clk: clk,
|
||||
keyPolicy: keyPolicy,
|
||||
certificateChains: certificateChains,
|
||||
issuerCertificates: issuerCertificates,
|
||||
stats: initStats(scope),
|
||||
scope: scope,
|
||||
remoteNonceService: remoteNonceService,
|
||||
|
@ -695,22 +702,65 @@ func (wfe *WebFrontEndImpl) processRevocation(
|
|||
// Compute and record the serial number of the provided certificate
|
||||
serial := core.SerialToString(providedCert.SerialNumber)
|
||||
logEvent.Extra["ProvidedCertificateSerial"] = serial
|
||||
notFoundProb := probs.NotFound("No such certificate")
|
||||
|
||||
// Lookup the certificate by the serial. If the certificate wasn't found, or
|
||||
// it wasn't a byte-for-byte match to the certificate requested for
|
||||
// revocation, return an error
|
||||
cert, err := wfe.SA.GetCertificate(ctx, serial)
|
||||
if err != nil || !bytes.Equal(cert.DER, revokeRequest.CertificateDER) {
|
||||
return probs.NotFound("No such certificate")
|
||||
var certDER []byte
|
||||
// If the PrecertificateRevocation feature flag is enabled that means we won't
|
||||
// do a byte-for-byte comparison of the providedCert against the stored cert
|
||||
// returned by SA.GetCertificate because a precert will always fail this
|
||||
// check. Instead, perform a signature validation of the providedCert using
|
||||
// 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
|
||||
parsedCertificate, err := x509.ParseCertificate(cert.DER)
|
||||
parsedCertificate, err := x509.ParseCertificate(certDER)
|
||||
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")
|
||||
}
|
||||
logEvent.Extra["RetrievedCertificateSerial"] = core.SerialToString(parsedCertificate.SerialNumber)
|
||||
logEvent.Extra["RetrievedCertificateSerial"] = serial
|
||||
logEvent.Extra["RetrievedCertificateDNSNames"] = parsedCertificate.DNSNames
|
||||
|
||||
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
|
||||
// of the names in the certificate or was the issuing account
|
||||
authorizedToRevoke := func(parsedCertificate *x509.Certificate) *probs.ProblemDetails {
|
||||
cert, err := wfe.SA.GetCertificate(ctx, core.SerialToString(parsedCertificate.SerialNumber))
|
||||
if err != nil {
|
||||
// Try to find a stored final certificate for the serial number
|
||||
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")
|
||||
}
|
||||
// If the cert/precert is owned by the requester then return nil, it is an
|
||||
// authorized revocation.
|
||||
if cert.RegistrationID == acct.ID {
|
||||
return nil
|
||||
}
|
||||
// Otherwise check if the account, while not the owner, has equivalent authorizations
|
||||
valid, err := wfe.acctHoldsAuthorizations(ctx, acct.ID, parsedCertificate.DNSNames)
|
||||
if err != nil {
|
||||
return probs.ServerInternal("Failed to retrieve authorizations for names in certificate")
|
||||
}
|
||||
// If it doesn't, return an unauthorized problem.
|
||||
if !valid {
|
||||
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")
|
||||
}
|
||||
// If it does, return nil. It is an an authorized revocation.
|
||||
return nil
|
||||
}
|
||||
return wfe.processRevocation(ctx, jwsBody, acct.ID, authorizedToRevoke, request, logEvent)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
|
@ -352,12 +353,18 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock) {
|
|||
|
||||
chainPEM, err := ioutil.ReadFile("../test/test-ca2.pem")
|
||||
test.AssertNotError(t, err, "Unable to read ../test/test-ca2.pem")
|
||||
chainDER, _ := pem.Decode(chainPEM)
|
||||
|
||||
certChains := map[string][]byte{
|
||||
"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")
|
||||
|
||||
wfe.SubscriberAgreementURL = agreementURL
|
||||
|
@ -2542,11 +2549,15 @@ func makeRevokeRequestJSON(reason *revocation.Reason) ([]byte, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return makeRevokeRequestJSONForCert(certBlock.Bytes, reason)
|
||||
}
|
||||
|
||||
func makeRevokeRequestJSONForCert(der []byte, reason *revocation.Reason) ([]byte, error) {
|
||||
revokeRequest := struct {
|
||||
CertificateDER core.JSONBuffer `json:"certificate"`
|
||||
Reason *revocation.Reason `json:"reason"`
|
||||
}{
|
||||
CertificateDER: certBlock.Bytes,
|
||||
CertificateDER: der,
|
||||
Reason: reason,
|
||||
}
|
||||
revokeRequestJSON, err := json.Marshal(revokeRequest)
|
||||
|
@ -2591,6 +2602,60 @@ func TestRevokeCertificateCertKey(t *testing.T) {
|
|||
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) {
|
||||
wfe, fc := setupWFE(t)
|
||||
ra := wfe.RA.(*MockRegistrationAuthority)
|
||||
|
|
Loading…
Reference in New Issue