Compare commits

...

14 commits

Author SHA1 Message Date
Mariano Cano
abd0cf23f9 Add experimental message in logs 2019-10-09 16:39:22 -07:00
Mariano Cano
9f65e820ae Add missing new line 2019-10-09 16:28:50 -07:00
Mariano Cano
6892017590 Try to fix dependency issues. 2019-10-09 12:57:53 -07:00
Mariano Cano
2047f74da6 Update dependencies for ct. 2019-10-09 12:31:55 -07:00
Mariano Cano
e0d58ca640 Fix merge 2019-10-09 12:22:20 -07:00
Mariano Cano
afb3c7cd08 Merge branch 'master' into certificate-transparency 2019-10-09 11:23:52 -07:00
Mariano Cano
600b1db302 Update cli dependency to add the extensions properly 2019-03-12 12:51:50 -07:00
Mariano Cano
b9104a92f9 Fix typo + dep ensure. 2019-02-15 10:34:23 -08:00
Mariano Cano
dff3f6f270 Merge branch 'master' into certificate-transparency 2019-02-15 10:29:24 -08:00
Mariano Cano
963fe0fa91 Add initial implementation of a multilog. 2019-02-12 16:55:01 -08:00
Mariano Cano
b766f49995 Add ct dependencies. 2019-02-11 19:31:54 -08:00
Mariano Cano
7012500aac Submit to ct log the renew and CA server certificate
Related to smallstep/ca-component#142
2019-02-11 18:57:14 -08:00
Mariano Cano
7b175004cb Initialize certNotAfter to avoid a renew when the server starts. 2019-02-11 18:55:32 -08:00
Mariano Cano
19c4842cdf Initial POS of certificate transparency
Related to smallstep/ca-component#142
2019-02-11 11:07:37 -08:00
8 changed files with 1656 additions and 16 deletions

1223
Gopkg.lock generated

File diff suppressed because it is too large Load diff

View file

@ -51,6 +51,10 @@
name = "gopkg.in/square/go-jose.v2"
version = "2.3.1"
[[constraint]]
branch = "master"
name = "github.com/google/certificate-transparency-go"
[prune]
go-tests = true
unused-packages = true

View file

@ -10,6 +10,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/ct"
"github.com/smallstep/certificates/db"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/x509util"
@ -30,6 +31,7 @@ type Authority struct {
startTime time.Time
provisioners *provisioner.Collection
db db.AuthDB
ctClient ct.Client
// Do not re-initialize
initOnce bool
}
@ -52,10 +54,19 @@ func New(config *Config, opts ...Option) (*Authority, error) {
return nil, err
}
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),
provisioners: provisioner.NewCollection(config.getAudiences()),
ctClient: ctClient,
}
for _, opt := range opts {
opt(a)

View file

@ -9,6 +9,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/ct"
"github.com/smallstep/certificates/db"
"github.com/smallstep/cli/crypto/tlsutil"
"github.com/smallstep/cli/crypto/x509util"
@ -59,6 +60,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.
@ -181,6 +183,14 @@ 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(c.getAudiences())
}

View file

@ -23,7 +23,12 @@ func (a *Authority) GetTLSOptions() *tlsutil.TLSOptions {
return a.config.TLS
}
var oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35}
var (
oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35}
// Certificate transparency extensions OIDs
ctPoisonOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}
ctSigendCertificateTimestampOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}
)
func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption {
return func(p x509util.Profile) error {
@ -79,6 +84,11 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti
}
}
// Add CT Poison extension
if a.ctClient != nil {
mods = append(mods, x509util.WithCTPoison())
}
if err := csr.CheckSignature(); err != nil {
return nil, nil, &apiError{errors.Wrap(err, "sign: invalid certificate request"),
http.StatusBadRequest, errContext}
@ -101,6 +111,25 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti
http.StatusInternalServerError, errContext}
}
if a.ctClient != nil {
// Submit precertificate chain and get SCTs
scts, err := a.ctClient.GetSCTs(crtBytes, issIdentity.Crt.Raw)
if err != nil {
return nil, nil, &apiError{errors.Wrap(err, "sign: error getting SCTs for certificate"),
http.StatusBadGateway, errContext}
}
// Remove ct poison extension and add sct extension
leaf.RemoveExtension(ctPoisonOID)
leaf.AddExtension(scts.GetExtension())
// Recreate final certificate
if crtBytes, err = leaf.CreateCertificate(); err != nil {
return nil, nil, &apiError{errors.Wrap(err, "sign: error creating final 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"),
@ -120,6 +149,14 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti
}
}
if a.ctClient != nil {
// Submit final certificate chain
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}
}
}
return serverCert, caCert, nil
}
@ -178,17 +215,45 @@ func (a *Authority) Renew(oldCert *x509.Certificate) (*x509.Certificate, *x509.C
}
}
leaf, err := x509util.NewLeafProfileWithTemplate(newCert,
issIdentity.Crt, issIdentity.Key)
opts := []x509util.WithOption{}
// Add CT Poison extension
if a.ctClient != nil {
opts = append(opts, x509util.WithCTPoison())
}
leaf, err := x509util.NewLeafProfileWithTemplate(newCert, issIdentity.Crt, issIdentity.Key, opts...)
if err != nil {
return nil, nil, &apiError{err, http.StatusInternalServerError, apiCtx{}}
}
// Remove previous SCTs if any
leaf.RemoveExtension(ctSigendCertificateTimestampOID)
crtBytes, err := leaf.CreateCertificate()
if err != nil {
return nil, nil, &apiError{errors.Wrap(err, "error renewing certificate from existing server certificate"),
http.StatusInternalServerError, apiCtx{}}
}
if a.ctClient != nil {
// Submit precertificate chain and get SCTs
scts, err := a.ctClient.GetSCTs(crtBytes, issIdentity.Crt.Raw)
if err != nil {
return nil, nil, &apiError{errors.Wrap(err, "renew: error getting SCTs for certificate"),
http.StatusBadGateway, apiCtx{}}
}
// Remove ct poison extension and add sct extension
leaf.RemoveExtension(ctPoisonOID)
leaf.AddExtension(scts.GetExtension())
// Recreate final certificate
if crtBytes, err = leaf.CreateCertificate(); err != nil {
return nil, nil, &apiError{errors.Wrap(err, "renew: error creating final leaf certificate"),
http.StatusInternalServerError, apiCtx{}}
}
}
serverCert, err := x509.ParseCertificate(crtBytes)
if err != nil {
return nil, nil, &apiError{errors.Wrap(err, "error parsing new server certificate"),
@ -200,6 +265,14 @@ func (a *Authority) Renew(oldCert *x509.Certificate) (*x509.Certificate, *x509.C
http.StatusInternalServerError, apiCtx{}}
}
if a.ctClient != nil {
// Submit final certificate chain
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, apiCtx{}}
}
}
return serverCert, caCert, nil
}
@ -278,9 +351,17 @@ func (a *Authority) Revoke(opts *RevokeOptions) error {
// GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server.
func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
opts := []x509util.WithOption{
x509util.WithHosts(strings.Join(a.config.DNSNames, ",")),
}
// Add CT Poison extension
if a.ctClient != nil {
opts = append(opts, x509util.WithCTPoison())
}
profile, err := x509util.NewLeafProfile("Step Online CA",
a.intermediateIdentity.Crt, a.intermediateIdentity.Key,
x509util.WithHosts(strings.Join(a.config.DNSNames, ",")))
a.intermediateIdentity.Crt, a.intermediateIdentity.Key, opts...)
if err != nil {
return nil, err
}
@ -290,6 +371,23 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
return nil, err
}
if a.ctClient != nil {
// Submit precertificate chain and get SCTs
scts, err := a.ctClient.GetSCTs(crtBytes, a.intermediateIdentity.Crt.Raw)
if err != nil {
return nil, errors.Wrap(err, "error getting SCTs for certificate")
}
// Remove ct poison extension and add sct extension
profile.RemoveExtension(ctPoisonOID)
profile.AddExtension(scts.GetExtension())
// Recreate final certificate
if crtBytes, err = profile.CreateCertificate(); err != nil {
return nil, errors.Wrap(err, "error creating final leaf certificate")
}
}
keyPEM, err := pemutil.Serialize(profile.SubjectPrivateKey())
if err != nil {
return nil, err
@ -306,6 +404,14 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
if err != nil {
return nil, err
}
if a.ctClient != nil {
// Submit final certificate chain
if _, err := a.ctClient.SubmitToLogs(crtBytes, intermediatePEM.Bytes); err != nil {
return nil, errors.Wrap(err, "error submitting final certificate to ct logs")
}
}
tlsCrt, err := tls.X509KeyPair(append(crtPEM,
pem.EncodeToMemory(intermediatePEM)...),
pem.EncodeToMemory(keyPEM))

View file

@ -72,6 +72,8 @@ func NewTLSRenewer(cert *tls.Certificate, fn RenewFunc, opts ...tlsRenewerOption
if r.renewJitter == 0 {
r.renewJitter = period / 20
}
// Initialize certNotAfter
r.certNotAfter = cert.Leaf.NotAfter.Add(-1 * time.Minute)
return r, nil
}

222
ct/ct.go Normal file
View file

@ -0,0 +1,222 @@
package ct
import (
"context"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"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"`
NotAfterStart time.Time `json:"notAfterStart,omitempty"`
NotAfterLimit time.Time `json:"notAfterLimit,omitempty"`
}
// 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 interface used to communicate with the certificate transparency logs.
type Client interface {
GetSCTs(asn1Data ...[]byte) (*SCT, error)
SubmitToLogs(asn1Data ...[]byte) (*SCT, error)
}
type logClient interface {
AddChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error)
AddPreChain(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) {
der, err := readPublicKey(c.Key)
if err != nil {
return nil, err
}
// Initialize ct client
logClient, err := client.New(c.URI, &http.Client{}, jsonclient.Options{
PublicKeyDER: der,
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)
log.Println("CT support is experimental and can change at any time")
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(asn1Data ...[]byte) (*SCT, error) {
chain := chainFromDERs(asn1Data)
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
sct, err := c.logClient.AddPreChain(ctx, chain)
if err != nil {
return nil, errors.Wrapf(err, "failed to get SCT from %s", c.config.URI)
}
logLeafHash("AddPreChain", asn1Data, sct, true)
return &SCT{
LogURL: c.config.URI,
SCT: sct,
}, nil
}
// SubmitToLogs submits the certificate to the certificate transparency logs.
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 nil, errors.Wrapf(err, "failed submit certificate to %s", c.config.URI)
}
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 {
var chain []ct.ASN1Cert
for _, der := range asn1Data {
chain = append(chain, ct.ASN1Cert{Data: der})
}
return chain
}
func logLeafHash(op string, asn1Data [][]byte, sct *ct.SignedCertificateTimestamp, isPrecert bool) {
var etype ct.LogEntryType
if isPrecert {
etype = ct.PrecertLogEntryType
} else {
etype = ct.X509LogEntryType
}
chain := make([]*ctx509.Certificate, len(asn1Data))
for i := range asn1Data {
cert, err := ctx509.ParseCertificate(asn1Data[i])
if err != nil {
log.Println(err)
return
}
chain[i] = cert
}
leafEntry, err := ct.MerkleTreeLeafFromChain(chain, etype, sct.Timestamp)
if err != nil {
log.Println(err)
return
}
leafHash, err := ct.LeafHashForLeaf(leafEntry)
if err != nil {
log.Println(err)
return
}
log.Printf("Op: %s, LogID: %x, LeafHash: %x, Timestamp: %d\n", op, sct.LogID.KeyID[:], leafHash, sct.Timestamp)
}

84
ct/multilog.go Normal file
View file

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