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:
Roland Shoemaker 2015-09-17 18:11:05 -07:00
parent ac095b0467
commit ff6eca7a29
24 changed files with 1136 additions and 67 deletions

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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()
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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.*)$"

View File

@ -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
}

274
publisher/publisher.go Normal file
View File

@ -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
}

339
publisher/publisher_test.go Normal file
View File

@ -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)
}

View File

@ -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,

View File

@ -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
}

View File

@ -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
}

View File

@ -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`;

View File

@ -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")
}

View File

@ -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
}

View File

@ -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()

View File

@ -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": {

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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'
]