diff --git a/authority/authority.go b/authority/authority.go index 5a0cf1ab..5625d549 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/smallstep/certificates/ct" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/crypto/x509util" ) @@ -28,6 +29,7 @@ type Authority struct { provisionerKeySetIndex *sync.Map sortedProvisioners provisionerSlice audiences []string + ctClient ct.Client // Do not re-initialize initOnce bool } @@ -55,6 +57,14 @@ func New(config *Config) (*Authority, error) { audiences = append(audiences, fmt.Sprintf("https://%s/sign", name), fmt.Sprintf("https://%s/1.0/sign", name)) } + var ctClient ct.Client + // only first one is supported at the moment. + if len(config.CTs) > 0 { + if ctClient, err = ct.New(config.CTs[0]); err != nil { + return nil, err + } + } + var a = &Authority{ config: config, certificates: new(sync.Map), @@ -64,6 +74,7 @@ func New(config *Config) (*Authority, error) { provisionerKeySetIndex: new(sync.Map), sortedProvisioners: sorted, audiences: audiences, + ctClient: ctClient, } if err := a.init(); err != nil { return nil, err diff --git a/authority/config.go b/authority/config.go index 3bc8e810..1cb013cf 100644 --- a/authority/config.go +++ b/authority/config.go @@ -7,6 +7,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/ct" "github.com/smallstep/cli/crypto/tlsutil" "github.com/smallstep/cli/crypto/x509util" ) @@ -46,6 +47,7 @@ type Config struct { AuthorityConfig *AuthConfig `json:"authority,omitempty"` TLS *tlsutil.TLSOptions `json:"tls,omitempty"` Password string `json:"password,omitempty"` + CTs []ct.Config `json:"cts"` } // AuthConfig represents the configuration options for the authority. @@ -153,5 +155,13 @@ func (c *Config) Validate() error { c.TLS.Renegotiation = c.TLS.Renegotiation || DefaultTLSOptions.Renegotiation } + if len(c.CTs) > 0 { + for _, ct := range c.CTs { + if err := ct.Validate(); err != nil { + return err + } + } + } + return c.AuthorityConfig.Validate() } diff --git a/authority/tls.go b/authority/tls.go index 03999f77..883c13a9 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -123,6 +123,11 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts SignOptions, ext } } + // Add CT Poison extension + if a.ctClient != nil { + mods = append(mods, x509util.WithCTPoison()) + } + stepCSR, err := stepx509.ParseCertificateRequest(csr.Raw) if err != nil { return nil, nil, &apiError{errors.Wrap(err, "sign: error converting x509 csr to stepx509 csr"), @@ -158,6 +163,47 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts SignOptions, ext http.StatusInternalServerError, errContext} } + // Certificate transparency + if a.ctClient != nil { + scts, err := a.ctClient.GetSCTs(serverCert, caCert) + if err != nil { + return nil, nil, &apiError{errors.Wrap(err, "sign: error getting SCTs for certificate"), + http.StatusBadGateway, errContext} + } + crt := leaf.Subject() + crt.ExtraExtensions = append(crt.ExtraExtensions, scts.GetExtension()) + for i, ext := range crt.ExtraExtensions { + if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}) { + crt.ExtraExtensions = append(crt.ExtraExtensions[:i], crt.ExtraExtensions[i+1:]...) + break + } + } + + for i, ext := range crt.Extensions { + if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}) { + crt.Extensions = append(crt.Extensions[:i], crt.Extensions[i+1:]...) + break + } + } + + crtBytes, err = leaf.CreateCertificate() + if err != nil { + return nil, nil, &apiError{errors.Wrap(err, "sign: error creating new leaf certificate"), + http.StatusInternalServerError, errContext} + } + + serverCert, err = x509.ParseCertificate(crtBytes) + if err != nil { + return nil, nil, &apiError{errors.Wrap(err, "sign: error parsing new leaf certificate"), + http.StatusInternalServerError, errContext} + } + + if err := a.ctClient.SubmitToLogs(serverCert, caCert); err != nil { + return nil, nil, &apiError{errors.Wrap(err, "sign: error submitting final certificate to ct logs"), + http.StatusBadGateway, errContext} + } + } + return serverCert, caCert, nil } diff --git a/ct/ct.go b/ct/ct.go new file mode 100644 index 00000000..c74f9ff6 --- /dev/null +++ b/ct/ct.go @@ -0,0 +1,185 @@ +package ct + +import ( + "context" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "fmt" + "io/ioutil" + "log" + "net/http" + "time" + + ct "github.com/google/certificate-transparency-go" + "github.com/google/certificate-transparency-go/client" + "github.com/google/certificate-transparency-go/jsonclient" + cttls "github.com/google/certificate-transparency-go/tls" + ctx509 "github.com/google/certificate-transparency-go/x509" + "github.com/pkg/errors" +) + +var ( + oidExtensionCTPoison = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3} + oidSignedCertificateTimestampList = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2} +) + +// Config represents the configuration for the certificate authority client. +type Config struct { + URI string `json:"uri"` + Key string `json:"key"` +} + +// Validate validates the ct configuration. +func (c *Config) Validate() error { + switch { + case c.URI == "": + return errors.New("ct uri cannot be empty") + case c.Key == "": + return errors.New("ct key cannot be empty") + default: + return nil + } +} + +// Client is the interfaced used to communicate with the certificate transparency logs. +type Client interface { + GetSCTs(chain ...*x509.Certificate) (*SCT, error) + SubmitToLogs(chain ...*x509.Certificate) error +} + +type logClient interface { + AddPreChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) + AddChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error) +} + +// SCT represents a Signed Certificate Timestamp. +type SCT struct { + LogURL string + SCT *ct.SignedCertificateTimestamp +} + +// GetExtension returns the extension representing an SCT that will be added to +// a certificate. +func (t *SCT) GetExtension() pkix.Extension { + val, err := cttls.Marshal(*t.SCT) + if err != nil { + panic(err) + } + value, err := cttls.Marshal(ctx509.SignedCertificateTimestampList{ + SCTList: []ctx509.SerializedSCT{ + {Val: val}, + }, + }) + if err != nil { + panic(err) + } + rawValue, err := asn1.Marshal(value) + if err != nil { + panic(err) + } + return pkix.Extension{ + Id: oidSignedCertificateTimestampList, + Critical: false, + Value: rawValue, + } +} + +// AddPoisonExtension appends the ct poison extension to the given certificate. +func AddPoisonExtension(cert *x509.Certificate) { + cert.Extensions = append(cert.Extensions, pkix.Extension{ + Id: oidExtensionCTPoison, + Critical: true, + }) +} + +// ClientImpl is the implementation of a certificate transparency Client. +type ClientImpl struct { + config Config + logClient logClient + timeout time.Duration +} + +// New creates a new Client +func New(c Config) (*ClientImpl, error) { + // Extract DER from key + data, err := ioutil.ReadFile(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) + } + + // Initialize ct client + logClient, err := client.New(c.URI, &http.Client{}, jsonclient.Options{ + PublicKeyDER: block.Bytes, + UserAgent: "smallstep certificates", + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to create client to %s", c.URI) + } + + // Validate connection + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if _, err := logClient.GetSTH(ctx); err != nil { + return nil, errors.Wrapf(err, "failed to connect to %s", c.URI) + } + log.Printf("connecting to CT log %s", c.URI) + + return &ClientImpl{ + config: c, + logClient: logClient, + timeout: 30 * time.Second, + }, nil +} + +// GetSCTs submit the precertificate to the logs and returns the list of SCTs to +// embed into the certificate. +func (c *ClientImpl) GetSCTs(chain ...*x509.Certificate) (*SCT, error) { + ctChain := chainFromCerts(chain) + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + sct, err := c.logClient.AddPreChain(ctx, ctChain) + if err != nil { + return nil, errors.Wrapf(err, "failed to get SCT from %s", c.config.URI) + } + return &SCT{ + LogURL: c.config.URI, + SCT: sct, + }, nil +} + +// SubmitToLogs submits the certificate to the certificate transparency logs. +func (c *ClientImpl) SubmitToLogs(chain ...*x509.Certificate) error { + ctChain := chainFromCerts(chain) + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + sct, err := c.logClient.AddChain(ctx, ctChain) + if err != nil { + return errors.Wrapf(err, "failed submit certificate to %s", c.config.URI) + } + + // Calculate the leaf hash + leafEntry := ct.CreateX509MerkleTreeLeaf(ctChain[0], sct.Timestamp) + leafHash, err := ct.LeafHashForLeaf(leafEntry) + if err != nil { + log.Println(err) + } + // Display the SCT + fmt.Printf("LogID: %x\n", sct.LogID.KeyID[:]) + fmt.Printf("LeafHash: %x\n", leafHash) + + return nil +} + +func chainFromCerts(certs []*x509.Certificate) []ct.ASN1Cert { + var chain []ct.ASN1Cert + for _, cert := range certs { + chain = append(chain, ct.ASN1Cert{Data: cert.Raw}) + } + return chain +}