mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-11-12 21:40:29 +00:00
844 lines
26 KiB
Go
844 lines
26 KiB
Go
// Package bundler implements certificate bundling functionality for
|
|
// CFSSL.
|
|
package bundler
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
goerr "errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cloudflare/cfssl/errors"
|
|
"github.com/cloudflare/cfssl/helpers"
|
|
"github.com/cloudflare/cfssl/log"
|
|
"github.com/cloudflare/cfssl/ubiquity"
|
|
)
|
|
|
|
// IntermediateStash contains the path to the directory where
|
|
// downloaded intermediates should be saved.
|
|
// When unspecified, downloaded intermediates are not saved.
|
|
var IntermediateStash string
|
|
|
|
// BundleFlavor is named optimization strategy on certificate chain selection when bundling.
|
|
type BundleFlavor string
|
|
|
|
const (
|
|
// Optimal means the shortest chain with newest intermediates and
|
|
// the most advanced crypto.
|
|
Optimal BundleFlavor = "optimal"
|
|
|
|
// Ubiquitous is aimed to provide the chain which is accepted
|
|
// by the most platforms.
|
|
Ubiquitous BundleFlavor = "ubiquitous"
|
|
|
|
// Force means the bundler only verfiies the input as a valid bundle, not optimization is done.
|
|
Force BundleFlavor = "force"
|
|
)
|
|
|
|
const (
|
|
sha2Warning = "The bundle contains certificates signed with advanced hash functions such as SHA2, which are problematic for certain operating systems, e.g. Windows XP SP2."
|
|
ecdsaWarning = "The bundle contains ECDSA signatures, which are problematic for certain operating systems, e.g. Windows XP, Android 2.2 and Android 2.3."
|
|
expiringWarningStub = "The bundle is expiring within 30 days."
|
|
untrustedWarningStub = "The bundle may not be trusted by the following platform(s):"
|
|
ubiquityWarning = "Unable to measure bundle ubiquity: No platform metadata present."
|
|
)
|
|
|
|
// A Bundler contains the certificate pools for producing certificate
|
|
// bundles. It contains any intermediates and root certificates that
|
|
// should be used.
|
|
type Bundler struct {
|
|
RootPool *x509.CertPool
|
|
IntermediatePool *x509.CertPool
|
|
KnownIssuers map[string]bool
|
|
opts options
|
|
}
|
|
|
|
type options struct {
|
|
keyUsages []x509.ExtKeyUsage
|
|
}
|
|
|
|
var defaultOptions = options{
|
|
keyUsages: []x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageAny,
|
|
},
|
|
}
|
|
|
|
// An Option sets options such as allowed key usages, etc.
|
|
type Option func(*options)
|
|
|
|
// WithKeyUsages lets you set which Extended Key Usage values are acceptable. By
|
|
// default x509.ExtKeyUsageAny will be used.
|
|
func WithKeyUsages(usages ...x509.ExtKeyUsage) Option {
|
|
return func(o *options) {
|
|
o.keyUsages = usages
|
|
}
|
|
}
|
|
|
|
// NewBundler creates a new Bundler from the files passed in; these
|
|
// files should contain a list of valid root certificates and a list
|
|
// of valid intermediate certificates, respectively.
|
|
func NewBundler(caBundleFile, intBundleFile string, opt ...Option) (*Bundler, error) {
|
|
var caBundle, intBundle []byte
|
|
var err error
|
|
|
|
if caBundleFile != "" {
|
|
log.Debug("Loading CA bundle: ", caBundleFile)
|
|
caBundle, err = ioutil.ReadFile(caBundleFile)
|
|
if err != nil {
|
|
log.Errorf("root bundle failed to load: %v", err)
|
|
return nil, errors.Wrap(errors.RootError, errors.ReadFailed, err)
|
|
}
|
|
}
|
|
|
|
if intBundleFile != "" {
|
|
log.Debug("Loading Intermediate bundle: ", intBundleFile)
|
|
intBundle, err = ioutil.ReadFile(intBundleFile)
|
|
if err != nil {
|
|
log.Errorf("intermediate bundle failed to load: %v", err)
|
|
return nil, errors.Wrap(errors.IntermediatesError, errors.ReadFailed, err)
|
|
}
|
|
}
|
|
|
|
if IntermediateStash != "" {
|
|
if _, err = os.Stat(IntermediateStash); err != nil && os.IsNotExist(err) {
|
|
log.Infof("intermediate stash directory %s doesn't exist, creating", IntermediateStash)
|
|
err = os.MkdirAll(IntermediateStash, 0755)
|
|
if err != nil {
|
|
log.Errorf("failed to create intermediate stash directory %s: %v",
|
|
IntermediateStash, err)
|
|
return nil, err
|
|
}
|
|
log.Infof("intermediate stash directory %s created", IntermediateStash)
|
|
}
|
|
}
|
|
|
|
return NewBundlerFromPEM(caBundle, intBundle, opt...)
|
|
|
|
}
|
|
|
|
// NewBundlerFromPEM creates a new Bundler from PEM-encoded root certificates and
|
|
// intermediate certificates.
|
|
// If caBundlePEM is nil, the resulting Bundler can only do "Force" bundle.
|
|
func NewBundlerFromPEM(caBundlePEM, intBundlePEM []byte, opt ...Option) (*Bundler, error) {
|
|
opts := defaultOptions
|
|
for _, o := range opt {
|
|
o(&opts)
|
|
}
|
|
|
|
log.Debug("parsing root certificates from PEM")
|
|
roots, err := helpers.ParseCertificatesPEM(caBundlePEM)
|
|
if err != nil {
|
|
log.Errorf("failed to parse root bundle: %v", err)
|
|
return nil, errors.New(errors.RootError, errors.ParseFailed)
|
|
}
|
|
|
|
log.Debug("parse intermediate certificates from PEM")
|
|
intermediates, err := helpers.ParseCertificatesPEM(intBundlePEM)
|
|
if err != nil {
|
|
log.Errorf("failed to parse intermediate bundle: %v", err)
|
|
return nil, errors.New(errors.IntermediatesError, errors.ParseFailed)
|
|
}
|
|
|
|
b := &Bundler{
|
|
KnownIssuers: map[string]bool{},
|
|
IntermediatePool: x509.NewCertPool(),
|
|
opts: opts,
|
|
}
|
|
|
|
log.Debug("building certificate pools")
|
|
|
|
// RootPool will be nil if caBundlePEM is nil, also
|
|
// that translates to caBundleFile is "".
|
|
// Systems root store will be used.
|
|
if caBundlePEM != nil {
|
|
b.RootPool = x509.NewCertPool()
|
|
}
|
|
|
|
for _, c := range roots {
|
|
b.RootPool.AddCert(c)
|
|
b.KnownIssuers[string(c.Signature)] = true
|
|
}
|
|
|
|
for _, c := range intermediates {
|
|
b.IntermediatePool.AddCert(c)
|
|
b.KnownIssuers[string(c.Signature)] = true
|
|
}
|
|
|
|
log.Debug("bundler set up")
|
|
return b, nil
|
|
}
|
|
|
|
// VerifyOptions generates an x509 VerifyOptions structure that can be
|
|
// used for verifying certificates.
|
|
func (b *Bundler) VerifyOptions() x509.VerifyOptions {
|
|
return x509.VerifyOptions{
|
|
Roots: b.RootPool,
|
|
Intermediates: b.IntermediatePool,
|
|
KeyUsages: b.opts.keyUsages,
|
|
}
|
|
}
|
|
|
|
// BundleFromFile takes a set of files containing the PEM-encoded leaf certificate
|
|
// (optionally along with some intermediate certs), the PEM-encoded private key
|
|
// and returns the bundle built from that key and the certificate(s).
|
|
func (b *Bundler) BundleFromFile(bundleFile, keyFile string, flavor BundleFlavor, password string) (*Bundle, error) {
|
|
log.Debug("Loading Certificate: ", bundleFile)
|
|
certsRaw, err := ioutil.ReadFile(bundleFile)
|
|
if err != nil {
|
|
return nil, errors.Wrap(errors.CertificateError, errors.ReadFailed, err)
|
|
}
|
|
|
|
var keyPEM []byte
|
|
// Load private key PEM only if a file is given
|
|
if keyFile != "" {
|
|
log.Debug("Loading private key: ", keyFile)
|
|
keyPEM, err = ioutil.ReadFile(keyFile)
|
|
if err != nil {
|
|
log.Debugf("failed to read private key: ", err)
|
|
return nil, errors.Wrap(errors.PrivateKeyError, errors.ReadFailed, err)
|
|
}
|
|
if len(keyPEM) == 0 {
|
|
log.Debug("key is empty")
|
|
return nil, errors.Wrap(errors.PrivateKeyError, errors.DecodeFailed, err)
|
|
}
|
|
}
|
|
|
|
return b.BundleFromPEMorDER(certsRaw, keyPEM, flavor, password)
|
|
}
|
|
|
|
// BundleFromPEMorDER builds a certificate bundle from the set of byte
|
|
// slices containing the PEM or DER-encoded certificate(s), private key.
|
|
func (b *Bundler) BundleFromPEMorDER(certsRaw, keyPEM []byte, flavor BundleFlavor, password string) (*Bundle, error) {
|
|
log.Debug("bundling from PEM files")
|
|
var key crypto.Signer
|
|
var err error
|
|
if len(keyPEM) != 0 {
|
|
key, err = helpers.ParsePrivateKeyPEM(keyPEM)
|
|
if err != nil {
|
|
log.Debugf("failed to parse private key: %v", err)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
certs, err := helpers.ParseCertificatesPEM(certsRaw)
|
|
if err != nil {
|
|
// If PEM doesn't work try DER
|
|
var keyDER crypto.Signer
|
|
var errDER error
|
|
certs, keyDER, errDER = helpers.ParseCertificatesDER(certsRaw, password)
|
|
// Only use DER key if no key read from file
|
|
if key == nil && keyDER != nil {
|
|
key = keyDER
|
|
}
|
|
if errDER != nil {
|
|
log.Debugf("failed to parse certificates: %v", err)
|
|
// If neither parser works pass along PEM error
|
|
return nil, err
|
|
}
|
|
|
|
}
|
|
if len(certs) == 0 {
|
|
log.Debugf("no certificates found")
|
|
return nil, errors.New(errors.CertificateError, errors.DecodeFailed)
|
|
}
|
|
|
|
log.Debugf("bundle ready")
|
|
return b.Bundle(certs, key, flavor)
|
|
}
|
|
|
|
// BundleFromRemote fetches the certificate served by the server at
|
|
// serverName (or ip, if the ip argument is not the empty string). It
|
|
// is expected that the method will be able to make a connection at
|
|
// port 443. The certificate used by the server in this connection is
|
|
// used to build the bundle, which will necessarily be keyless.
|
|
func (b *Bundler) BundleFromRemote(serverName, ip string, flavor BundleFlavor) (*Bundle, error) {
|
|
config := &tls.Config{
|
|
RootCAs: b.RootPool,
|
|
ServerName: serverName,
|
|
}
|
|
|
|
// Dial by IP if present
|
|
var dialName string
|
|
if ip != "" {
|
|
dialName = ip + ":443"
|
|
} else {
|
|
dialName = serverName + ":443"
|
|
}
|
|
|
|
log.Debugf("bundling from remote %s", dialName)
|
|
|
|
dialer := &net.Dialer{Timeout: time.Duration(5) * time.Second}
|
|
conn, err := tls.DialWithDialer(dialer, "tcp", dialName, config)
|
|
var dialError string
|
|
// If there's an error in tls.Dial, try again with
|
|
// InsecureSkipVerify to fetch the remote bundle to (re-)bundle
|
|
// with. If the bundle is indeed not usable (expired, mismatched
|
|
// hostnames, etc.), report the error. Otherwise, create a
|
|
// working bundle and insert the tls error in the bundle.Status.
|
|
if err != nil {
|
|
log.Debugf("dial failed: %v", err)
|
|
// record the error msg
|
|
dialError = fmt.Sprintf("Failed rigid TLS handshake with %s: %v", dialName, err)
|
|
// dial again with InsecureSkipVerify
|
|
log.Debugf("try again with InsecureSkipVerify.")
|
|
config.InsecureSkipVerify = true
|
|
conn, err = tls.DialWithDialer(dialer, "tcp", dialName, config)
|
|
if err != nil {
|
|
log.Debugf("dial with InsecureSkipVerify failed: %v", err)
|
|
return nil, errors.Wrap(errors.DialError, errors.Unknown, err)
|
|
}
|
|
}
|
|
|
|
connState := conn.ConnectionState()
|
|
|
|
certs := connState.PeerCertificates
|
|
|
|
err = conn.VerifyHostname(serverName)
|
|
if err != nil {
|
|
log.Debugf("failed to verify hostname: %v", err)
|
|
return nil, errors.Wrap(errors.CertificateError, errors.VerifyFailed, err)
|
|
}
|
|
|
|
// Bundle with remote certs. Inject the initial dial error, if any, to the status reporting.
|
|
bundle, err := b.Bundle(certs, nil, flavor)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if dialError != "" {
|
|
bundle.Status.Messages = append(bundle.Status.Messages, dialError)
|
|
}
|
|
return bundle, err
|
|
}
|
|
|
|
type fetchedIntermediate struct {
|
|
Cert *x509.Certificate
|
|
Name string
|
|
}
|
|
|
|
// fetchRemoteCertificate retrieves a single URL pointing to a certificate
|
|
// and attempts to first parse it as a DER-encoded certificate; if
|
|
// this fails, it attempts to decode it as a PEM-encoded certificate.
|
|
func fetchRemoteCertificate(certURL string) (fi *fetchedIntermediate, err error) {
|
|
log.Debugf("fetching remote certificate: %s", certURL)
|
|
var resp *http.Response
|
|
resp, err = http.Get(certURL)
|
|
if err != nil {
|
|
log.Debugf("failed HTTP get: %v", err)
|
|
return
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
var certData []byte
|
|
certData, err = ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
log.Debugf("failed to read response body: %v", err)
|
|
return
|
|
}
|
|
|
|
log.Debugf("attempting to parse certificate as DER")
|
|
crt, err := x509.ParseCertificate(certData)
|
|
if err != nil {
|
|
log.Debugf("attempting to parse certificate as PEM")
|
|
crt, err = helpers.ParseCertificatePEM(certData)
|
|
if err != nil {
|
|
log.Debugf("failed to parse certificate: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
log.Debugf("certificate fetch succeeds")
|
|
fi = &fetchedIntermediate{Cert: crt, Name: constructCertFileName(crt)}
|
|
return
|
|
}
|
|
|
|
func reverse(certs []*x509.Certificate) []*x509.Certificate {
|
|
n := len(certs)
|
|
if n == 0 {
|
|
return certs
|
|
}
|
|
rcerts := []*x509.Certificate{}
|
|
for i := n - 1; i >= 0; i-- {
|
|
rcerts = append(rcerts, certs[i])
|
|
}
|
|
return rcerts
|
|
}
|
|
|
|
// Check if the certs form a partial cert chain: every cert verifies
|
|
// the signature of the one in front of it.
|
|
func partialVerify(certs []*x509.Certificate) bool {
|
|
n := len(certs)
|
|
if n == 0 {
|
|
return false
|
|
}
|
|
for i := 0; i < n-1; i++ {
|
|
if certs[i].CheckSignatureFrom(certs[i+1]) != nil {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isSelfSigned(cert *x509.Certificate) bool {
|
|
return cert.CheckSignatureFrom(cert) == nil
|
|
}
|
|
|
|
func isChainRootNode(cert *x509.Certificate) bool {
|
|
if isSelfSigned(cert) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (b *Bundler) verifyChain(chain []*fetchedIntermediate) bool {
|
|
// This process will verify if the root of the (partial) chain is in our root pool,
|
|
// and will fail otherwise.
|
|
log.Debugf("verifying chain")
|
|
for vchain := chain[:]; len(vchain) > 0; vchain = vchain[1:] {
|
|
cert := vchain[0]
|
|
// If this is a certificate in one of the pools, skip it.
|
|
if b.KnownIssuers[string(cert.Cert.Signature)] {
|
|
log.Debugf("certificate is known")
|
|
continue
|
|
}
|
|
|
|
_, err := cert.Cert.Verify(b.VerifyOptions())
|
|
if err != nil {
|
|
log.Debugf("certificate failed verification: %v", err)
|
|
return false
|
|
} else if len(chain) == len(vchain) && isChainRootNode(cert.Cert) {
|
|
// The first certificate in the chain is a root; it shouldn't be stored.
|
|
log.Debug("looking at root certificate, will not store")
|
|
continue
|
|
}
|
|
|
|
// leaf cert has an empty name, don't store leaf cert.
|
|
if cert.Name == "" {
|
|
continue
|
|
}
|
|
|
|
log.Debug("add certificate to intermediate pool:", cert.Name)
|
|
b.IntermediatePool.AddCert(cert.Cert)
|
|
b.KnownIssuers[string(cert.Cert.Signature)] = true
|
|
|
|
if IntermediateStash != "" {
|
|
fileName := filepath.Join(IntermediateStash, cert.Name)
|
|
|
|
var block = pem.Block{Type: "CERTIFICATE", Bytes: cert.Cert.Raw}
|
|
|
|
log.Debugf("write intermediate to stash directory: %s", fileName)
|
|
// If the write fails, verification should not fail.
|
|
err = ioutil.WriteFile(fileName, pem.EncodeToMemory(&block), 0644)
|
|
if err != nil {
|
|
log.Errorf("failed to write new intermediate: %v", err)
|
|
} else {
|
|
log.Info("stashed new intermediate ", cert.Name)
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// constructCertFileName returns a uniquely identifying file name for a certificate
|
|
func constructCertFileName(cert *x509.Certificate) string {
|
|
// construct the filename as the CN with no period and space
|
|
name := strings.Replace(cert.Subject.CommonName, ".", "", -1)
|
|
name = strings.Replace(name, " ", "", -1)
|
|
|
|
// add SKI and serial number as extra identifier
|
|
name += fmt.Sprintf("_%x", cert.SubjectKeyId)
|
|
name += fmt.Sprintf("_%x", cert.SerialNumber.Bytes())
|
|
|
|
name += ".crt"
|
|
return name
|
|
}
|
|
|
|
// fetchIntermediates goes through each of the URLs in the AIA "Issuing
|
|
// CA" extensions and fetches those certificates. If those
|
|
// certificates are not present in either the root pool or
|
|
// intermediate pool, the certificate is saved to file and added to
|
|
// the list of intermediates to be used for verification. This will
|
|
// not add any new certificates to the root pool; if the ultimate
|
|
// issuer is not trusted, fetching the certicate here will not change
|
|
// that.
|
|
func (b *Bundler) fetchIntermediates(certs []*x509.Certificate) (err error) {
|
|
if IntermediateStash != "" {
|
|
log.Debugf("searching intermediates")
|
|
if _, err := os.Stat(IntermediateStash); err != nil && os.IsNotExist(err) {
|
|
log.Infof("intermediate stash directory %s doesn't exist, creating", IntermediateStash)
|
|
err = os.MkdirAll(IntermediateStash, 0755)
|
|
if err != nil {
|
|
log.Errorf("failed to create intermediate stash directory %s: %v", IntermediateStash, err)
|
|
return err
|
|
}
|
|
log.Infof("intermediate stash directory %s created", IntermediateStash)
|
|
}
|
|
}
|
|
// stores URLs and certificate signatures that have been seen
|
|
seen := map[string]bool{}
|
|
var foundChains int
|
|
|
|
// Construct a verify chain as a reversed partial bundle,
|
|
// such that the certs are ordered by promxity to the root CAs.
|
|
var chain []*fetchedIntermediate
|
|
for i, cert := range certs {
|
|
var name string
|
|
|
|
// Only construct filenames for non-leaf intermediate certs
|
|
// so they will be saved to disk if necessary.
|
|
// Leaf cert gets a empty name and will be skipped.
|
|
if i > 0 {
|
|
name = constructCertFileName(cert)
|
|
}
|
|
|
|
chain = append([]*fetchedIntermediate{{cert, name}}, chain...)
|
|
seen[string(cert.Signature)] = true
|
|
}
|
|
|
|
// Verify the chain and store valid intermediates in the chain.
|
|
// If it doesn't verify, fetch the intermediates and extend the chain
|
|
// in a DFS manner and verify each time we hit a root.
|
|
for {
|
|
if len(chain) == 0 {
|
|
log.Debugf("search complete")
|
|
if foundChains == 0 {
|
|
return x509.UnknownAuthorityError{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
current := chain[0]
|
|
var advanced bool
|
|
if b.verifyChain(chain) {
|
|
foundChains++
|
|
}
|
|
log.Debugf("walk AIA issuers")
|
|
for _, url := range current.Cert.IssuingCertificateURL {
|
|
if seen[url] {
|
|
log.Debugf("url %s has been seen", url)
|
|
continue
|
|
}
|
|
crt, err := fetchRemoteCertificate(url)
|
|
if err != nil {
|
|
continue
|
|
} else if seen[string(crt.Cert.Signature)] {
|
|
log.Debugf("fetched certificate is known")
|
|
continue
|
|
}
|
|
seen[url] = true
|
|
seen[string(crt.Cert.Signature)] = true
|
|
chain = append([]*fetchedIntermediate{crt}, chain...)
|
|
advanced = true
|
|
break
|
|
}
|
|
|
|
if !advanced {
|
|
log.Debugf("didn't advance, stepping back")
|
|
chain = chain[1:]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bundle takes an X509 certificate (already in the
|
|
// Certificate structure), a private key as crypto.Signer in one of the appropriate
|
|
// formats (i.e. *rsa.PrivateKey or *ecdsa.PrivateKey, or even a opaque key), using them to
|
|
// build a certificate bundle.
|
|
func (b *Bundler) Bundle(certs []*x509.Certificate, key crypto.Signer, flavor BundleFlavor) (*Bundle, error) {
|
|
log.Infof("bundling certificate for %+v", certs[0].Subject)
|
|
if len(certs) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Detect reverse ordering of the cert chain.
|
|
if len(certs) > 1 && !partialVerify(certs) {
|
|
rcerts := reverse(certs)
|
|
if partialVerify(rcerts) {
|
|
certs = rcerts
|
|
}
|
|
}
|
|
|
|
var ok bool
|
|
cert := certs[0]
|
|
if key != nil {
|
|
switch {
|
|
case cert.PublicKeyAlgorithm == x509.RSA:
|
|
|
|
var rsaPublicKey *rsa.PublicKey
|
|
if rsaPublicKey, ok = key.Public().(*rsa.PublicKey); !ok {
|
|
return nil, errors.New(errors.PrivateKeyError, errors.KeyMismatch)
|
|
}
|
|
if cert.PublicKey.(*rsa.PublicKey).N.Cmp(rsaPublicKey.N) != 0 {
|
|
return nil, errors.New(errors.PrivateKeyError, errors.KeyMismatch)
|
|
}
|
|
case cert.PublicKeyAlgorithm == x509.ECDSA:
|
|
var ecdsaPublicKey *ecdsa.PublicKey
|
|
if ecdsaPublicKey, ok = key.Public().(*ecdsa.PublicKey); !ok {
|
|
return nil, errors.New(errors.PrivateKeyError, errors.KeyMismatch)
|
|
}
|
|
if cert.PublicKey.(*ecdsa.PublicKey).X.Cmp(ecdsaPublicKey.X) != 0 {
|
|
return nil, errors.New(errors.PrivateKeyError, errors.KeyMismatch)
|
|
}
|
|
default:
|
|
return nil, errors.New(errors.PrivateKeyError, errors.NotRSAOrECC)
|
|
}
|
|
} else {
|
|
switch {
|
|
case cert.PublicKeyAlgorithm == x509.RSA:
|
|
case cert.PublicKeyAlgorithm == x509.ECDSA:
|
|
default:
|
|
return nil, errors.New(errors.PrivateKeyError, errors.NotRSAOrECC)
|
|
}
|
|
}
|
|
|
|
bundle := new(Bundle)
|
|
bundle.Cert = cert
|
|
bundle.Key = key
|
|
bundle.Issuer = &cert.Issuer
|
|
bundle.Subject = &cert.Subject
|
|
|
|
bundle.buildHostnames()
|
|
|
|
if flavor == Force {
|
|
// force bundle checks the certificates
|
|
// forms a verification chain.
|
|
if !partialVerify(certs) {
|
|
return nil,
|
|
errors.Wrap(errors.CertificateError, errors.VerifyFailed,
|
|
goerr.New("Unable to verify the certificate chain"))
|
|
}
|
|
bundle.Chain = certs
|
|
} else {
|
|
// disallow self-signed cert
|
|
if cert.CheckSignatureFrom(cert) == nil {
|
|
return nil, errors.New(errors.CertificateError, errors.SelfSigned)
|
|
}
|
|
|
|
chains, err := cert.Verify(b.VerifyOptions())
|
|
if err != nil {
|
|
log.Debugf("verification failed: %v", err)
|
|
// If the error was an unknown authority, try to fetch
|
|
// the intermediate specified in the AIA and add it to
|
|
// the intermediates bundle.
|
|
if _, ok := err.(x509.UnknownAuthorityError); !ok {
|
|
return nil, errors.Wrap(errors.CertificateError, errors.VerifyFailed, err)
|
|
}
|
|
|
|
log.Debugf("searching for intermediates via AIA issuer")
|
|
searchErr := b.fetchIntermediates(certs)
|
|
if searchErr != nil {
|
|
log.Debugf("search failed: %v", searchErr)
|
|
return nil, errors.Wrap(errors.CertificateError, errors.VerifyFailed, err)
|
|
}
|
|
|
|
log.Debugf("verifying new chain")
|
|
chains, err = cert.Verify(b.VerifyOptions())
|
|
if err != nil {
|
|
log.Debugf("failed to verify chain: %v", err)
|
|
return nil, errors.Wrap(errors.CertificateError, errors.VerifyFailed, err)
|
|
}
|
|
log.Debugf("verify ok")
|
|
}
|
|
var matchingChains [][]*x509.Certificate
|
|
switch flavor {
|
|
case Optimal:
|
|
matchingChains = optimalChains(chains)
|
|
case Ubiquitous:
|
|
if len(ubiquity.Platforms) == 0 {
|
|
log.Warning("No metadata, Ubiquitous falls back to Optimal.")
|
|
}
|
|
matchingChains = ubiquitousChains(chains)
|
|
default:
|
|
matchingChains = ubiquitousChains(chains)
|
|
}
|
|
|
|
bundle.Chain = matchingChains[0]
|
|
}
|
|
|
|
statusCode := int(errors.Success)
|
|
var messages []string
|
|
// Check if bundle is expiring.
|
|
expiringCerts := checkExpiringCerts(bundle.Chain)
|
|
if len(expiringCerts) > 0 {
|
|
statusCode |= errors.BundleExpiringBit
|
|
messages = append(messages, expirationWarning(expiringCerts))
|
|
}
|
|
// Check if bundle contains SHA2 certs.
|
|
if ubiquity.ChainHashUbiquity(bundle.Chain) <= ubiquity.SHA2Ubiquity {
|
|
statusCode |= errors.BundleNotUbiquitousBit
|
|
messages = append(messages, sha2Warning)
|
|
}
|
|
// Check if bundle contains ECDSA signatures.
|
|
if ubiquity.ChainKeyAlgoUbiquity(bundle.Chain) <= ubiquity.ECDSA256Ubiquity {
|
|
statusCode |= errors.BundleNotUbiquitousBit
|
|
messages = append(messages, ecdsaWarning)
|
|
}
|
|
|
|
// when forcing a bundle, bundle ubiquity doesn't matter
|
|
// also we don't retrieve the anchoring root of the bundle
|
|
var untrusted []string
|
|
if flavor != Force {
|
|
// Add root store presence info
|
|
root := bundle.Chain[len(bundle.Chain)-1]
|
|
bundle.Root = root
|
|
log.Infof("the anchoring root is %v", root.Subject)
|
|
// Check if there is any platform that doesn't trust the chain.
|
|
// Also, an warning will be generated if ubiquity.Platforms is nil,
|
|
untrusted = ubiquity.UntrustedPlatforms(root)
|
|
untrustedMsg := untrustedPlatformsWarning(untrusted)
|
|
if len(untrustedMsg) > 0 {
|
|
log.Debug("Populate untrusted platform warning.")
|
|
statusCode |= errors.BundleNotUbiquitousBit
|
|
messages = append(messages, untrustedMsg)
|
|
}
|
|
}
|
|
|
|
// Check if there is any platform that rejects the chain because of SHA1 deprecation.
|
|
sha1Msgs := ubiquity.SHA1DeprecationMessages(bundle.Chain)
|
|
if len(sha1Msgs) > 0 {
|
|
log.Debug("Populate SHA1 deprecation warning.")
|
|
statusCode |= errors.BundleNotUbiquitousBit
|
|
messages = append(messages, sha1Msgs...)
|
|
}
|
|
|
|
bundle.Status = &BundleStatus{ExpiringSKIs: getSKIs(bundle.Chain, expiringCerts), Code: statusCode, Messages: messages, Untrusted: untrusted}
|
|
|
|
// attempt to not to include the root certificate for optimization
|
|
if flavor != Force {
|
|
// Include at least one intermediate if the leaf has enabled OCSP and is not CA.
|
|
if bundle.Cert.OCSPServer != nil && !bundle.Cert.IsCA && len(bundle.Chain) <= 2 {
|
|
// No op. Return one intermediate if there is one.
|
|
} else {
|
|
// do not include the root.
|
|
bundle.Chain = bundle.Chain[:len(bundle.Chain)-1]
|
|
}
|
|
}
|
|
|
|
bundle.Status.IsRebundled = diff(bundle.Chain, certs)
|
|
bundle.Expires = helpers.ExpiryTime(bundle.Chain)
|
|
bundle.LeafExpires = bundle.Chain[0].NotAfter
|
|
|
|
log.Debugf("bundle complete")
|
|
return bundle, nil
|
|
}
|
|
|
|
// checkExpiringCerts returns indices of certs that are expiring within 30 days.
|
|
func checkExpiringCerts(chain []*x509.Certificate) (expiringIntermediates []int) {
|
|
now := time.Now()
|
|
for i, cert := range chain {
|
|
if cert.NotAfter.Sub(now).Hours() < 720 {
|
|
expiringIntermediates = append(expiringIntermediates, i)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// getSKIs returns a list of cert subject key id in the bundle chain with matched indices.
|
|
func getSKIs(chain []*x509.Certificate, indices []int) (skis []string) {
|
|
for _, index := range indices {
|
|
ski := fmt.Sprintf("%X", chain[index].SubjectKeyId)
|
|
skis = append(skis, ski)
|
|
}
|
|
return
|
|
}
|
|
|
|
// expirationWarning generates a warning message with expiring certs.
|
|
func expirationWarning(expiringIntermediates []int) (ret string) {
|
|
if len(expiringIntermediates) == 0 {
|
|
return
|
|
}
|
|
|
|
ret = expiringWarningStub
|
|
if len(expiringIntermediates) > 1 {
|
|
ret = ret + "The expiring certs are"
|
|
} else {
|
|
ret = ret + "The expiring cert is"
|
|
}
|
|
for _, index := range expiringIntermediates {
|
|
ret = ret + " #" + strconv.Itoa(index+1)
|
|
}
|
|
ret = ret + " in the chain."
|
|
return
|
|
}
|
|
|
|
// untrustedPlatformsWarning generates a warning message with untrusted platform names.
|
|
func untrustedPlatformsWarning(platforms []string) string {
|
|
if len(ubiquity.Platforms) == 0 {
|
|
return ubiquityWarning
|
|
}
|
|
|
|
if len(platforms) == 0 {
|
|
return ""
|
|
}
|
|
|
|
msg := untrustedWarningStub
|
|
for i, platform := range platforms {
|
|
if i > 0 {
|
|
msg += ","
|
|
}
|
|
msg += " " + platform
|
|
}
|
|
msg += "."
|
|
return msg
|
|
}
|
|
|
|
// Optimal chains are the shortest chains, with newest intermediates and most advanced crypto suite being the tie breaker.
|
|
func optimalChains(chains [][]*x509.Certificate) [][]*x509.Certificate {
|
|
// Find shortest chains
|
|
chains = ubiquity.Filter(chains, ubiquity.CompareChainLength)
|
|
// Find the chains with longest expiry.
|
|
chains = ubiquity.Filter(chains, ubiquity.CompareChainExpiry)
|
|
// Find the chains with more advanced crypto suite
|
|
chains = ubiquity.Filter(chains, ubiquity.CompareChainCryptoSuite)
|
|
|
|
return chains
|
|
}
|
|
|
|
// Ubiquitous chains are the chains with highest platform coverage and break ties with the optimal strategy.
|
|
func ubiquitousChains(chains [][]*x509.Certificate) [][]*x509.Certificate {
|
|
// Filter out chains with highest cross platform ubiquity.
|
|
chains = ubiquity.Filter(chains, ubiquity.ComparePlatformUbiquity)
|
|
// Prefer that all intermediates are SHA-2 certs if the leaf is a SHA-2 cert, in order to improve ubiquity.
|
|
chains = ubiquity.Filter(chains, ubiquity.CompareSHA2Homogeneity)
|
|
// Filter shortest chains
|
|
chains = ubiquity.Filter(chains, ubiquity.CompareChainLength)
|
|
// Filter chains with highest signature hash ubiquity.
|
|
chains = ubiquity.Filter(chains, ubiquity.CompareChainHashUbiquity)
|
|
// Filter chains with highest keyAlgo ubiquity.
|
|
chains = ubiquity.Filter(chains, ubiquity.CompareChainKeyAlgoUbiquity)
|
|
// Filter chains with intermediates that last longer.
|
|
chains = ubiquity.Filter(chains, ubiquity.CompareExpiryUbiquity)
|
|
// Use the optimal strategy as final tie breaker.
|
|
return optimalChains(chains)
|
|
}
|
|
|
|
// diff checkes if two input cert chains are not identical
|
|
func diff(chain1, chain2 []*x509.Certificate) bool {
|
|
// Check if bundled one is different from the input.
|
|
diff := false
|
|
if len(chain1) != len(chain2) {
|
|
diff = true
|
|
} else {
|
|
for i := 0; i < len(chain1); i++ {
|
|
cert1 := chain1[i]
|
|
cert2 := chain2[i]
|
|
// Use signature to differentiate.
|
|
if !bytes.Equal(cert1.Signature, cert2.Signature) {
|
|
diff = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return diff
|
|
}
|