Submit all issued certificates to configured CT logs
Adds a new service, Publisher, which exists to submit issued certificates to various Certificate Transparency logs. Once submitted the Publisher will also parse and store the returned SCT (Signed Certificate Timestamp) receipts that are used to prove inclusion in a specific log in the SA database. A SA migration adds the new SCT receipt table. The Publisher only exposes one method, SubmitToCT, which is called in a goroutine by ca.IssueCertificate as to not block any other issuance operations. This method will iterate through all of the configured logs attempting to submit the certificate, and any required intermediate certificates, to them. If a submission to a log fails it will be retried the pre-configured number of times and will either use a back-off set in a Retry-After header or a pre-configured back-off between submission attempts. This changeset is the first of a number of changes ending with serving SCT receipts in OCSP responses and purposefully leaves out the following pieces for follow-up PRs. * A fake CT server for integration testing * A external tool to search the database for certificates lacking a full set of SCT receipts * A method to construct X.509 v3 extensions containing receipts for the OCSP responder * Returned SCT signature verification (beyond just checking that the signature is of the correct type so we aren't just serving arbitrary binary blobs to clients) Resolves #95.
This commit is contained in:
parent
ac095b0467
commit
ff6eca7a29
|
@ -53,6 +53,7 @@ type CertificateAuthorityImpl struct {
|
|||
SA core.StorageAuthority
|
||||
PA core.PolicyAuthority
|
||||
DB core.CertificateAuthorityDatabase
|
||||
Publisher core.Publisher
|
||||
Clk clock.Clock // TODO(jmhodges): should be private, like log
|
||||
log *blog.AuditLogger
|
||||
Prefix int // Prepended to the serial number
|
||||
|
@ -95,7 +96,7 @@ func NewCertificateAuthorityImpl(cadb core.CertificateAuthorityDatabase, config
|
|||
return nil, err
|
||||
}
|
||||
|
||||
issuer, err := loadIssuer(issuerCert)
|
||||
issuer, err := core.LoadCert(issuerCert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -170,19 +171,6 @@ func loadKey(keyConfig cmd.KeyConfig) (priv crypto.Signer, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func loadIssuer(filename string) (issuerCert *x509.Certificate, err error) {
|
||||
if filename == "" {
|
||||
err = errors.New("Issuer certificate was not provided in config.")
|
||||
return
|
||||
}
|
||||
issuerCertPEM, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
issuerCert, err = helpers.ParseCertificatePEM(issuerCertPEM)
|
||||
return
|
||||
}
|
||||
|
||||
// GenerateOCSP produces a new OCSP response and returns it
|
||||
func (ca *CertificateAuthorityImpl) GenerateOCSP(xferObj core.OCSPSigningRequest) ([]byte, error) {
|
||||
cert, err := x509.ParseCertificate(xferObj.CertDER)
|
||||
|
@ -402,7 +390,6 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
|
|||
// Attempt to generate the OCSP Response now. If this raises an error, it is
|
||||
// logged but is not returned to the caller, as an error at this point does
|
||||
// not constitute an issuance failure.
|
||||
|
||||
certObj, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
ca.log.Warning(fmt.Sprintf("Post-Issuance OCSP failed parsing Certificate: %s", err))
|
||||
|
@ -410,7 +397,6 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
|
|||
}
|
||||
|
||||
serial := core.SerialToString(certObj.SerialNumber)
|
||||
|
||||
signRequest := ocsp.SignRequest{
|
||||
Certificate: certObj,
|
||||
Status: string(core.OCSPStatusGood),
|
||||
|
@ -428,6 +414,9 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
|
|||
return cert, nil
|
||||
}
|
||||
|
||||
// Submit the certificate to any configured CT logs
|
||||
go ca.Publisher.SubmitToCT(certObj.Raw)
|
||||
|
||||
// Do not return an err at this point; caller must know that the Certificate
|
||||
// was issued. (Also, it should be impossible for err to be non-nil here)
|
||||
return cert, nil
|
||||
|
|
|
@ -205,9 +205,9 @@ func TestRevoke(t *testing.T) {
|
|||
defer ctx.cleanUp()
|
||||
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
|
||||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
|
||||
ca.PA = ctx.pa
|
||||
ca.SA = ctx.sa
|
||||
ca.Publisher = &mocks.MockPublisher{}
|
||||
|
||||
csr, _ := x509.ParseCertificateRequest(CNandSANCSR)
|
||||
certObj, err := ca.IssueCertificate(*csr, ctx.reg.ID)
|
||||
|
@ -246,6 +246,7 @@ func TestIssueCertificate(t *testing.T) {
|
|||
defer ctx.cleanUp()
|
||||
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
|
||||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
ca.Publisher = &mocks.MockPublisher{}
|
||||
ca.PA = ctx.pa
|
||||
ca.SA = ctx.sa
|
||||
|
||||
|
@ -322,6 +323,7 @@ func TestRejectNoName(t *testing.T) {
|
|||
defer ctx.cleanUp()
|
||||
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
|
||||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
ca.Publisher = &mocks.MockPublisher{}
|
||||
ca.PA = ctx.pa
|
||||
ca.SA = ctx.sa
|
||||
|
||||
|
@ -338,6 +340,7 @@ func TestRejectTooManyNames(t *testing.T) {
|
|||
defer ctx.cleanUp()
|
||||
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
|
||||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
ca.Publisher = &mocks.MockPublisher{}
|
||||
ca.PA = ctx.pa
|
||||
ca.SA = ctx.sa
|
||||
|
||||
|
@ -352,6 +355,7 @@ func TestDeduplication(t *testing.T) {
|
|||
defer ctx.cleanUp()
|
||||
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
|
||||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
ca.Publisher = &mocks.MockPublisher{}
|
||||
ca.PA = ctx.pa
|
||||
ca.SA = ctx.sa
|
||||
|
||||
|
@ -381,6 +385,7 @@ func TestRejectValidityTooLong(t *testing.T) {
|
|||
defer ctx.cleanUp()
|
||||
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
|
||||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
ca.Publisher = &mocks.MockPublisher{}
|
||||
ca.PA = ctx.pa
|
||||
ca.SA = ctx.sa
|
||||
|
||||
|
@ -395,6 +400,7 @@ func TestShortKey(t *testing.T) {
|
|||
ctx := setup(t)
|
||||
defer ctx.cleanUp()
|
||||
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
|
||||
ca.Publisher = &mocks.MockPublisher{}
|
||||
ca.PA = ctx.pa
|
||||
ca.SA = ctx.sa
|
||||
|
||||
|
@ -408,6 +414,7 @@ func TestRejectBadAlgorithm(t *testing.T) {
|
|||
ctx := setup(t)
|
||||
defer ctx.cleanUp()
|
||||
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
|
||||
ca.Publisher = &mocks.MockPublisher{}
|
||||
ca.PA = ctx.pa
|
||||
ca.SA = ctx.sa
|
||||
|
||||
|
|
|
@ -57,6 +57,13 @@ func main() {
|
|||
sac, err := rpc.NewStorageAuthorityClient(saRPC)
|
||||
cmd.FailOnError(err, "Failed to create SA client")
|
||||
|
||||
pubRPC, err := rpc.NewAmqpRPCClient("CA->Publisher", c.AMQP.Publisher.Server, srv.Channel)
|
||||
cmd.FailOnError(err, "Unable to create RPC client")
|
||||
|
||||
pubc, err := rpc.NewPublisherClient(pubRPC)
|
||||
cmd.FailOnError(err, "Failed to create Publisher client")
|
||||
|
||||
cai.Publisher = &pubc
|
||||
cai.SA = &sac
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2015 ISRG. All rights reserved
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
||||
|
||||
"github.com/letsencrypt/boulder/cmd"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
"github.com/letsencrypt/boulder/publisher"
|
||||
"github.com/letsencrypt/boulder/rpc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cmd.NewAppShell("boulder-publisher", "Submits issued certificates to CT logs")
|
||||
app.Action = func(c cmd.Config) {
|
||||
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
|
||||
cmd.FailOnError(err, "Could not connect to statsd")
|
||||
|
||||
// Set up logging
|
||||
auditlogger, err := blog.Dial(c.Syslog.Network, c.Syslog.Server, c.Syslog.Tag, stats)
|
||||
cmd.FailOnError(err, "Could not connect to syslog")
|
||||
|
||||
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
|
||||
defer auditlogger.AuditPanic()
|
||||
|
||||
blog.SetAuditLogger(auditlogger)
|
||||
|
||||
pubi, err := publisher.NewPublisherImpl(c.Publisher.CT)
|
||||
cmd.FailOnError(err, "Could not setup Publisher")
|
||||
|
||||
go cmd.DebugServer(c.Publisher.DebugAddr)
|
||||
go cmd.ProfileCmd("Publisher", stats)
|
||||
|
||||
connectionHandler := func(srv *rpc.AmqpRPCServer) {
|
||||
saRPC, err := rpc.NewAmqpRPCClient("Publisher->SA", c.AMQP.SA.Server, srv.Channel)
|
||||
cmd.FailOnError(err, "Unable to create SA RPC client")
|
||||
|
||||
sac, err := rpc.NewStorageAuthorityClient(saRPC)
|
||||
cmd.FailOnError(err, "Unable to create SA client")
|
||||
|
||||
pubi.SA = &sac
|
||||
}
|
||||
|
||||
pubs, err := rpc.NewAmqpRPCServer(c.AMQP.Publisher.Server, connectionHandler)
|
||||
cmd.FailOnError(err, "Unable to create Publisher RPC server")
|
||||
rpc.NewPublisherServer(pubs, &pubi)
|
||||
|
||||
auditlogger.Info(app.VersionString())
|
||||
|
||||
err = pubs.Start(c)
|
||||
cmd.FailOnError(err, "Unable to run Publisher RPC server")
|
||||
}
|
||||
|
||||
app.Run()
|
||||
}
|
25
cmd/shell.go
25
cmd/shell.go
|
@ -41,6 +41,7 @@ import (
|
|||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
"github.com/letsencrypt/boulder/publisher"
|
||||
)
|
||||
|
||||
// Config stores configuration parameters that applications
|
||||
|
@ -56,14 +57,15 @@ type Config struct {
|
|||
|
||||
// General
|
||||
AMQP struct {
|
||||
Server string
|
||||
Insecure bool
|
||||
RA Queue
|
||||
VA Queue
|
||||
SA Queue
|
||||
CA Queue
|
||||
OCSP Queue
|
||||
TLS *TLSConfig
|
||||
Server string
|
||||
Insecure bool
|
||||
RA Queue
|
||||
VA Queue
|
||||
SA Queue
|
||||
CA Queue
|
||||
OCSP Queue
|
||||
Publisher Queue
|
||||
TLS *TLSConfig
|
||||
}
|
||||
|
||||
WFE struct {
|
||||
|
@ -164,6 +166,13 @@ type Config struct {
|
|||
DebugAddr string
|
||||
}
|
||||
|
||||
Publisher struct {
|
||||
CT publisher.CTConfig
|
||||
|
||||
// DebugAddr is the address to run the /debug handlers on.
|
||||
DebugAddr string
|
||||
}
|
||||
|
||||
ExternalCertImporter struct {
|
||||
CertsToImportCSVFilename string
|
||||
DomainsToImportCSVFilename string
|
||||
|
|
|
@ -111,6 +111,7 @@ type StorageGetter interface {
|
|||
GetCertificateByShortSerial(string) (Certificate, error)
|
||||
GetCertificateStatus(string) (CertificateStatus, error)
|
||||
AlreadyDeniedCSR([]string) (bool, error)
|
||||
GetSCTReceipt(string, string) (SignedCertificateTimestamp, error)
|
||||
}
|
||||
|
||||
// StorageAdder are the Boulder SA's write/update methods
|
||||
|
@ -125,6 +126,8 @@ type StorageAdder interface {
|
|||
UpdateOCSP(serial string, ocspResponse []byte) error
|
||||
|
||||
AddCertificate([]byte, int64) (string, error)
|
||||
|
||||
AddSCTReceipt(SignedCertificateTimestamp) error
|
||||
}
|
||||
|
||||
// StorageAuthority interface represents a simple key/value
|
||||
|
@ -151,3 +154,8 @@ type DNSResolver interface {
|
|||
LookupCAA(string) ([]*dns.CAA, time.Duration, error)
|
||||
LookupMX(string) ([]string, time.Duration, error)
|
||||
}
|
||||
|
||||
// Publisher defines the public interface for the Boulder Publisher
|
||||
type Publisher interface {
|
||||
SubmitToCT([]byte) error
|
||||
}
|
||||
|
|
|
@ -7,9 +7,12 @@ package core
|
|||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -553,6 +556,95 @@ type OCSPSigningRequest struct {
|
|||
RevokedAt time.Time
|
||||
}
|
||||
|
||||
type SignedCertificateTimestamp struct {
|
||||
ID int `db:"id"`
|
||||
// The version of the protocol to which the SCT conforms
|
||||
SCTVersion uint8 `db:"sctVersion"`
|
||||
// the SHA-256 hash of the log's public key, calculated over
|
||||
// the DER encoding of the key represented as SubjectPublicKeyInfo.
|
||||
LogID string `db:"logID"`
|
||||
// Timestamp (in ms since unix epoc) at which the SCT was issued
|
||||
Timestamp uint64 `db:"timestamp"`
|
||||
// For future extensions to the protocol
|
||||
Extensions []byte `db:"extensions"`
|
||||
// The Log's signature for this SCT
|
||||
Signature []byte `db:"signature"`
|
||||
|
||||
// The serial of the certificate this SCT is for
|
||||
CertificateSerial string `db:"certificateSerial"`
|
||||
|
||||
LockCol int64
|
||||
}
|
||||
|
||||
type RPCSignedCertificateTimestamp SignedCertificateTimestamp
|
||||
|
||||
type rawSignedCertificateTimestamp struct {
|
||||
Version uint8 `json:"sct_version"`
|
||||
LogID string `json:"id"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
Signature string `json:"signature"`
|
||||
Extensions string `json:"extensions"`
|
||||
}
|
||||
|
||||
func (sct *SignedCertificateTimestamp) UnmarshalJSON(data []byte) error {
|
||||
var err error
|
||||
var rawSCT rawSignedCertificateTimestamp
|
||||
if err = json.Unmarshal(data, &rawSCT); err != nil {
|
||||
return fmt.Errorf("Failed to unmarshal SCT receipt, %s", err)
|
||||
}
|
||||
sct.LogID = rawSCT.LogID
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to decode log ID, %s", err)
|
||||
}
|
||||
sct.Signature, err = base64.StdEncoding.DecodeString(rawSCT.Signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to decode SCT signature, %s", err)
|
||||
}
|
||||
sct.Extensions, err = base64.StdEncoding.DecodeString(rawSCT.Extensions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to decode SCT extensions, %s", err)
|
||||
}
|
||||
sct.SCTVersion = rawSCT.Version
|
||||
sct.Timestamp = rawSCT.Timestamp
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
sctHashSHA256 = 4
|
||||
sctSigECDSA = 3
|
||||
)
|
||||
|
||||
// CheckSignature validates that the returned SCT signature is a valid SHA256 +
|
||||
// ECDSA signature but does not verify that a specific public key signed it.
|
||||
func (sct *SignedCertificateTimestamp) CheckSignature() error {
|
||||
if len(sct.Signature) < 4 {
|
||||
return errors.New("SCT signature is truncated")
|
||||
}
|
||||
// Since all of the known logs currently only use SHA256 hashes and ECDSA
|
||||
// keys, only allow those
|
||||
if sct.Signature[0] != sctHashSHA256 {
|
||||
return fmt.Errorf("Unsupported SCT hash function [%d]", sct.Signature[0])
|
||||
}
|
||||
if sct.Signature[1] != sctSigECDSA {
|
||||
return fmt.Errorf("Unsupported SCT signature algorithm [%d]", sct.Signature[1])
|
||||
}
|
||||
|
||||
var ecdsaSig struct {
|
||||
R, S *big.Int
|
||||
}
|
||||
// Ignore the two length bytes and attempt to unmarshal the signature directly
|
||||
signatureBytes := sct.Signature[4:]
|
||||
signatureBytes, err := asn1.Unmarshal(signatureBytes, &ecdsaSig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse SCT signature, %s", err)
|
||||
}
|
||||
if len(signatureBytes) > 0 {
|
||||
return fmt.Errorf("Trailing garbage after signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevocationCode is used to specify a certificate revocation reason
|
||||
type RevocationCode int
|
||||
|
||||
|
|
31
core/util.go
31
core/util.go
|
@ -350,6 +350,37 @@ func UniqueNames(names []string) (unique []string) {
|
|||
return
|
||||
}
|
||||
|
||||
// LoadCertBundle loads a PEM bundle of certificates from disk
|
||||
func LoadCertBundle(filename string) ([]*x509.Certificate, error) {
|
||||
bundleBytes, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var bundle []*x509.Certificate
|
||||
var block *pem.Block
|
||||
rest := bundleBytes
|
||||
for {
|
||||
block, rest = pem.Decode(rest)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if block.Type != "CERTIFICATE" {
|
||||
return nil, fmt.Errorf("Block has invalid type: %s", block.Type)
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bundle = append(bundle, cert)
|
||||
}
|
||||
|
||||
if len(bundle) == 0 {
|
||||
return nil, fmt.Errorf("Bundle doesn't contain any certificates")
|
||||
}
|
||||
|
||||
return bundle, nil
|
||||
}
|
||||
|
||||
// LoadCert loads a PEM certificate specified by filename or returns a error
|
||||
func LoadCert(filename string) (cert *x509.Certificate, err error) {
|
||||
certPEM, err := ioutil.ReadFile(filename)
|
||||
|
|
|
@ -31,6 +31,7 @@ USER_BOULDER_RA="ra"
|
|||
USER_BOULDER_VA="va"
|
||||
USER_BOULDER_WFE="wfe"
|
||||
USER_BOULDER_OCSP="ocsp-updater"
|
||||
USER_BOULDER_PUBLISHER="publisher"
|
||||
|
||||
# PASSWORDS
|
||||
PASS_BOULDER_AM="guest"
|
||||
|
@ -40,6 +41,7 @@ PASS_BOULDER_RA="guest"
|
|||
PASS_BOULDER_VA="guest"
|
||||
PASS_BOULDER_WFE="guest"
|
||||
PASS_BOULDER_OCSP="guest"
|
||||
PASS_BOULDER_PUBLISHER="guest"
|
||||
|
||||
# To use different options, you should create an override
|
||||
# file with whatever changes you want for the above variables
|
||||
|
@ -69,6 +71,7 @@ admin declare queue name="CA.server" durable=false
|
|||
admin declare queue name="SA.server" durable=false
|
||||
admin declare queue name="RA.server" durable=false
|
||||
admin declare queue name="VA.server" durable=false
|
||||
admin declare queue name="Publisher.server" durable=false
|
||||
|
||||
admin declare exchange name="boulder" type=topic durable=false
|
||||
|
||||
|
@ -83,6 +86,7 @@ admin declare user name=${USER_BOULDER_RA} password=${PASS_BOULDER_RA} tags=""
|
|||
admin declare user name=${USER_BOULDER_VA} password=${PASS_BOULDER_VA} tags=""
|
||||
admin declare user name=${USER_BOULDER_WFE} password=${PASS_BOULDER_WFE} tags=""
|
||||
admin declare user name=${USER_BOULDER_OCSP} password=${PASS_BOULDER_OCSP} tags=""
|
||||
admin declare user name=${USER_BOULDER_PUBLISHER} password=${PASS_BOULDER_PUBLISHER} tags=""
|
||||
|
||||
##################################################
|
||||
## Permissions RegExes ##
|
||||
|
@ -115,7 +119,7 @@ admin declare permission vhost=${VHOST} user=${USER_BOULDER_RA} \
|
|||
admin declare permission vhost=${VHOST} user=${USER_BOULDER_CA} \
|
||||
configure="^(CA\.server|CA->SA.*)$" \
|
||||
write="^(boulder|CA\.server|CA->SA.*)$" \
|
||||
read="^(boulder|CA\.server|CA->SA.*)$"
|
||||
read="^(boulder|CA\.server|CA->(SA|Publisher).*)$"
|
||||
|
||||
# SA uses only SA.server
|
||||
admin declare permission vhost=${VHOST} user=${USER_BOULDER_SA} \
|
||||
|
@ -134,3 +138,9 @@ admin declare permission vhost=${VHOST} user=${USER_BOULDER_OCSP} \
|
|||
configure="^(OCSP->CA.*)$" \
|
||||
write="^(boulder|OCSP->CA.*)$" \
|
||||
read="^(boulder|OCSP->CA.*)$"
|
||||
|
||||
# Publisher uses Publisher.server and Publisher->SA
|
||||
admin declare permission vhost=${VHOST} user=${USER_BOULDER_PUBLISHER} \
|
||||
configure="^Publisher\.server$" \
|
||||
write="^(boulder|Publisher\.server)$" \
|
||||
read="^(boulder|Publisher\.server|Publisher->SA.*)$"
|
||||
|
|
|
@ -297,6 +297,19 @@ func (sa *MockSA) UpdateRegistration(reg core.Registration) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// GetSCTReceipt is a mock
|
||||
func (sa *MockSA) GetSCTReceipt(serial string, logID string) (sct core.SignedCertificateTimestamp, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// AddSCTReceipt is a mock
|
||||
func (sa *MockSA) AddSCTReceipt(sct core.SignedCertificateTimestamp) (err error) {
|
||||
if sct.Signature == nil {
|
||||
err = fmt.Errorf("Bad times")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetLatestValidAuthorization is a mock
|
||||
func (sa *MockSA) GetLatestValidAuthorization(registrationId int64, identifier core.AcmeIdentifier) (authz core.Authorization, err error) {
|
||||
if registrationId == 1 && identifier.Type == "dns" {
|
||||
|
@ -307,3 +320,13 @@ func (sa *MockSA) GetLatestValidAuthorization(registrationId int64, identifier c
|
|||
}
|
||||
return core.Authorization{}, errors.New("no authz")
|
||||
}
|
||||
|
||||
// MockPublisher is a mock
|
||||
type MockPublisher struct {
|
||||
// empty
|
||||
}
|
||||
|
||||
// SubmitToCT is a mock
|
||||
func (*MockPublisher) SubmitToCT([]byte) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
// Copyright 2015 ISRG. All rights reserved
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package publisher
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
"github.com/letsencrypt/boulder/sa"
|
||||
)
|
||||
|
||||
type LogDescription struct {
|
||||
ID string
|
||||
URI string
|
||||
PublicKey *ecdsa.PublicKey
|
||||
}
|
||||
|
||||
type rawLogDescription struct {
|
||||
URI string `json:"uri"`
|
||||
PublicKey string `json:"key"`
|
||||
}
|
||||
|
||||
func (logDesc *LogDescription) UnmarshalJSON(data []byte) error {
|
||||
var rawLogDesc rawLogDescription
|
||||
if err := json.Unmarshal(data, &rawLogDesc); err != nil {
|
||||
return fmt.Errorf("Failed to unmarshal log description, %s", err)
|
||||
}
|
||||
logDesc.URI = rawLogDesc.URI
|
||||
// Load Key
|
||||
pkBytes, err := base64.StdEncoding.DecodeString(rawLogDesc.PublicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("")
|
||||
}
|
||||
pk, err := x509.ParsePKIXPublicKey(pkBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("")
|
||||
}
|
||||
ecdsaKey, ok := pk.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("Failed to unmarshal log description for %s, unsupported public key type", logDesc.URI)
|
||||
}
|
||||
logDesc.PublicKey = ecdsaKey
|
||||
|
||||
// Generate key hash for log ID
|
||||
pkHash := sha256.Sum256(pkBytes)
|
||||
logDesc.ID = base64.StdEncoding.EncodeToString(pkHash[:])
|
||||
if len(logDesc.ID) != 44 {
|
||||
return fmt.Errorf("Invalid log ID length [%d]", len(logDesc.ID))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CTConfig defines the JSON configuration file schema
|
||||
type CTConfig struct {
|
||||
Logs []LogDescription `json:"logs"`
|
||||
SubmissionRetries int `json:"submissionRetries"`
|
||||
SubmissionBackoffString string `json:"submissionBackoff"`
|
||||
IntermediateBundleFilename string `json:"intermediateBundleFilename"`
|
||||
}
|
||||
|
||||
type ctSubmissionRequest struct {
|
||||
Chain []string `json:"chain"`
|
||||
}
|
||||
|
||||
const (
|
||||
sctVersion = 0
|
||||
sctSigType = 0
|
||||
sctX509EntryType = 0
|
||||
)
|
||||
|
||||
// PublisherImpl defines a Publisher
|
||||
type PublisherImpl struct {
|
||||
log *blog.AuditLogger
|
||||
client *http.Client
|
||||
submissionBackoff time.Duration
|
||||
submissionRetries int
|
||||
issuerBundle []string
|
||||
ctLogs []LogDescription
|
||||
|
||||
SA core.StorageAuthority
|
||||
}
|
||||
|
||||
// NewPublisherImpl creates a Publisher that will submit certificates
|
||||
// to any CT logs configured in CTConfig
|
||||
func NewPublisherImpl(ctConfig CTConfig) (pub PublisherImpl, err error) {
|
||||
logger := blog.GetAuditLogger()
|
||||
logger.Notice("Publisher Authority Starting")
|
||||
|
||||
if ctConfig.IntermediateBundleFilename == "" {
|
||||
err = fmt.Errorf("No CT submission bundle provided")
|
||||
return
|
||||
}
|
||||
bundle, err := core.LoadCertBundle(ctConfig.IntermediateBundleFilename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, cert := range bundle {
|
||||
pub.issuerBundle = append(pub.issuerBundle, base64.StdEncoding.EncodeToString(cert.Raw))
|
||||
}
|
||||
ctBackoff, err := time.ParseDuration(ctConfig.SubmissionBackoffString)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, log := range ctConfig.Logs {
|
||||
if !strings.HasPrefix(log.URI, "https://") && !strings.HasPrefix(log.URI, "http://") {
|
||||
err = fmt.Errorf("Log URI [%s] is not absolute", log.URI)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pub.log = logger
|
||||
pub.client = &http.Client{}
|
||||
pub.submissionBackoff = ctBackoff
|
||||
pub.submissionRetries = ctConfig.SubmissionRetries
|
||||
pub.ctLogs = ctConfig.Logs
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (pub *PublisherImpl) submitToCTLog(serial string, jsonSubmission []byte, log LogDescription) error {
|
||||
done := false
|
||||
var sct core.SignedCertificateTimestamp
|
||||
backoff := pub.submissionBackoff
|
||||
var retries int
|
||||
for retries = 0; retries <= pub.submissionRetries; retries++ {
|
||||
if retries > 0 {
|
||||
time.Sleep(backoff)
|
||||
}
|
||||
resp, err := postJSON(pub.client, fmt.Sprintf("%s%s", log.URI, "/ct/v1/add-chain"), jsonSubmission, &sct)
|
||||
if err != nil {
|
||||
// Retry the request, log the error
|
||||
pub.log.Warning(fmt.Sprintf("Error POSTing JSON to CT log submission endpoint [%s]: %s", log.URI, err))
|
||||
backoff = pub.submissionBackoff
|
||||
continue
|
||||
} else if resp.StatusCode == http.StatusRequestTimeout || resp.StatusCode == http.StatusServiceUnavailable {
|
||||
// Retry the request after either the configured backoff period or the period
|
||||
// specified by the Retry-After header
|
||||
backoff = pub.submissionBackoff
|
||||
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
|
||||
backoff = time.Second * time.Duration(seconds)
|
||||
}
|
||||
continue
|
||||
} else if resp.StatusCode != http.StatusOK {
|
||||
// If we hit an otherwise unexpected status code break the loop and return
|
||||
// an error immediately
|
||||
return fmt.Errorf("Unexpected status code returned from CT log: %d", resp.StatusCode)
|
||||
}
|
||||
done = true
|
||||
break
|
||||
}
|
||||
|
||||
if !done {
|
||||
err := fmt.Errorf(
|
||||
"Unable to submit certificate to CT log [Serial: %s, Log URI: %s, Retries: %d]",
|
||||
serial,
|
||||
log.URI,
|
||||
retries,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sct.CheckSignature(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pub.log.Debug(fmt.Sprintf(
|
||||
"Submitted certificate to CT log [Serial: %s, Log URI: %s, Retries: %d, Signature: %x]",
|
||||
serial,
|
||||
log.URI,
|
||||
retries, sct.Signature,
|
||||
))
|
||||
|
||||
// Set certificate serial and add SCT to DB
|
||||
sct.CertificateSerial = serial
|
||||
err := pub.SA.AddSCTReceipt(sct)
|
||||
if err != nil {
|
||||
if _, ok := err.(sa.ErrDuplicateReceipt); ok {
|
||||
pub.log.Warning(fmt.Sprintf(
|
||||
"SCT receipt for [Serial: %s, Log URI: %s] already exists in database",
|
||||
serial,
|
||||
log.URI,
|
||||
))
|
||||
return nil
|
||||
} else if err != nil {
|
||||
err = fmt.Errorf(
|
||||
"Error adding SCT receipt for [%s to %s]: %s",
|
||||
sct.CertificateSerial,
|
||||
log.URI,
|
||||
err,
|
||||
)
|
||||
return err
|
||||
}
|
||||
}
|
||||
pub.log.Info(fmt.Sprintf(
|
||||
"Stored SCT receipt from CT log submission [Serial: %s, Log URI: %s]",
|
||||
serial,
|
||||
log.URI,
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubmitToCT will submit the certificate represented by certDER to any CT
|
||||
// logs configured in pub.CT.Logs
|
||||
func (pub *PublisherImpl) SubmitToCT(der []byte) error {
|
||||
cert, err := x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
pub.log.Err(fmt.Sprintf("Unable to parse certificate, %s", err))
|
||||
return err
|
||||
}
|
||||
|
||||
submission := ctSubmissionRequest{Chain: []string{base64.StdEncoding.EncodeToString(cert.Raw)}}
|
||||
// Add all intermediate certificates needed for submission
|
||||
submission.Chain = append(submission.Chain, pub.issuerBundle...)
|
||||
jsonSubmission, err := json.Marshal(submission)
|
||||
if err != nil {
|
||||
pub.log.Err(fmt.Sprintf("Unable to marshal CT submission, %s", err))
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ctLog := range pub.ctLogs {
|
||||
err = pub.submitToCTLog(core.SerialToString(cert.SerialNumber), jsonSubmission, ctLog)
|
||||
if err != nil {
|
||||
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
|
||||
pub.log.AuditErr(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func postJSON(client *http.Client, uri string, data []byte, respObj interface{}) (*http.Response, error) {
|
||||
req, err := http.NewRequest("POST", uri, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Creating request failed, %s", err)
|
||||
}
|
||||
req.Header.Set("Keep-Alive", "timeout=15, max=100")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Request failed, %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to read response body, %s", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, respObj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to unmarshal SCT receipt, %s", err)
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
|
@ -0,0 +1,339 @@
|
|||
// Copyright 2015 ISRG. All rights reserved
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package publisher
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/letsencrypt/boulder/mocks"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
)
|
||||
|
||||
var testLeaf = `-----BEGIN CERTIFICATE-----
|
||||
MIIHAjCCBeqgAwIBAgIQfwAAAQAAAUtRVNy9a8fMcDANBgkqhkiG9w0BAQsFADBa
|
||||
MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MRcwFQYDVQQLEw5UcnVz
|
||||
dElEIFNlcnZlcjEeMBwGA1UEAxMVVHJ1c3RJRCBTZXJ2ZXIgQ0EgQTUyMB4XDTE1
|
||||
MDIwMzIxMjQ1MVoXDTE4MDIwMjIxMjQ1MVowfzEYMBYGA1UEAxMPbGV0c2VuY3J5
|
||||
cHQub3JnMSkwJwYDVQQKEyBJTlRFUk5FVCBTRUNVUklUWSBSRVNFQVJDSCBHUk9V
|
||||
UDEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzETMBEGA1UECBMKQ2FsaWZvcm5pYTEL
|
||||
MAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGE6T8
|
||||
LcmS6g8lH/1Y5orXeZOva4gthrS+VmJUWlz3K4Er5q8CmVFTmD/rYL6tA31JYCAi
|
||||
p2bVQ8z/PgWYGosuMzox2OO9MqnLwTTG074sCHTZi4foFb6KacS8xVu25u8RRBd8
|
||||
1WJNlw736FO0pJUkkE3gDSPz1QTpw3gc6n7SyppaFr40D5PpK3PPoNCPfoz2bFtH
|
||||
m2KRsUH924LRfitUZdI68kxJP7QG1SAbdZxA/qDcfvDSgCYW5WNmMKS4v+GHuMkJ
|
||||
gBe20tML+hItmF5S9mYm/GbkFLG8YwWZrytUZrSjxmuL9nj3MaBrAPQw3/T582ry
|
||||
KM8+z188kbnA7A+BAgMBAAGjggOdMIIDmTAOBgNVHQ8BAf8EBAMCBaAwggInBgNV
|
||||
HSAEggIeMIICGjCCAQsGCmCGSAGG+S8ABgMwgfwwQAYIKwYBBQUHAgEWNGh0dHBz
|
||||
Oi8vc2VjdXJlLmlkZW50cnVzdC5jb20vY2VydGlmaWNhdGVzL3BvbGljeS90cy8w
|
||||
gbcGCCsGAQUFBwICMIGqGoGnVGhpcyBUcnVzdElEIFNlcnZlciBDZXJ0aWZpY2F0
|
||||
ZSBoYXMgYmVlbiBpc3N1ZWQgaW4gYWNjb3JkYW5jZSB3aXRoIElkZW5UcnVzdCdz
|
||||
IFRydXN0SUQgQ2VydGlmaWNhdGUgUG9saWN5IGZvdW5kIGF0IGh0dHBzOi8vc2Vj
|
||||
dXJlLmlkZW50cnVzdC5jb20vY2VydGlmaWNhdGVzL3BvbGljeS90cy8wggEHBgZn
|
||||
gQwBAgIwgfwwQAYIKwYBBQUHAgEWNGh0dHBzOi8vc2VjdXJlLmlkZW50cnVzdC5j
|
||||
b20vY2VydGlmaWNhdGVzL3BvbGljeS90cy8wgbcGCCsGAQUFBwICMIGqGoGnVGhp
|
||||
cyBUcnVzdElEIFNlcnZlciBDZXJ0aWZpY2F0ZSBoYXMgYmVlbiBpc3N1ZWQgaW4g
|
||||
YWNjb3JkYW5jZSB3aXRoIElkZW5UcnVzdCdzIFRydXN0SUQgQ2VydGlmaWNhdGUg
|
||||
UG9saWN5IGZvdW5kIGF0IGh0dHBzOi8vc2VjdXJlLmlkZW50cnVzdC5jb20vY2Vy
|
||||
dGlmaWNhdGVzL3BvbGljeS90cy8wHQYDVR0OBBYEFNLAuFI2ugD0U24OgEPtX6+p
|
||||
/xJQMEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly92YWxpZGF0aW9uLmlkZW50cnVz
|
||||
dC5jb20vY3JsL3RydXN0aWRjYWE1Mi5jcmwwgYQGCCsGAQUFBwEBBHgwdjAwBggr
|
||||
BgEFBQcwAYYkaHR0cDovL2NvbW1lcmNpYWwub2NzcC5pZGVudHJ1c3QuY29tMEIG
|
||||
CCsGAQUFBzAChjZodHRwOi8vdmFsaWRhdGlvbi5pZGVudHJ1c3QuY29tL2NlcnRz
|
||||
L3RydXN0aWRjYWE1Mi5wN2MwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
|
||||
MB8GA1UdIwQYMBaAFKJWJDzQ1BW56L94oxMQWEguFlThMC8GA1UdEQQoMCaCD2xl
|
||||
dHNlbmNyeXB0Lm9yZ4ITd3d3LmxldHNlbmNyeXB0Lm9yZzANBgkqhkiG9w0BAQsF
|
||||
AAOCAQEAgEmnzpYncB/E5SCHa5cnGorvNNE6Xsp3YXK9fJBT2++chQTkyFYpE12T
|
||||
TR+cb7CTdRiYErNHXV8Hl/XTK8mxGxK8KXM9zUDlfrl7yBnyGTl2Sk8qJwA2kGuu
|
||||
X9KA1o3MFkKMD809ITAlvPoQpml1Ke0aFo4NLO/LJKnJpkyF8L+JQrkfLNHpKYn3
|
||||
PvnyJnurVTXDOIwQw8HVXbw6UKAad87e1hKGLYOpsaaKCLaNw1vg8uI+O9mv1MC6
|
||||
FTfP1pSlr11s+Ih4YancuJud41rT8lXCUbDs1Uws9pPdVzLt8zk5M0vbHmTCljbg
|
||||
UC5XkUmEvadMfgWslIQD0r6+BRRS+A==
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
var testIntermediate = `-----BEGIN CERTIFICATE-----
|
||||
MIIG3zCCBMegAwIBAgIQAJv84kD9Vb7ZJp4MASwbdzANBgkqhkiG9w0BAQsFADBK
|
||||
MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu
|
||||
VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMzIwMTgwNTM4WhcNMjIw
|
||||
MzIwMTgwNTM4WjBaMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MRcw
|
||||
FQYDVQQLEw5UcnVzdElEIFNlcnZlcjEeMBwGA1UEAxMVVHJ1c3RJRCBTZXJ2ZXIg
|
||||
Q0EgQTUyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl2nXmZiFAj/p
|
||||
JkJ26PRzP6kyRCaQeC54V5EZoF12K0n5k1pdWs6C88LY5Uw2eisdDdump/6REnzt
|
||||
cgG3jKHF2syd/gn7V+IURw/onpGPlC2AMpOTA/UoeGi6fg9CtDF6BRQiUzPko61s
|
||||
j6++Y2uyMp/ZF7nJ4GB8mdYx4eSgtz+vsjKsfoyc3ALr4bwfFJy8kfey+0Lz4SAr
|
||||
y7+P87NwY/r3dSgCq8XUsO3qJX+HzTcUloM8QAIboJ4ZR3/zsMzFJWC4NRLxUesX
|
||||
3Pxbpdmb70BM13dx6ftFi37y42mwQmYXRpA6zUY98bAJb9z/7jNhyvzHLjztXgrR
|
||||
vyISaYBLIwIDAQABo4ICrzCCAqswgYkGCCsGAQUFBwEBBH0wezAwBggrBgEFBQcw
|
||||
AYYkaHR0cDovL2NvbW1lcmNpYWwub2NzcC5pZGVudHJ1c3QuY29tMEcGCCsGAQUF
|
||||
BzAChjtodHRwOi8vdmFsaWRhdGlvbi5pZGVudHJ1c3QuY29tL3Jvb3RzL2NvbW1l
|
||||
cmNpYWxyb290Y2ExLnA3YzAfBgNVHSMEGDAWgBTtRBnA0/AGi+6ke75C5yZUyI42
|
||||
djAPBgNVHRMBAf8EBTADAQH/MIIBMQYDVR0gBIIBKDCCASQwggEgBgRVHSAAMIIB
|
||||
FjBQBggrBgEFBQcCAjBEMEIWPmh0dHBzOi8vc2VjdXJlLmlkZW50cnVzdC5jb20v
|
||||
Y2VydGlmaWNhdGVzL3BvbGljeS90cy9pbmRleC5odG1sMAAwgcEGCCsGAQUFBwIC
|
||||
MIG0GoGxVGhpcyBUcnVzdElEIFNlcnZlciBDZXJ0aWZpY2F0ZSBoYXMgYmVlbiBp
|
||||
c3N1ZWQgaW4gYWNjb3JkYW5jZSB3aXRoIElkZW5UcnVzdCdzIFRydXN0SUQgQ2Vy
|
||||
dGlmaWNhdGUgUG9saWN5IGZvdW5kIGF0IGh0dHBzOi8vc2VjdXJlLmlkZW50cnVz
|
||||
dC5jb20vY2VydGlmaWNhdGVzL3BvbGljeS90cy9pbmRleC5odG1sMEoGA1UdHwRD
|
||||
MEEwP6A9oDuGOWh0dHA6Ly92YWxpZGF0aW9uLmlkZW50cnVzdC5jb20vY3JsL2Nv
|
||||
bW1lcmNpYWxyb290Y2ExLmNybDA7BgNVHSUENDAyBggrBgEFBQcDAQYIKwYBBQUH
|
||||
AwIGCCsGAQUFBwMFBggrBgEFBQcDBgYIKwYBBQUHAwcwDgYDVR0PAQH/BAQDAgGG
|
||||
MB0GA1UdDgQWBBSiViQ80NQVuei/eKMTEFhILhZU4TANBgkqhkiG9w0BAQsFAAOC
|
||||
AgEAm4oWcizMGDsjzYFKfWUKferHD1Vusclu4/dra0PCx3HctXJMnuXc4Ngvn6Ab
|
||||
BcanG0Uht+bkuC4TaaS3QMCl0LwcsIzlfRzDJdxIpREWHH8yoNoPafVN3u2iGiyT
|
||||
5qda4Ej4WQgOmmNiluZPk8a4d4MkAxyQdVF/AVVx6Or+9d+bkQenjPSxWVmi/bfW
|
||||
RBXq2AcD8Ej7AIU15dRnLEkESmJm4xtV2aqmCd0SSBGhJHYLcInUPzWVg1zcB5EQ
|
||||
78GOTue8UrZvbcYhOufHG0k5JX5HVoVZ6GSXKqn5kqbcHXT6adVoWT/BxZruZiKQ
|
||||
qkryoZoSywt7dDdDhpC2+oAOC+XwX2HJp2mrPaAea1+E4LM9C9iEDtjsn5FfsBz0
|
||||
VRbMRdaoayXzOlTRhF3pGU2LLCmrXy/pqpqAGYPxyHr3auRn9fjv77UMEqVFdfOc
|
||||
CspkK71IGqM9UwwMtCZBp0fK/Xv9o1d85paXcJ/aH8zg6EK4UkuXDFnLsg1LrIru
|
||||
+YHeHOeSaXJlcjzwWVY/Exe5HymtqGH8klMhy65bjtapNt76+j2CJgxOdPEiTy/l
|
||||
9LH5ujlo5qgemXE3ePwYZ9D3iiJThTf3tWkvdbz2wCPJAy2EHS0FxHMfx5sXsFsa
|
||||
OY8B7wwvZTLzU6WWs781TJXx2CE04PneeeArLpVLkiGIWjk=
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
const issuerPath = "../test/test-ca.pem"
|
||||
|
||||
var log = mocks.UseMockLog()
|
||||
|
||||
func getPort(hs *httptest.Server) (int, error) {
|
||||
url, err := url.Parse(hs.URL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_, portString, err := net.SplitHostPort(url.Host)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
port, err := strconv.ParseInt(portString, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(port), nil
|
||||
}
|
||||
|
||||
func logSrv() *httptest.Server {
|
||||
m := http.NewServeMux()
|
||||
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var jsonReq ctSubmissionRequest
|
||||
err := decoder.Decode(&jsonReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Submissions should always contain at least one cert
|
||||
if len(jsonReq.Chain) >= 1 {
|
||||
fmt.Fprint(w, `{"signature":"BAMASDBGAiEAknaySJVdB3FqG9bUKHgyu7V9AdEabpTc71BELUp6/iECIQDObrkwlQq6Azfj5XOA5E12G/qy/WuRn97z7qMSXXc82Q=="}`)
|
||||
}
|
||||
})
|
||||
|
||||
server := httptest.NewUnstartedServer(m)
|
||||
server.Start()
|
||||
return server
|
||||
}
|
||||
|
||||
func retryableLogSrv(retries int, after *int) *httptest.Server {
|
||||
hits := 0
|
||||
m := http.NewServeMux()
|
||||
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if hits >= retries {
|
||||
fmt.Fprint(w, `{"signature":"BAMASDBGAiEAknaySJVdB3FqG9bUKHgyu7V9AdEabpTc71BELUp6/iECIQDObrkwlQq6Azfj5XOA5E12G/qy/WuRn97z7qMSXXc82Q=="}`)
|
||||
} else {
|
||||
hits++
|
||||
if after != nil {
|
||||
w.Header().Set("Retry-After", fmt.Sprintf("%d", *after))
|
||||
}
|
||||
w.WriteHeader(http.StatusRequestTimeout)
|
||||
}
|
||||
})
|
||||
|
||||
server := httptest.NewUnstartedServer(m)
|
||||
server.Start()
|
||||
return server
|
||||
}
|
||||
|
||||
func emptyLogSrv() *httptest.Server {
|
||||
m := http.NewServeMux()
|
||||
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var jsonReq ctSubmissionRequest
|
||||
err := decoder.Decode(&jsonReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Submissions should always contain at least one cert
|
||||
if len(jsonReq.Chain) >= 1 {
|
||||
fmt.Fprint(w, `{"signature":""}`)
|
||||
}
|
||||
})
|
||||
|
||||
server := httptest.NewUnstartedServer(m)
|
||||
server.Start()
|
||||
return server
|
||||
}
|
||||
|
||||
func setup(t *testing.T, port, retries int) (PublisherImpl, *x509.Certificate) {
|
||||
intermediatePEM, _ := pem.Decode([]byte(testIntermediate))
|
||||
|
||||
pub, err := NewPublisherImpl(CTConfig{
|
||||
Logs: []LogDescription{LogDescription{URI: fmt.Sprintf("http://localhost:%d", port)}},
|
||||
SubmissionBackoffString: "0s",
|
||||
IntermediateBundleFilename: issuerPath,
|
||||
SubmissionRetries: retries,
|
||||
})
|
||||
test.AssertNotError(t, err, "Couldn't create new Publisher")
|
||||
pub.issuerBundle = append(pub.issuerBundle, base64.StdEncoding.EncodeToString(intermediatePEM.Bytes))
|
||||
pub.SA = &mocks.MockSA{}
|
||||
|
||||
leafPEM, _ := pem.Decode([]byte(testLeaf))
|
||||
leaf, err := x509.ParseCertificate(leafPEM.Bytes)
|
||||
test.AssertNotError(t, err, "Couldn't parse leafPEM.Bytes")
|
||||
|
||||
return pub, leaf
|
||||
}
|
||||
|
||||
func TestNewPublisherImpl(t *testing.T) {
|
||||
// Allowed
|
||||
ctConf := CTConfig{SubmissionBackoffString: "0s", IntermediateBundleFilename: issuerPath}
|
||||
_, err := NewPublisherImpl(ctConf)
|
||||
test.AssertNotError(t, err, "Couldn't create new Publisher")
|
||||
|
||||
ctConf = CTConfig{Logs: []LogDescription{LogDescription{URI: "http://localhost"}}, SubmissionBackoffString: "0s", IntermediateBundleFilename: issuerPath}
|
||||
_, err = NewPublisherImpl(ctConf)
|
||||
test.AssertNotError(t, err, "Couldn't create new Publisher")
|
||||
}
|
||||
|
||||
func TestCheckSignature(t *testing.T) {
|
||||
// Based on a submission to the aviator log
|
||||
goodSigBytes, err := base64.StdEncoding.DecodeString("BAMASDBGAiEAknaySJVdB3FqG9bUKHgyu7V9AdEabpTc71BELUp6/iECIQDObrkwlQq6Azfj5XOA5E12G/qy/WuRn97z7qMSXXc82Q==")
|
||||
test.AssertNotError(t, err, "Couldn't decode signature")
|
||||
|
||||
testReceipt := core.SignedCertificateTimestamp{
|
||||
Signature: goodSigBytes,
|
||||
}
|
||||
|
||||
// Good signature
|
||||
err = testReceipt.CheckSignature()
|
||||
test.AssertNotError(t, err, "Valid signature check failed")
|
||||
|
||||
// Invalid signature (too short, trailing garbage)
|
||||
testReceipt.Signature = goodSigBytes[1:]
|
||||
err = testReceipt.CheckSignature()
|
||||
test.AssertError(t, err, "Invalid signature check failed")
|
||||
testReceipt.Signature = append(goodSigBytes, []byte{0, 0, 1}...)
|
||||
err = testReceipt.CheckSignature()
|
||||
test.AssertError(t, err, "Invalid signature check failed")
|
||||
}
|
||||
|
||||
func TestSubmitToCT(t *testing.T) {
|
||||
server := logSrv()
|
||||
defer server.Close()
|
||||
port, err := getPort(server)
|
||||
test.AssertNotError(t, err, "Failed to get test server port")
|
||||
|
||||
pub, leaf := setup(t, port, 0)
|
||||
|
||||
log.Clear()
|
||||
err = pub.SubmitToCT(leaf.Raw)
|
||||
test.AssertNotError(t, err, "Certificate submission failed")
|
||||
|
||||
// No Intermediate
|
||||
pub.issuerBundle = []string{}
|
||||
log.Clear()
|
||||
err = pub.SubmitToCT(leaf.Raw)
|
||||
test.AssertNotError(t, err, "Certificate submission failed")
|
||||
}
|
||||
|
||||
func TestGoodRetry(t *testing.T) {
|
||||
server := retryableLogSrv(1, nil)
|
||||
defer server.Close()
|
||||
port, err := getPort(server)
|
||||
test.AssertNotError(t, err, "Failed to get test server port")
|
||||
|
||||
pub, leaf := setup(t, port, 1)
|
||||
|
||||
log.Clear()
|
||||
err = pub.SubmitToCT(leaf.Raw)
|
||||
test.AssertNotError(t, err, "Certificate submission failed")
|
||||
}
|
||||
|
||||
func TestFatalRetry(t *testing.T) {
|
||||
server := retryableLogSrv(1, nil)
|
||||
defer server.Close()
|
||||
port, err := getPort(server)
|
||||
test.AssertNotError(t, err, "Failed to get test server port")
|
||||
|
||||
pub, leaf := setup(t, port, 0)
|
||||
|
||||
log.Clear()
|
||||
err = pub.SubmitToCT(leaf.Raw)
|
||||
test.AssertEquals(t, len(log.GetAllMatching("Unable to submit certificate to CT log.*")), 1)
|
||||
}
|
||||
|
||||
func TestUnexpectedError(t *testing.T) {
|
||||
pub, leaf := setup(t, 0, 0)
|
||||
|
||||
log.Clear()
|
||||
_ = pub.SubmitToCT(leaf.Raw)
|
||||
test.AssertEquals(t, len(log.GetAllMatching("Unable to submit certificate to CT log.*")), 1)
|
||||
}
|
||||
|
||||
func TestRetryAfter(t *testing.T) {
|
||||
retryAfter := 2
|
||||
server := retryableLogSrv(2, &retryAfter)
|
||||
defer server.Close()
|
||||
port, err := getPort(server)
|
||||
test.AssertNotError(t, err, "Failed to get test server port")
|
||||
|
||||
pub, leaf := setup(t, port, 2)
|
||||
|
||||
log.Clear()
|
||||
startedWaiting := time.Now()
|
||||
err = pub.SubmitToCT(leaf.Raw)
|
||||
test.AssertNotError(t, err, "Certificate submission failed")
|
||||
test.Assert(t, time.Since(startedWaiting) >= time.Duration(retryAfter*2)*time.Second, fmt.Sprintf("Submitter retried submission too fast: %s", time.Since(startedWaiting)))
|
||||
}
|
||||
|
||||
func TestMultiLog(t *testing.T) {
|
||||
srvA := logSrv()
|
||||
defer srvA.Close()
|
||||
srvB := logSrv()
|
||||
defer srvB.Close()
|
||||
portA, err := getPort(srvA)
|
||||
test.AssertNotError(t, err, "Failed to get test server port")
|
||||
portB, err := getPort(srvB)
|
||||
test.AssertNotError(t, err, "Failed to get test server port")
|
||||
|
||||
pub, leaf := setup(t, portA, 0)
|
||||
pub.ctLogs = append(pub.ctLogs, LogDescription{URI: fmt.Sprintf("http://localhost:%d", portB)})
|
||||
|
||||
log.Clear()
|
||||
err = pub.SubmitToCT(leaf.Raw)
|
||||
test.AssertNotError(t, err, "Certificate submission failed")
|
||||
}
|
||||
|
||||
func TestBadServer(t *testing.T) {
|
||||
srv := emptyLogSrv()
|
||||
defer srv.Close()
|
||||
port, err := getPort(srv)
|
||||
test.AssertNotError(t, err, "Failed to get test server port")
|
||||
|
||||
pub, leaf := setup(t, port, 0)
|
||||
|
||||
log.Clear()
|
||||
err = pub.SubmitToCT(leaf.Raw)
|
||||
test.AssertEquals(t, len(log.GetAllMatching("SCT signature is truncated")), 1)
|
||||
}
|
|
@ -193,6 +193,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut
|
|||
SA: ssa,
|
||||
PA: pa,
|
||||
DB: cadb,
|
||||
Publisher: &mocks.MockPublisher{},
|
||||
ValidityPeriod: time.Hour * 2190,
|
||||
NotAfter: time.Now().Add(time.Hour * 8761),
|
||||
Clk: fc,
|
||||
|
|
|
@ -276,12 +276,12 @@ func AmqpChannel(conf cmd.Config) (*amqp.Channel, error) {
|
|||
log.Info("AMQPS: Loading TLS Options.")
|
||||
|
||||
if strings.HasPrefix(conf.AMQP.Server, "amqps") == false {
|
||||
err = fmt.Errorf("AMQPS: Not using an AMQPS URL. To use AMQP instead of AMQPS, set insecure=true.")
|
||||
err = fmt.Errorf("AMQPS: Not using an AMQPS URL. To use AMQP instead of AMQPS, set insecure=true")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if conf.AMQP.TLS == nil {
|
||||
err = fmt.Errorf("AMQPS: No TLS configuration provided. To use AMQP instead of AMQPS, set insecure=true.")
|
||||
err = fmt.Errorf("AMQPS: No TLS configuration provided. To use AMQP instead of AMQPS, set insecure=true")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -60,6 +60,9 @@ const (
|
|||
MethodFinalizeAuthorization = "FinalizeAuthorization" // SA
|
||||
MethodAddCertificate = "AddCertificate" // SA
|
||||
MethodAlreadyDeniedCSR = "AlreadyDeniedCSR" // SA
|
||||
MethodGetSCTReceipt = "GetSCTReceipt" // SA
|
||||
MethodAddSCTReceipt = "AddSCTReceipt" // SA
|
||||
MethodSubmitToCT = "SubmitToCT" // Pub
|
||||
)
|
||||
|
||||
// Request structs
|
||||
|
@ -573,6 +576,32 @@ func (vac ValidationAuthorityClient) CheckCAARecords(ident core.AcmeIdentifier)
|
|||
return
|
||||
}
|
||||
|
||||
func NewPublisherServer(rpc RPCServer, impl core.Publisher) (err error) {
|
||||
rpc.Handle(MethodSubmitToCT, func(req []byte) (response []byte, err error) {
|
||||
err = impl.SubmitToCT(req)
|
||||
return
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublisherClient is a client to communicate with the Publisher Authority
|
||||
type PublisherClient struct {
|
||||
rpc RPCClient
|
||||
}
|
||||
|
||||
// NewPublisherClient constructs an RPC client
|
||||
func NewPublisherClient(client RPCClient) (pub PublisherClient, err error) {
|
||||
pub = PublisherClient{rpc: client}
|
||||
return
|
||||
}
|
||||
|
||||
// SubmitToCT sends a request to submit a certifcate to CT logs
|
||||
func (pub PublisherClient) SubmitToCT(der []byte) (err error) {
|
||||
_, err = pub.rpc.DispatchSync(MethodSubmitToCT, der)
|
||||
return
|
||||
}
|
||||
|
||||
// NewCertificateAuthorityServer constructs an RPC server
|
||||
//
|
||||
// CertificateAuthorityClient / Server
|
||||
|
@ -987,6 +1016,49 @@ func NewStorageAuthorityServer(rpc RPCServer, impl core.StorageAuthority) error
|
|||
return
|
||||
})
|
||||
|
||||
rpc.Handle(MethodGetSCTReceipt, func(req []byte) (response []byte, err error) {
|
||||
var gsctReq struct {
|
||||
Serial string
|
||||
LogID string
|
||||
}
|
||||
|
||||
err = json.Unmarshal(req, &gsctReq)
|
||||
if err != nil {
|
||||
// AUDIT[ Improper Messages ] 0786b6f2-91ca-4f48-9883-842a19084c64
|
||||
improperMessage(MethodGetSCTReceipt, err, req)
|
||||
return
|
||||
}
|
||||
|
||||
sct, err := impl.GetSCTReceipt(gsctReq.Serial, gsctReq.LogID)
|
||||
jsonResponse, err := json.Marshal(core.RPCSignedCertificateTimestamp(sct))
|
||||
if err != nil {
|
||||
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
|
||||
errorCondition(MethodGetSCTReceipt, err, req)
|
||||
return
|
||||
}
|
||||
|
||||
return jsonResponse, nil
|
||||
})
|
||||
|
||||
rpc.Handle(MethodAddSCTReceipt, func(req []byte) (response []byte, err error) {
|
||||
var sct core.RPCSignedCertificateTimestamp
|
||||
err = json.Unmarshal(req, &sct)
|
||||
if err != nil {
|
||||
// AUDIT[ Improper Messages ] 0786b6f2-91ca-4f48-9883-842a19084c64
|
||||
improperMessage(MethodAddSCTReceipt, err, req)
|
||||
return
|
||||
}
|
||||
|
||||
err = impl.AddSCTReceipt(core.SignedCertificateTimestamp(sct))
|
||||
if err != nil {
|
||||
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
|
||||
errorCondition(MethodGetCertificateByShortSerial, err, req)
|
||||
return
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1249,3 +1321,35 @@ func (cac StorageAuthorityClient) AlreadyDeniedCSR(names []string) (exists bool,
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cac StorageAuthorityClient) GetSCTReceipt(serial string, logID string) (receipt core.SignedCertificateTimestamp, err error) {
|
||||
var gsctReq struct {
|
||||
Serial string
|
||||
LogID string
|
||||
}
|
||||
gsctReq.Serial = serial
|
||||
gsctReq.LogID = logID
|
||||
|
||||
data, err := json.Marshal(gsctReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := cac.rpc.DispatchSync(MethodGetSCTReceipt, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(response, receipt)
|
||||
return
|
||||
}
|
||||
|
||||
func (cac StorageAuthorityClient) AddSCTReceipt(sct core.SignedCertificateTimestamp) (err error) {
|
||||
data, err := json.Marshal(sct)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = cac.rpc.DispatchSync(MethodAddSCTReceipt, data)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
|
||||
CREATE TABLE `sctReceipts` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`sctVersion` tinyint(1) NOT NULL,
|
||||
`logID` varchar(255) NOT NULL,
|
||||
`timestamp` bigint(20) NOT NULL,
|
||||
`extensions` blob,
|
||||
`signature` blob,
|
||||
`certificateSerial` varchar(255) NOT NULL,
|
||||
`LockCol` bigint(20) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `certificateSerial_logID` (`certificateSerial`, `logID`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
||||
DROP TABLE `sctReceipts`;
|
|
@ -142,4 +142,5 @@ func initTables(dbMap *gorp.DbMap) {
|
|||
dbMap.AddTableWithName(core.OCSPResponse{}, "ocspResponses").SetKeys(true, "ID")
|
||||
dbMap.AddTableWithName(core.CRL{}, "crls").SetKeys(false, "Serial")
|
||||
dbMap.AddTableWithName(core.DeniedCSR{}, "deniedCSRs").SetKeys(true, "ID")
|
||||
dbMap.AddTableWithName(core.SignedCertificateTimestamp{}, "sctReceipts").SetKeys(true, "ID").SetVersionCol("LockCol")
|
||||
}
|
||||
|
|
|
@ -636,3 +636,46 @@ func (ssa *SQLStorageAuthority) AlreadyDeniedCSR(names []string) (already bool,
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
// ErrNoReceipt is a error type for non-existent SCT receipt
|
||||
type ErrNoReceipt string
|
||||
|
||||
func (e ErrNoReceipt) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// GetSCTReceipt gets a specific SCT receipt for a given certificate serial and
|
||||
// CT log ID
|
||||
func (ssa *SQLStorageAuthority) GetSCTReceipt(serial string, logID string) (receipt core.SignedCertificateTimestamp, err error) {
|
||||
err = ssa.dbMap.SelectOne(
|
||||
&receipt,
|
||||
"SELECT * FROM sctReceipts WHERE certificateSerial = :serial AND logID = :logID",
|
||||
map[string]interface{}{
|
||||
"serial": serial,
|
||||
"logID": logID,
|
||||
},
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = ErrNoReceipt(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ErrDuplicateReceipt is a error type for duplicate SCT receipts
|
||||
type ErrDuplicateReceipt string
|
||||
|
||||
func (e ErrDuplicateReceipt) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// AddSCTReceipt adds a new SCT receipt to the (append-only) sctReceipts table
|
||||
func (ssa *SQLStorageAuthority) AddSCTReceipt(sct core.SignedCertificateTimestamp) error {
|
||||
err := ssa.dbMap.Insert(&sct)
|
||||
if err != nil && strings.HasPrefix(err.Error(), "Error 1062: Duplicate entry") {
|
||||
err = ErrDuplicateReceipt(err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
package sa
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
@ -18,6 +20,7 @@ import (
|
|||
|
||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock"
|
||||
jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/letsencrypt/boulder/mocks"
|
||||
"github.com/letsencrypt/boulder/sa/satest"
|
||||
|
@ -367,6 +370,58 @@ func TestDeniedCSR(t *testing.T) {
|
|||
test.Assert(t, !exists, "Found non-existent CSR")
|
||||
}
|
||||
|
||||
const (
|
||||
sctVersion = 0
|
||||
sctTimestamp = 1435787268907
|
||||
sctLogID = "aPaY+B9kgr46jO65KB1M/HFRXWeT1ETRCmesu09P+8Q="
|
||||
sctSignature = "BAMASDBGAiEA/4kz9wQq3NhvZ6VlOmjq2Z9MVHGrUjF8uxUG9n1uRc4CIQD2FYnnszKXrR9AP5kBWmTgh3fXy+VlHK8HZXfbzdFf7g=="
|
||||
sctCertSerial = "ff000000000000012607e11a78ac01f9"
|
||||
)
|
||||
|
||||
func TestAddSCTReceipt(t *testing.T) {
|
||||
sigBytes, err := base64.StdEncoding.DecodeString(sctSignature)
|
||||
test.AssertNotError(t, err, "Failed to decode SCT signature")
|
||||
sct := core.SignedCertificateTimestamp{
|
||||
SCTVersion: sctVersion,
|
||||
LogID: sctLogID,
|
||||
Timestamp: sctTimestamp,
|
||||
Signature: sigBytes,
|
||||
CertificateSerial: sctCertSerial,
|
||||
}
|
||||
sa, _, cleanup := initSA(t)
|
||||
defer cleanup()
|
||||
err = sa.AddSCTReceipt(sct)
|
||||
test.AssertNotError(t, err, "Failed to add SCT receipt")
|
||||
// Append only and unique on signature and across LogID and CertificateSerial
|
||||
err = sa.AddSCTReceipt(sct)
|
||||
test.AssertError(t, err, "Incorrectly added duplicate SCT receipt")
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
func TestGetSCTReceipt(t *testing.T) {
|
||||
sigBytes, err := base64.StdEncoding.DecodeString(sctSignature)
|
||||
test.AssertNotError(t, err, "Failed to decode SCT signature")
|
||||
sct := core.SignedCertificateTimestamp{
|
||||
SCTVersion: sctVersion,
|
||||
LogID: sctLogID,
|
||||
Timestamp: sctTimestamp,
|
||||
Signature: sigBytes,
|
||||
CertificateSerial: sctCertSerial,
|
||||
}
|
||||
sa, _, cleanup := initSA(t)
|
||||
defer cleanup()
|
||||
err = sa.AddSCTReceipt(sct)
|
||||
test.AssertNotError(t, err, "Failed to add SCT receipt")
|
||||
|
||||
sqlSCT, err := sa.GetSCTReceipt(sctCertSerial, sctLogID)
|
||||
test.AssertNotError(t, err, "Failed to get existing SCT receipt")
|
||||
test.Assert(t, sqlSCT.SCTVersion == sct.SCTVersion, "Invalid SCT version")
|
||||
test.Assert(t, sqlSCT.LogID == sct.LogID, "Invalid log ID")
|
||||
test.Assert(t, sqlSCT.Timestamp == sct.Timestamp, "Invalid timestamp")
|
||||
test.Assert(t, bytes.Compare(sqlSCT.Signature, sct.Signature) == 0, "Invalid signature")
|
||||
test.Assert(t, sqlSCT.CertificateSerial == sct.CertificateSerial, "Invalid certificate serial")
|
||||
}
|
||||
|
||||
func TestUpdateOCSP(t *testing.T) {
|
||||
sa, fc, cleanUp := initSA(t)
|
||||
defer cleanUp()
|
||||
|
|
|
@ -28,6 +28,10 @@
|
|||
"CA": {
|
||||
"client": "CA.client",
|
||||
"server": "CA.server"
|
||||
},
|
||||
"Publisher": {
|
||||
"client": "Publisher.client",
|
||||
"server": "Publisher.server"
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -164,7 +168,22 @@
|
|||
"messageLimit": 0,
|
||||
"nagTimes": ["24h", "72h", "168h", "336h"],
|
||||
"emailTemplate": "test/example-expiration-template",
|
||||
"debugAddr": "localhost:8004"
|
||||
"debugAddr": "localhost:8008"
|
||||
},
|
||||
|
||||
"publisher": {
|
||||
"ct": {
|
||||
"logs": [
|
||||
{
|
||||
"uri": "http://127.0.0.1:4500",
|
||||
"key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfahLEimAoz2t01p3uMziiLOl/fHTDM0YDOhBRuiBARsV4UvxG2LdNgoIGLrtCzWE0J5APC2em4JlvR8EEEFMoA=="
|
||||
}
|
||||
],
|
||||
"submissionRetries": 1,
|
||||
"submissionBackoff": "1s",
|
||||
"intermediateBundleFilename": "test/test-ca.pem"
|
||||
},
|
||||
"debugAddr": "localhost:8009"
|
||||
},
|
||||
|
||||
"common": {
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
#!/bin/bash
|
||||
cd $(dirname $0)/..
|
||||
source test/db-common.sh
|
||||
|
||||
function die() {
|
||||
if [ ! -z "$1" ]; then
|
||||
echo $1 > /dev/stderr
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
SERVICES="ca
|
||||
sa
|
||||
policy"
|
||||
DBENVS="development
|
||||
test
|
||||
integration"
|
||||
|
||||
for svc in $SERVICES; do
|
||||
for dbenv in $DBENVS; do
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
# Common variables used by Goose-related scripts.
|
||||
set -o errexit
|
||||
set -o xtrace
|
||||
|
||||
function die() {
|
||||
if [ ! -z "$1" ]; then
|
||||
echo $1 > /dev/stderr
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
SERVICES="ca
|
||||
sa
|
||||
policy"
|
||||
DBENVS="development
|
||||
test
|
||||
integration"
|
|
@ -1,19 +0,0 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Run this script after pulling changes that have migrations, to migrate your
|
||||
# local DB.
|
||||
#
|
||||
cd $(dirname $0)/..
|
||||
|
||||
source test/db-common.sh
|
||||
|
||||
for svc in $SERVICES; do
|
||||
for dbenv in $DBENVS; do
|
||||
db="boulder_${svc}_${dbenv}"
|
||||
|
||||
goose -path=./$svc/_db/ -env=$dbenv up || die "unable to migrate ${db}"
|
||||
echo "migrated ${db} database"
|
||||
done
|
||||
done
|
||||
echo "migrated all databases"
|
||||
|
|
@ -77,6 +77,7 @@ def start(race_detection):
|
|||
'cmd/boulder-sa',
|
||||
'cmd/boulder-ca',
|
||||
'cmd/boulder-va',
|
||||
'cmd/boulder-publisher',
|
||||
'cmd/ocsp-responder',
|
||||
'test/dns-test-srv'
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue