Initial POS of certificate transparency
Related to smallstep/ca-component#142
This commit is contained in:
parent
cedf8784b6
commit
19c4842cdf
4 changed files with 252 additions and 0 deletions
|
@ -8,6 +8,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/ct"
|
||||||
"github.com/smallstep/cli/crypto/pemutil"
|
"github.com/smallstep/cli/crypto/pemutil"
|
||||||
"github.com/smallstep/cli/crypto/x509util"
|
"github.com/smallstep/cli/crypto/x509util"
|
||||||
)
|
)
|
||||||
|
@ -28,6 +29,7 @@ type Authority struct {
|
||||||
provisionerKeySetIndex *sync.Map
|
provisionerKeySetIndex *sync.Map
|
||||||
sortedProvisioners provisionerSlice
|
sortedProvisioners provisionerSlice
|
||||||
audiences []string
|
audiences []string
|
||||||
|
ctClient ct.Client
|
||||||
// Do not re-initialize
|
// Do not re-initialize
|
||||||
initOnce bool
|
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))
|
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{
|
var a = &Authority{
|
||||||
config: config,
|
config: config,
|
||||||
certificates: new(sync.Map),
|
certificates: new(sync.Map),
|
||||||
|
@ -64,6 +74,7 @@ func New(config *Config) (*Authority, error) {
|
||||||
provisionerKeySetIndex: new(sync.Map),
|
provisionerKeySetIndex: new(sync.Map),
|
||||||
sortedProvisioners: sorted,
|
sortedProvisioners: sorted,
|
||||||
audiences: audiences,
|
audiences: audiences,
|
||||||
|
ctClient: ctClient,
|
||||||
}
|
}
|
||||||
if err := a.init(); err != nil {
|
if err := a.init(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/smallstep/certificates/ct"
|
||||||
"github.com/smallstep/cli/crypto/tlsutil"
|
"github.com/smallstep/cli/crypto/tlsutil"
|
||||||
"github.com/smallstep/cli/crypto/x509util"
|
"github.com/smallstep/cli/crypto/x509util"
|
||||||
)
|
)
|
||||||
|
@ -46,6 +47,7 @@ type Config struct {
|
||||||
AuthorityConfig *AuthConfig `json:"authority,omitempty"`
|
AuthorityConfig *AuthConfig `json:"authority,omitempty"`
|
||||||
TLS *tlsutil.TLSOptions `json:"tls,omitempty"`
|
TLS *tlsutil.TLSOptions `json:"tls,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
|
CTs []ct.Config `json:"cts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthConfig represents the configuration options for the authority.
|
// 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
|
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()
|
return c.AuthorityConfig.Validate()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
stepCSR, err := stepx509.ParseCertificateRequest(csr.Raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, &apiError{errors.Wrap(err, "sign: error converting x509 csr to stepx509 csr"),
|
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}
|
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
|
return serverCert, caCert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
185
ct/ct.go
Normal file
185
ct/ct.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue