diff --git a/authority/tls.go b/authority/tls.go index 3afc52cc..7a2a386e 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -188,7 +188,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts SignOptions, ext if a.ctClient != nil { // Submit final certificate chain - if err := a.ctClient.SubmitToLogs(serverCert.Raw, caCert.Raw); err != nil { + if _, err := a.ctClient.SubmitToLogs(serverCert.Raw, caCert.Raw); err != nil { return nil, nil, &apiError{errors.Wrap(err, "sign: error submitting final certificate to ct logs"), http.StatusBadGateway, errContext} } @@ -306,7 +306,7 @@ func (a *Authority) Renew(ocx *x509.Certificate) (*x509.Certificate, *x509.Certi if a.ctClient != nil { // Submit final certificate chain - if err := a.ctClient.SubmitToLogs(serverCert.Raw, caCert.Raw); err != nil { + if _, err := a.ctClient.SubmitToLogs(serverCert.Raw, caCert.Raw); err != nil { return nil, nil, &apiError{errors.Wrap(err, "renew: error submitting final certificate to ct logs"), http.StatusBadGateway, context{}} } @@ -373,7 +373,7 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { if a.ctClient != nil { // Submit final certificate chain - if err := a.ctClient.SubmitToLogs(crtBytes, intermediatePEM.Bytes); err != nil { + if _, err := a.ctClient.SubmitToLogs(crtBytes, intermediatePEM.Bytes); err != nil { return nil, errors.Wrap(err, "error submitting final certificate to ct logs") } } diff --git a/ct/ct.go b/ct/ct.go index 291f1a9a..7165cb82 100644 --- a/ct/ct.go +++ b/ct/ct.go @@ -26,8 +26,10 @@ var ( // Config represents the configuration for the certificate authority client. type Config struct { - URI string `json:"uri"` - Key string `json:"key"` + URI string `json:"uri"` + Key string `json:"key"` + NotAfterStart time.Time `json:"notAfterStart,omitempty"` + NotAfterLimit time.Time `json:"notAfterLimit,omitempty"` } // Validate validates the ct configuration. @@ -42,10 +44,10 @@ func (c *Config) Validate() error { } } -// Client is the interfaced used to communicate with the certificate transparency logs. +// Client is the interface used to communicate with the certificate transparency logs. type Client interface { GetSCTs(asn1Data ...[]byte) (*SCT, error) - SubmitToLogs(asn1Data ...[]byte) error + SubmitToLogs(asn1Data ...[]byte) (*SCT, error) } type logClient interface { @@ -102,19 +104,14 @@ type ClientImpl struct { // New creates a new Client func New(c Config) (*ClientImpl, error) { - // Extract DER from key - data, err := ioutil.ReadFile(c.Key) + der, err := readPublicKey(c.Key) if err != nil { - return nil, errors.Wrapf(err, "error reading %s", c.Key) - } - block, rest := pem.Decode(data) - if block == nil || len(rest) > 0 { - return nil, errors.Wrapf(err, "invalid public key %s", c.Key) + return nil, err } // Initialize ct client logClient, err := client.New(c.URI, &http.Client{}, jsonclient.Options{ - PublicKeyDER: block.Bytes, + PublicKeyDER: der, UserAgent: "smallstep certificates", }) if err != nil { @@ -146,7 +143,7 @@ func (c *ClientImpl) GetSCTs(asn1Data ...[]byte) (*SCT, error) { if err != nil { return nil, errors.Wrapf(err, "failed to get SCT from %s", c.config.URI) } - logLeafHash(asn1Data, sct, true) + logLeafHash("AddPreChain", asn1Data, sct, true) return &SCT{ LogURL: c.config.URI, SCT: sct, @@ -154,16 +151,32 @@ func (c *ClientImpl) GetSCTs(asn1Data ...[]byte) (*SCT, error) { } // SubmitToLogs submits the certificate to the certificate transparency logs. -func (c *ClientImpl) SubmitToLogs(asn1Data ...[]byte) error { +func (c *ClientImpl) SubmitToLogs(asn1Data ...[]byte) (*SCT, error) { chain := chainFromDERs(asn1Data) ctx, cancel := context.WithTimeout(context.Background(), c.timeout) defer cancel() sct, err := c.logClient.AddChain(ctx, chain) if err != nil { - return errors.Wrapf(err, "failed submit certificate to %s", c.config.URI) + return nil, errors.Wrapf(err, "failed submit certificate to %s", c.config.URI) } - logLeafHash(asn1Data, sct, false) - return nil + logLeafHash("AddChain", asn1Data, sct, false) + return &SCT{ + LogURL: c.config.URI, + SCT: sct, + }, nil +} + +// readPublicKey extracts the DER from the given file. +func readPublicKey(filename string) ([]byte, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, errors.Wrapf(err, "error reading %s", filename) + } + block, rest := pem.Decode(data) + if block == nil || len(rest) > 0 { + return nil, errors.Wrapf(err, "invalid public key %s", filename) + } + return block.Bytes, nil } func chainFromDERs(asn1Data [][]byte) []ct.ASN1Cert { @@ -174,7 +187,7 @@ func chainFromDERs(asn1Data [][]byte) []ct.ASN1Cert { return chain } -func logLeafHash(asn1Data [][]byte, sct *ct.SignedCertificateTimestamp, isPrecert bool) { +func logLeafHash(op string, asn1Data [][]byte, sct *ct.SignedCertificateTimestamp, isPrecert bool) { var etype ct.LogEntryType if isPrecert { etype = ct.PrecertLogEntryType @@ -204,5 +217,5 @@ func logLeafHash(asn1Data [][]byte, sct *ct.SignedCertificateTimestamp, isPrecer return } - log.Printf("LogID: %x, LeafHash: %x, Timestamp: %d\n", sct.LogID.KeyID[:], leafHash, sct.Timestamp) + log.Printf("Op: %s, LogID: %x, LeafHash: %x, Timestamp: %d\n", op, sct.LogID.KeyID[:], leafHash, sct.Timestamp) } diff --git a/ct/multilog.go b/ct/multilog.go new file mode 100644 index 00000000..3af65182 --- /dev/null +++ b/ct/multilog.go @@ -0,0 +1,84 @@ +package ct + +// MultiLog is the interface used to send certificates to multiple logs. +type MultiLog interface { + GetSCTs(asn1Data ...[]byte) ([]*SCT, error) + SubmitToLogs(asn1Data ...[]byte) ([]*SCT, error) +} + +// MultiLogImpl is the implementation used to send certificates to multiple +// logs. +type MultiLogImpl struct { + clients []Client + configs []Config +} + +type result struct { + sct *SCT + err error + uri string +} + +// NewMultiLog returns a MultiLog with the given configuration. +func NewMultiLog(config []Config) (MultiLog, error) { + ml := new(MultiLogImpl) + for _, cfg := range config { + client, err := New(cfg) + if err != nil { + return nil, err + } + ml.clients = append(ml.clients, client) + } + return ml, nil +} + +// GetSCTs submit the precertificate to the logs and returns the list of SCTs to +// embed into the certificate. +func (c *MultiLogImpl) GetSCTs(asn1Data ...[]byte) (scts []*SCT, err error) { + results := make(chan result, len(c.clients)) + for i := range c.clients { + client := c.clients[i] + config := c.configs[i] + go func() { + sct, err := client.GetSCTs(asn1Data...) + results <- result{sct: sct, err: err, uri: config.URI} + }() + } + + for i := 0; i < len(c.clients); i++ { + res := <-results + switch { + case res.sct != nil: + scts = append(scts, res.sct) + case res.err != nil && err != nil: + err = res.err + } + } + + return scts, err +} + +// SubmitToLogs submits the certificate to the certificate transparency logs. +func (c *MultiLogImpl) SubmitToLogs(asn1Data ...[]byte) (scts []*SCT, err error) { + results := make(chan result, len(c.clients)) + for i := range c.clients { + client := c.clients[i] + config := c.configs[i] + go func() { + sct, err := client.SubmitToLogs(asn1Data...) + results <- result{sct: sct, err: err, uri: config.URI} + }() + } + + for i := 0; i < len(c.clients); i++ { + res := <-results + switch { + case res.sct != nil: + scts = append(scts, res.sct) + case res.err != nil && err != nil: + err = res.err + } + } + + return scts, err +}