Skip to content

Commit b26eaae

Browse files
committed
Merge pull request letsencrypt#454 from letsencrypt/ct-submission
Submit issued certificates to CT logs
2 parents ac095b0 + ff6eca7 commit b26eaae

24 files changed

+1136
-67
lines changed

ca/certificate-authority.go

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ type CertificateAuthorityImpl struct {
5353
SA core.StorageAuthority
5454
PA core.PolicyAuthority
5555
DB core.CertificateAuthorityDatabase
56+
Publisher core.Publisher
5657
Clk clock.Clock // TODO(jmhodges): should be private, like log
5758
log *blog.AuditLogger
5859
Prefix int // Prepended to the serial number
@@ -95,7 +96,7 @@ func NewCertificateAuthorityImpl(cadb core.CertificateAuthorityDatabase, config
9596
return nil, err
9697
}
9798

98-
issuer, err := loadIssuer(issuerCert)
99+
issuer, err := core.LoadCert(issuerCert)
99100
if err != nil {
100101
return nil, err
101102
}
@@ -170,19 +171,6 @@ func loadKey(keyConfig cmd.KeyConfig) (priv crypto.Signer, err error) {
170171
return
171172
}
172173

173-
func loadIssuer(filename string) (issuerCert *x509.Certificate, err error) {
174-
if filename == "" {
175-
err = errors.New("Issuer certificate was not provided in config.")
176-
return
177-
}
178-
issuerCertPEM, err := ioutil.ReadFile(filename)
179-
if err != nil {
180-
return
181-
}
182-
issuerCert, err = helpers.ParseCertificatePEM(issuerCertPEM)
183-
return
184-
}
185-
186174
// GenerateOCSP produces a new OCSP response and returns it
187175
func (ca *CertificateAuthorityImpl) GenerateOCSP(xferObj core.OCSPSigningRequest) ([]byte, error) {
188176
cert, err := x509.ParseCertificate(xferObj.CertDER)
@@ -402,15 +390,13 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
402390
// Attempt to generate the OCSP Response now. If this raises an error, it is
403391
// logged but is not returned to the caller, as an error at this point does
404392
// not constitute an issuance failure.
405-
406393
certObj, err := x509.ParseCertificate(certDER)
407394
if err != nil {
408395
ca.log.Warning(fmt.Sprintf("Post-Issuance OCSP failed parsing Certificate: %s", err))
409396
return cert, nil
410397
}
411398

412399
serial := core.SerialToString(certObj.SerialNumber)
413-
414400
signRequest := ocsp.SignRequest{
415401
Certificate: certObj,
416402
Status: string(core.OCSPStatusGood),
@@ -428,6 +414,9 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest
428414
return cert, nil
429415
}
430416

417+
// Submit the certificate to any configured CT logs
418+
go ca.Publisher.SubmitToCT(certObj.Raw)
419+
431420
// Do not return an err at this point; caller must know that the Certificate
432421
// was issued. (Also, it should be impossible for err to be non-nil here)
433422
return cert, nil

ca/certificate-authority_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,9 @@ func TestRevoke(t *testing.T) {
205205
defer ctx.cleanUp()
206206
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
207207
test.AssertNotError(t, err, "Failed to create CA")
208-
209208
ca.PA = ctx.pa
210209
ca.SA = ctx.sa
210+
ca.Publisher = &mocks.MockPublisher{}
211211

212212
csr, _ := x509.ParseCertificateRequest(CNandSANCSR)
213213
certObj, err := ca.IssueCertificate(*csr, ctx.reg.ID)
@@ -246,6 +246,7 @@ func TestIssueCertificate(t *testing.T) {
246246
defer ctx.cleanUp()
247247
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
248248
test.AssertNotError(t, err, "Failed to create CA")
249+
ca.Publisher = &mocks.MockPublisher{}
249250
ca.PA = ctx.pa
250251
ca.SA = ctx.sa
251252

@@ -322,6 +323,7 @@ func TestRejectNoName(t *testing.T) {
322323
defer ctx.cleanUp()
323324
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
324325
test.AssertNotError(t, err, "Failed to create CA")
326+
ca.Publisher = &mocks.MockPublisher{}
325327
ca.PA = ctx.pa
326328
ca.SA = ctx.sa
327329

@@ -338,6 +340,7 @@ func TestRejectTooManyNames(t *testing.T) {
338340
defer ctx.cleanUp()
339341
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
340342
test.AssertNotError(t, err, "Failed to create CA")
343+
ca.Publisher = &mocks.MockPublisher{}
341344
ca.PA = ctx.pa
342345
ca.SA = ctx.sa
343346

@@ -352,6 +355,7 @@ func TestDeduplication(t *testing.T) {
352355
defer ctx.cleanUp()
353356
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
354357
test.AssertNotError(t, err, "Failed to create CA")
358+
ca.Publisher = &mocks.MockPublisher{}
355359
ca.PA = ctx.pa
356360
ca.SA = ctx.sa
357361

@@ -381,6 +385,7 @@ func TestRejectValidityTooLong(t *testing.T) {
381385
defer ctx.cleanUp()
382386
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
383387
test.AssertNotError(t, err, "Failed to create CA")
388+
ca.Publisher = &mocks.MockPublisher{}
384389
ca.PA = ctx.pa
385390
ca.SA = ctx.sa
386391

@@ -395,6 +400,7 @@ func TestShortKey(t *testing.T) {
395400
ctx := setup(t)
396401
defer ctx.cleanUp()
397402
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
403+
ca.Publisher = &mocks.MockPublisher{}
398404
ca.PA = ctx.pa
399405
ca.SA = ctx.sa
400406

@@ -408,6 +414,7 @@ func TestRejectBadAlgorithm(t *testing.T) {
408414
ctx := setup(t)
409415
defer ctx.cleanUp()
410416
ca, err := NewCertificateAuthorityImpl(ctx.caDB, ctx.caConfig, ctx.fc, caCertFile)
417+
ca.Publisher = &mocks.MockPublisher{}
411418
ca.PA = ctx.pa
412419
ca.SA = ctx.sa
413420

cmd/boulder-ca/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ func main() {
5757
sac, err := rpc.NewStorageAuthorityClient(saRPC)
5858
cmd.FailOnError(err, "Failed to create SA client")
5959

60+
pubRPC, err := rpc.NewAmqpRPCClient("CA->Publisher", c.AMQP.Publisher.Server, srv.Channel)
61+
cmd.FailOnError(err, "Unable to create RPC client")
62+
63+
pubc, err := rpc.NewPublisherClient(pubRPC)
64+
cmd.FailOnError(err, "Failed to create Publisher client")
65+
66+
cai.Publisher = &pubc
6067
cai.SA = &sac
6168
}
6269

cmd/boulder-publisher/main.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2015 ISRG. All rights reserved
2+
// This Source Code Form is subject to the terms of the Mozilla Public
3+
// License, v. 2.0. If a copy of the MPL was not distributed with this
4+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
6+
package main
7+
8+
import (
9+
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
10+
11+
"github.com/letsencrypt/boulder/cmd"
12+
blog "github.com/letsencrypt/boulder/log"
13+
"github.com/letsencrypt/boulder/publisher"
14+
"github.com/letsencrypt/boulder/rpc"
15+
)
16+
17+
func main() {
18+
app := cmd.NewAppShell("boulder-publisher", "Submits issued certificates to CT logs")
19+
app.Action = func(c cmd.Config) {
20+
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
21+
cmd.FailOnError(err, "Could not connect to statsd")
22+
23+
// Set up logging
24+
auditlogger, err := blog.Dial(c.Syslog.Network, c.Syslog.Server, c.Syslog.Tag, stats)
25+
cmd.FailOnError(err, "Could not connect to syslog")
26+
27+
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
28+
defer auditlogger.AuditPanic()
29+
30+
blog.SetAuditLogger(auditlogger)
31+
32+
pubi, err := publisher.NewPublisherImpl(c.Publisher.CT)
33+
cmd.FailOnError(err, "Could not setup Publisher")
34+
35+
go cmd.DebugServer(c.Publisher.DebugAddr)
36+
go cmd.ProfileCmd("Publisher", stats)
37+
38+
connectionHandler := func(srv *rpc.AmqpRPCServer) {
39+
saRPC, err := rpc.NewAmqpRPCClient("Publisher->SA", c.AMQP.SA.Server, srv.Channel)
40+
cmd.FailOnError(err, "Unable to create SA RPC client")
41+
42+
sac, err := rpc.NewStorageAuthorityClient(saRPC)
43+
cmd.FailOnError(err, "Unable to create SA client")
44+
45+
pubi.SA = &sac
46+
}
47+
48+
pubs, err := rpc.NewAmqpRPCServer(c.AMQP.Publisher.Server, connectionHandler)
49+
cmd.FailOnError(err, "Unable to create Publisher RPC server")
50+
rpc.NewPublisherServer(pubs, &pubi)
51+
52+
auditlogger.Info(app.VersionString())
53+
54+
err = pubs.Start(c)
55+
cmd.FailOnError(err, "Unable to run Publisher RPC server")
56+
}
57+
58+
app.Run()
59+
}

cmd/shell.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141

4242
"github.com/letsencrypt/boulder/core"
4343
blog "github.com/letsencrypt/boulder/log"
44+
"github.com/letsencrypt/boulder/publisher"
4445
)
4546

4647
// Config stores configuration parameters that applications
@@ -56,14 +57,15 @@ type Config struct {
5657

5758
// General
5859
AMQP struct {
59-
Server string
60-
Insecure bool
61-
RA Queue
62-
VA Queue
63-
SA Queue
64-
CA Queue
65-
OCSP Queue
66-
TLS *TLSConfig
60+
Server string
61+
Insecure bool
62+
RA Queue
63+
VA Queue
64+
SA Queue
65+
CA Queue
66+
OCSP Queue
67+
Publisher Queue
68+
TLS *TLSConfig
6769
}
6870

6971
WFE struct {
@@ -164,6 +166,13 @@ type Config struct {
164166
DebugAddr string
165167
}
166168

169+
Publisher struct {
170+
CT publisher.CTConfig
171+
172+
// DebugAddr is the address to run the /debug handlers on.
173+
DebugAddr string
174+
}
175+
167176
ExternalCertImporter struct {
168177
CertsToImportCSVFilename string
169178
DomainsToImportCSVFilename string

core/interfaces.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ type StorageGetter interface {
111111
GetCertificateByShortSerial(string) (Certificate, error)
112112
GetCertificateStatus(string) (CertificateStatus, error)
113113
AlreadyDeniedCSR([]string) (bool, error)
114+
GetSCTReceipt(string, string) (SignedCertificateTimestamp, error)
114115
}
115116

116117
// StorageAdder are the Boulder SA's write/update methods
@@ -125,6 +126,8 @@ type StorageAdder interface {
125126
UpdateOCSP(serial string, ocspResponse []byte) error
126127

127128
AddCertificate([]byte, int64) (string, error)
129+
130+
AddSCTReceipt(SignedCertificateTimestamp) error
128131
}
129132

130133
// StorageAuthority interface represents a simple key/value
@@ -151,3 +154,8 @@ type DNSResolver interface {
151154
LookupCAA(string) ([]*dns.CAA, time.Duration, error)
152155
LookupMX(string) ([]string, time.Duration, error)
153156
}
157+
158+
// Publisher defines the public interface for the Boulder Publisher
159+
type Publisher interface {
160+
SubmitToCT([]byte) error
161+
}

core/objects.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ package core
77

88
import (
99
"crypto/x509"
10+
"encoding/asn1"
1011
"encoding/base64"
1112
"encoding/json"
13+
"errors"
1214
"fmt"
15+
"math/big"
1316
"net"
1417
"strings"
1518
"time"
@@ -553,6 +556,95 @@ type OCSPSigningRequest struct {
553556
RevokedAt time.Time
554557
}
555558

559+
type SignedCertificateTimestamp struct {
560+
ID int `db:"id"`
561+
// The version of the protocol to which the SCT conforms
562+
SCTVersion uint8 `db:"sctVersion"`
563+
// the SHA-256 hash of the log's public key, calculated over
564+
// the DER encoding of the key represented as SubjectPublicKeyInfo.
565+
LogID string `db:"logID"`
566+
// Timestamp (in ms since unix epoc) at which the SCT was issued
567+
Timestamp uint64 `db:"timestamp"`
568+
// For future extensions to the protocol
569+
Extensions []byte `db:"extensions"`
570+
// The Log's signature for this SCT
571+
Signature []byte `db:"signature"`
572+
573+
// The serial of the certificate this SCT is for
574+
CertificateSerial string `db:"certificateSerial"`
575+
576+
LockCol int64
577+
}
578+
579+
type RPCSignedCertificateTimestamp SignedCertificateTimestamp
580+
581+
type rawSignedCertificateTimestamp struct {
582+
Version uint8 `json:"sct_version"`
583+
LogID string `json:"id"`
584+
Timestamp uint64 `json:"timestamp"`
585+
Signature string `json:"signature"`
586+
Extensions string `json:"extensions"`
587+
}
588+
589+
func (sct *SignedCertificateTimestamp) UnmarshalJSON(data []byte) error {
590+
var err error
591+
var rawSCT rawSignedCertificateTimestamp
592+
if err = json.Unmarshal(data, &rawSCT); err != nil {
593+
return fmt.Errorf("Failed to unmarshal SCT receipt, %s", err)
594+
}
595+
sct.LogID = rawSCT.LogID
596+
if err != nil {
597+
return fmt.Errorf("Failed to decode log ID, %s", err)
598+
}
599+
sct.Signature, err = base64.StdEncoding.DecodeString(rawSCT.Signature)
600+
if err != nil {
601+
return fmt.Errorf("Failed to decode SCT signature, %s", err)
602+
}
603+
sct.Extensions, err = base64.StdEncoding.DecodeString(rawSCT.Extensions)
604+
if err != nil {
605+
return fmt.Errorf("Failed to decode SCT extensions, %s", err)
606+
}
607+
sct.SCTVersion = rawSCT.Version
608+
sct.Timestamp = rawSCT.Timestamp
609+
return nil
610+
}
611+
612+
const (
613+
sctHashSHA256 = 4
614+
sctSigECDSA = 3
615+
)
616+
617+
// CheckSignature validates that the returned SCT signature is a valid SHA256 +
618+
// ECDSA signature but does not verify that a specific public key signed it.
619+
func (sct *SignedCertificateTimestamp) CheckSignature() error {
620+
if len(sct.Signature) < 4 {
621+
return errors.New("SCT signature is truncated")
622+
}
623+
// Since all of the known logs currently only use SHA256 hashes and ECDSA
624+
// keys, only allow those
625+
if sct.Signature[0] != sctHashSHA256 {
626+
return fmt.Errorf("Unsupported SCT hash function [%d]", sct.Signature[0])
627+
}
628+
if sct.Signature[1] != sctSigECDSA {
629+
return fmt.Errorf("Unsupported SCT signature algorithm [%d]", sct.Signature[1])
630+
}
631+
632+
var ecdsaSig struct {
633+
R, S *big.Int
634+
}
635+
// Ignore the two length bytes and attempt to unmarshal the signature directly
636+
signatureBytes := sct.Signature[4:]
637+
signatureBytes, err := asn1.Unmarshal(signatureBytes, &ecdsaSig)
638+
if err != nil {
639+
return fmt.Errorf("Failed to parse SCT signature, %s", err)
640+
}
641+
if len(signatureBytes) > 0 {
642+
return fmt.Errorf("Trailing garbage after signature")
643+
}
644+
645+
return nil
646+
}
647+
556648
// RevocationCode is used to specify a certificate revocation reason
557649
type RevocationCode int
558650

0 commit comments

Comments
 (0)