mirror of https://github.com/docker/docs.git
Merge pull request #83 from docker/cooking-the-tofu-to-support-rotations
Cooking the tofu to support rotations
This commit is contained in:
commit
9d31d343f3
|
@ -63,7 +63,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/endophage/gotuf",
|
"ImportPath": "github.com/endophage/gotuf",
|
||||||
"Rev": "a8a23ab6e67bd0e9fbaf563aabd9e6ee7ea344d2"
|
"Rev": "9640c9b3f2ff0ba75baf7d1a57632e16cb78d5e6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/go-sql-driver/mysql",
|
"ImportPath": "github.com/go-sql-driver/mysql",
|
||||||
|
|
|
@ -52,7 +52,13 @@ func VerifyRoot(s *data.Signed, minVersion int, keys map[string]data.PublicKey)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := verifier.Verify(keys[sig.KeyID], sig.Signature, msg); err != nil {
|
key, ok := keys[sig.KeyID]
|
||||||
|
if !ok {
|
||||||
|
logrus.Debugf("continuing b/c signing key isn't present in keys: %s\n", sig.KeyID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := verifier.Verify(key, sig.Signature, msg); err != nil {
|
||||||
logrus.Debugf("continuing b/c signature was invalid\n")
|
logrus.Debugf("continuing b/c signature was invalid\n")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package client
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -125,7 +124,7 @@ func validateRootSuccessfully(t *testing.T, rootType data.KeyAlgorithm) {
|
||||||
assert.Len(t, repo.KeyStoreManager.TrustedCertificateStore().GetCertificates(), 1)
|
assert.Len(t, repo.KeyStoreManager.TrustedCertificateStore().GetCertificates(), 1)
|
||||||
|
|
||||||
//
|
//
|
||||||
// Test certificate mismatch logic. We remove all certs, and a different cert to the
|
// Test certificate mismatch logic. We remove all certs, add a different cert to the
|
||||||
// same CN, and expect ValidateRoot to fail
|
// same CN, and expect ValidateRoot to fail
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -142,7 +141,7 @@ func validateRootSuccessfully(t *testing.T, rootType data.KeyAlgorithm) {
|
||||||
// in the store for the dnsName docker.com/notary, so TOFUS doesn't apply
|
// in the store for the dnsName docker.com/notary, so TOFUS doesn't apply
|
||||||
_, err = repo.ListTargets()
|
_, err = repo.ListTargets()
|
||||||
if assert.Error(t, err, "An error was expected") {
|
if assert.Error(t, err, "An error was expected") {
|
||||||
assert.Equal(t, err, keystoremanager.ErrValidationFail)
|
assert.Equal(t, err, &keystoremanager.ErrValidationFail{Reason: "failed to validate integrity of roots"})
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -187,7 +186,7 @@ func validateRootSuccessfully(t *testing.T, rootType data.KeyAlgorithm) {
|
||||||
//
|
//
|
||||||
err = repo.KeyStoreManager.ValidateRoot(&testSignedRoot, "secure.example.com")
|
err = repo.KeyStoreManager.ValidateRoot(&testSignedRoot, "secure.example.com")
|
||||||
if assert.Error(t, err, "An error was expected") {
|
if assert.Error(t, err, "An error was expected") {
|
||||||
assert.Equal(t, err, errors.New("tuf: valid signatures did not meet threshold"))
|
assert.Equal(t, err, &keystoremanager.ErrValidationFail{Reason: "failed to validate integrity of roots"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,7 +223,7 @@ func TestValidateRootWithInvalidData(t *testing.T) {
|
||||||
//
|
//
|
||||||
err = keyStoreManager.ValidateRoot(&testSignedRoot, "diogomonica.com/notary")
|
err = keyStoreManager.ValidateRoot(&testSignedRoot, "diogomonica.com/notary")
|
||||||
if assert.Error(t, err, "An error was expected") {
|
if assert.Error(t, err, "An error was expected") {
|
||||||
assert.Equal(t, err, keystoremanager.ErrValidationFail)
|
assert.Equal(t, err, &keystoremanager.ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"})
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -252,7 +251,7 @@ func TestValidateRootWithInvalidData(t *testing.T) {
|
||||||
|
|
||||||
err = keyStoreManager.ValidateRoot(&testSignedRoot, "docker.com/notary")
|
err = keyStoreManager.ValidateRoot(&testSignedRoot, "docker.com/notary")
|
||||||
if assert.Error(t, err, "An error was expected") {
|
if assert.Error(t, err, "An error was expected") {
|
||||||
assert.Equal(t, err, keystoremanager.ErrValidationFail)
|
assert.Equal(t, err, &keystoremanager.ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"})
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -268,6 +267,6 @@ func TestValidateRootWithInvalidData(t *testing.T) {
|
||||||
|
|
||||||
err = keyStoreManager.ValidateRoot(&testSignedRoot, "docker.com/notary")
|
err = keyStoreManager.ValidateRoot(&testSignedRoot, "docker.com/notary")
|
||||||
if assert.Error(t, err, "An error was expected") {
|
if assert.Error(t, err, "An error was expected") {
|
||||||
assert.Equal(t, err, keystoremanager.ErrValidationFail)
|
assert.Equal(t, err, &keystoremanager.ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,14 +34,29 @@ const (
|
||||||
rsaRootKeySize = 4096 // Used for new root keys
|
rsaRootKeySize = 4096 // Used for new root keys
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// ErrValidationFail is returned when there is no valid trusted certificates
|
||||||
// ErrValidationFail is returned when there is no trusted certificate in any of the
|
// being served inside of the roots.json
|
||||||
// root keys available in the roots.json
|
type ErrValidationFail struct {
|
||||||
ErrValidationFail = errors.New("could not validate the path to a trusted root")
|
Reason string
|
||||||
// ErrRootRotationFail is returned when we fail to do a full root key rotation
|
}
|
||||||
// by either failing to add the new root certificate, or delete the old ones
|
|
||||||
ErrRootRotationFail = errors.New("could not rotate trust to a new trusted root")
|
// ErrValidationFail is returned when there is no valid trusted certificates
|
||||||
)
|
// being served inside of the roots.json
|
||||||
|
func (err ErrValidationFail) Error() string {
|
||||||
|
return fmt.Sprintf("could not validate the path to a trusted root: %s", err.Reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrRootRotationFail is returned when we fail to do a full root key rotation
|
||||||
|
// by either failing to add the new root certificate, or delete the old ones
|
||||||
|
type ErrRootRotationFail struct {
|
||||||
|
Reason string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrRootRotationFail is returned when we fail to do a full root key rotation
|
||||||
|
// by either failing to add the new root certificate, or delete the old ones
|
||||||
|
func (err ErrRootRotationFail) Error() string {
|
||||||
|
return fmt.Sprintf("could not rotate trust to a new trusted root: %s", err.Reason)
|
||||||
|
}
|
||||||
|
|
||||||
// NewKeyStoreManager returns an initialized KeyStoreManager, or an error
|
// NewKeyStoreManager returns an initialized KeyStoreManager, or an error
|
||||||
// if it fails to create the KeyFileStores or load certificates
|
// if it fails to create the KeyFileStores or load certificates
|
||||||
|
@ -168,172 +183,241 @@ func (km *KeyStoreManager) GetRootCryptoService(rootKeyID, passphrase string) (*
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
ValidateRoot iterates over every root key included in the TUF data and
|
ValidateRoot receives a new root, validates its correctness and attempts to
|
||||||
attempts to validate the certificate by first checking for an exact match on
|
do root key rotation if needed.
|
||||||
the certificate store, and subsequently trying to find a valid chain on the
|
|
||||||
trustedCAStore.
|
|
||||||
|
|
||||||
Currently this method operates on a Trust On First Use (TOFU) model: if we
|
First we list the current trusted certificates we have for a particular GUN. If
|
||||||
|
that list is non-empty means that we've already seen this repository before, and
|
||||||
|
have a list of trusted certificates for it. In this case, we use this list of
|
||||||
|
certificates to attempt to validate this root file.
|
||||||
|
|
||||||
|
If the previous validation suceeds, or in the case where we found no trusted
|
||||||
|
certificates for this particular GUN, we check the integrity of the root by
|
||||||
|
making sure that it is validated by itself. This means that we will attempt to
|
||||||
|
validate the root data with the certificates that are included in the root keys
|
||||||
|
themselves.
|
||||||
|
|
||||||
|
If this last steps succeeds, we attempt to do root rotation, by ensuring that
|
||||||
|
we only trust the certificates that are present in the new root.
|
||||||
|
|
||||||
|
This mechanism of operation is essentially Trust On First Use (TOFU): if we
|
||||||
have never seen a certificate for a particular CN, we trust it. If later we see
|
have never seen a certificate for a particular CN, we trust it. If later we see
|
||||||
a different certificate for that certificate, we return an ErrValidationFailed error.
|
a different certificate for that certificate, we return an ErrValidationFailed error.
|
||||||
|
|
||||||
Note that since we only allow trust data to be downloaded over an HTTPS channel
|
Note that since we only allow trust data to be downloaded over an HTTPS channel
|
||||||
we are using the current web-of-trust to validate the first download of the certificate
|
we are using the current public PKI to validate the first download of the certificate
|
||||||
adding an extra layer of security over the normal (SSH style) trust model.
|
adding an extra layer of security over the normal (SSH style) trust model.
|
||||||
We shall call this: TOFUS.
|
We shall call this: TOFUS.
|
||||||
|
|
||||||
ValidateRoot also supports root key rotation, trusting a new certificate that has
|
|
||||||
been included in the roots.json, and removing trust in the old one.
|
|
||||||
*/
|
*/
|
||||||
func (km *KeyStoreManager) ValidateRoot(root *data.Signed, dnsName string) error {
|
func (km *KeyStoreManager) ValidateRoot(root *data.Signed, gun string) error {
|
||||||
logrus.Debugf("entered ValidateRoot with dns: %s", dnsName)
|
logrus.Debugf("entered ValidateRoot with dns: %s", gun)
|
||||||
rootSigned, err := data.RootFromSigned(root)
|
signedRoot, err := data.RootFromSigned(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// validKeys will store all the keys that were considered valid either by
|
// Retrieve all the leaf certificates in root for which the CN matches the GUN
|
||||||
// direct certificate match, or CA chain path
|
allValidCerts, err := validRootLeafCerts(signedRoot, gun)
|
||||||
validKeys := make(map[string]data.PublicKey)
|
if err != nil {
|
||||||
|
logrus.Debugf("error retrieving valid leaf certificates for: %s, %v", gun, err)
|
||||||
// allCerts will keep a list of all leafCerts that were found, and is used
|
return &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}
|
||||||
// to aid on root certificate rotation
|
|
||||||
allCerts := make(map[string]*x509.Certificate)
|
|
||||||
|
|
||||||
// Before we loop through all root keys available, make sure any exist
|
|
||||||
rootRoles, ok := rootSigned.Signed.Roles["root"]
|
|
||||||
if !ok {
|
|
||||||
return errors.New("no root roles found in tuf metadata")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("found the following root keys in roots.json: %v", rootRoles.KeyIDs)
|
// Retrieve all the trusted certificates that match this gun
|
||||||
|
certsForCN, err := km.trustedCertificateStore.GetCertificatesByCN(gun)
|
||||||
|
if err != nil {
|
||||||
|
// If the error that we get back is different than ErrNoCertificatesFound
|
||||||
|
// we couldn't check if there are any certificates with this CN already
|
||||||
|
// trusted. Let's take the conservative approach and return a failed validation
|
||||||
|
if _, ok := err.(*trustmanager.ErrNoCertificatesFound); !ok {
|
||||||
|
logrus.Debugf("error retrieving trusted certificates for: %s, %v", gun, err)
|
||||||
|
return &ErrValidationFail{Reason: "unable to retrieve trusted certificates"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have certificates that match this specific GUN, let's make sure to
|
||||||
|
// use them first to validate that this new root is valid.
|
||||||
|
if len(certsForCN) != 0 {
|
||||||
|
logrus.Debugf("found %d valid certificates for %s", len(certsForCN), gun)
|
||||||
|
err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(certsForCN))
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err)
|
||||||
|
return &ErrValidationFail{Reason: "failed to validate integrity of roots"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the integrity of the new root (does it have valid signatures)
|
||||||
|
err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(allValidCerts))
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err)
|
||||||
|
return &ErrValidationFail{Reason: "failed to validate integrity of roots"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getting here means A) we had trusted certificates and both the
|
||||||
|
// old and new validated this root; or B) we had no trusted certificates but
|
||||||
|
// the new set of certificates has integrity (self-signed)
|
||||||
|
logrus.Debugf("entering root certificate rotation for: %s", gun)
|
||||||
|
|
||||||
|
// Do root certificate rotation: we trust only the certs present in the new root
|
||||||
|
// First we add all the new certificates (even if they already exist)
|
||||||
|
for _, cert := range allValidCerts {
|
||||||
|
err := km.trustedCertificateStore.AddCert(cert)
|
||||||
|
if err != nil {
|
||||||
|
// If the error is already exists we don't fail the rotation
|
||||||
|
if _, ok := err.(*trustmanager.ErrCertExists); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logrus.Debugf("error adding new trusted certificate for: %s, %v", gun, err)
|
||||||
|
}
|
||||||
|
// Getting the CertID for debug only, don't care about the error
|
||||||
|
certID, _ := trustmanager.FingerprintCert(cert)
|
||||||
|
logrus.Debugf("adding trust certificate with certID %s for %s", certID, gun)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we delete old certificates that aren't present in the new root
|
||||||
|
for certID, cert := range certsToRemove(certsForCN, allValidCerts) {
|
||||||
|
logrus.Debugf("removing certificate with certID: %s", certID)
|
||||||
|
err = km.trustedCertificateStore.RemoveCert(cert)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Debugf("failed to remove trusted certificate with keyID: %s, %v", certID, err)
|
||||||
|
return &ErrRootRotationFail{Reason: "failed to rotate root keys"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Root validation succeeded for %s", gun)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validRootLeafCerts returns a list of non-exipired, non-sha1 certificates whoose
|
||||||
|
// Common-Names match the provided GUN
|
||||||
|
func validRootLeafCerts(root *data.SignedRoot, gun string) ([]*x509.Certificate, error) {
|
||||||
|
// Get a list of all of the leaf certificates present in root
|
||||||
|
allLeafCerts, _ := parseAllCerts(root)
|
||||||
|
var validLeafCerts []*x509.Certificate
|
||||||
|
|
||||||
|
// Go through every leaf certificate and check that the CN matches the gun
|
||||||
|
for _, cert := range allLeafCerts {
|
||||||
|
// Validate that this leaf certificate has a CN that matches the exact gun
|
||||||
|
if cert.Subject.CommonName != gun {
|
||||||
|
logrus.Debugf("error leaf certificate CN: %s doesn't match the given GUN: %s", cert.Subject.CommonName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Make sure the certificate is not expired
|
||||||
|
if time.Now().After(cert.NotAfter) {
|
||||||
|
logrus.Debugf("error leaf certificate is expired")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't allow root certificates that use SHA1
|
||||||
|
if cert.SignatureAlgorithm == x509.SHA1WithRSA ||
|
||||||
|
cert.SignatureAlgorithm == x509.DSAWithSHA1 ||
|
||||||
|
cert.SignatureAlgorithm == x509.ECDSAWithSHA1 {
|
||||||
|
|
||||||
|
logrus.Debugf("error certificate uses deprecated hashing algorithm (SHA1)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
validLeafCerts = append(validLeafCerts, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(validLeafCerts) < 1 {
|
||||||
|
return nil, errors.New("no valid leaf certificates found in any of the root keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
return validLeafCerts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAllCerts returns two maps, one with all of the leafCertificates and one
|
||||||
|
// with all the intermediate certificates found in signedRoot
|
||||||
|
func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, map[string][]*x509.Certificate) {
|
||||||
|
leafCerts := make(map[string]*x509.Certificate)
|
||||||
|
intCerts := make(map[string][]*x509.Certificate)
|
||||||
|
|
||||||
|
// Before we loop through all root keys available, make sure any exist
|
||||||
|
rootRoles, ok := signedRoot.Signed.Roles["root"]
|
||||||
|
if !ok {
|
||||||
|
logrus.Debugf("tried to parse certificates from invalid root signed data")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("found the following root keys: %v", rootRoles.KeyIDs)
|
||||||
// Iterate over every keyID for the root role inside of roots.json
|
// Iterate over every keyID for the root role inside of roots.json
|
||||||
for _, keyID := range rootRoles.KeyIDs {
|
for _, keyID := range rootRoles.KeyIDs {
|
||||||
|
// check that the key exists in the signed root keys map
|
||||||
|
key, ok := signedRoot.Signed.Keys[keyID]
|
||||||
|
if !ok {
|
||||||
|
logrus.Debugf("error while getting data for keyID: %s", keyID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Decode all the x509 certificates that were bundled with this
|
// Decode all the x509 certificates that were bundled with this
|
||||||
// Specific root key
|
// Specific root key
|
||||||
decodedCerts, err := trustmanager.LoadCertBundleFromPEM([]byte(rootSigned.Signed.Keys[keyID].Public()))
|
decodedCerts, err := trustmanager.LoadCertBundleFromPEM(key.Public())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Debugf("error while parsing root certificate with keyID: %s, %v", keyID, err)
|
logrus.Debugf("error while parsing root certificate with keyID: %s, %v", keyID, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all non-CA certificates in the decoded certificates
|
// Get all non-CA certificates in the decoded certificates
|
||||||
leafCerts := trustmanager.GetLeafCerts(decodedCerts)
|
leafCertList := trustmanager.GetLeafCerts(decodedCerts)
|
||||||
|
|
||||||
// If we got no leaf certificates or we got more than one, fail
|
// If we got no leaf certificates or we got more than one, fail
|
||||||
if len(leafCerts) != 1 {
|
if len(leafCertList) != 1 {
|
||||||
logrus.Debugf("wasn't able to find a leaf certificate in the chain of keyID: %s", keyID)
|
logrus.Debugf("invalid chain due to leaf certificate missing or too many leaf certificates for keyID: %s", keyID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the ID of the leaf certificate
|
// Get the ID of the leaf certificate
|
||||||
leafCert := leafCerts[0]
|
leafCert := leafCertList[0]
|
||||||
leafID, err := trustmanager.FingerprintCert(leafCert)
|
leafID, err := trustmanager.FingerprintCert(leafCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Debugf("error while fingerprinting root certificate with keyID: %s, %v", keyID, err)
|
logrus.Debugf("error while fingerprinting root certificate with keyID: %s, %v", keyID, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that this leaf certificate has a CN that matches the exact gun
|
// Store the leaf cert in the map
|
||||||
if leafCert.Subject.CommonName != dnsName {
|
leafCerts[leafID] = leafCert
|
||||||
logrus.Debugf("error leaf certificate CN: %s doesn't match the given dns name: %s", leafCert.Subject.CommonName, dnsName)
|
|
||||||
continue
|
// Get all the remainder certificates marked as a CA to be used as intermediates
|
||||||
|
intermediateCerts := trustmanager.GetIntermediateCerts(decodedCerts)
|
||||||
|
intCerts[leafID] = intermediateCerts
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all the valid leafs to the certificates map so we can refer to them later
|
return leafCerts, intCerts
|
||||||
allCerts[leafID] = leafCert
|
}
|
||||||
|
|
||||||
// Retrieve all the trusted certificates that match this dns Name
|
// certsToRemove returns all the certifificates from oldCerts that aren't present
|
||||||
certsForCN, err := km.trustedCertificateStore.GetCertificatesByCN(dnsName)
|
// in newCerts
|
||||||
if err != nil {
|
func certsToRemove(oldCerts, newCerts []*x509.Certificate) map[string]*x509.Certificate {
|
||||||
// If the error that we get back is different than ErrNoCertificatesFound
|
certsToRemove := make(map[string]*x509.Certificate)
|
||||||
// we couldn't check if there are any certificates with this CN already
|
|
||||||
// trusted. Let's take the conservative approach and not trust this key
|
// If no newCerts were provided
|
||||||
if _, ok := err.(*trustmanager.ErrNoCertificatesFound); !ok {
|
if len(newCerts) == 0 {
|
||||||
logrus.Debugf("error retrieving certificates for: %s, %v", dnsName, err)
|
return certsToRemove
|
||||||
continue
|
}
|
||||||
}
|
|
||||||
}
|
// Populate a map with all the IDs from newCert
|
||||||
|
var newCertMap = make(map[string]struct{})
|
||||||
// If there are no certificates with this CN, lets TOFUS!
|
for _, cert := range newCerts {
|
||||||
// Note that this logic should only exist in docker 1.8
|
certID, err := trustmanager.FingerprintCert(cert)
|
||||||
if len(certsForCN) == 0 {
|
if err != nil {
|
||||||
km.trustedCertificateStore.AddCert(leafCert)
|
logrus.Debugf("error while fingerprinting root certificate with keyID: %s, %v", certID, err)
|
||||||
certsForCN = append(certsForCN, leafCert)
|
continue
|
||||||
logrus.Debugf("using TOFUS on %s with keyID: %s", dnsName, leafID)
|
}
|
||||||
}
|
newCertMap[certID] = struct{}{}
|
||||||
|
}
|
||||||
// Iterate over all known certificates for this CN and see if any are trusted
|
|
||||||
for _, cert := range certsForCN {
|
// Iterate over all the old certificates and check to see if we should remove them
|
||||||
// Check to see if there is an exact match of this certificate.
|
for _, cert := range oldCerts {
|
||||||
certID, err := trustmanager.FingerprintCert(cert)
|
certID, err := trustmanager.FingerprintCert(cert)
|
||||||
if err == nil && certID == leafID {
|
if err != nil {
|
||||||
validKeys[keyID] = rootSigned.Signed.Keys[keyID]
|
logrus.Debugf("error while fingerprinting root certificate with certID: %s, %v", certID, err)
|
||||||
logrus.Debugf("found an exact match for %s with keyID: %s", dnsName, keyID)
|
continue
|
||||||
}
|
}
|
||||||
}
|
if _, ok := newCertMap[certID]; !ok {
|
||||||
|
certsToRemove[certID] = cert
|
||||||
// Check to see if this leafCertificate has a chain to one of the Root
|
}
|
||||||
// CAs of our CA Store.
|
}
|
||||||
err = trustmanager.Verify(km.trustedCAStore, dnsName, decodedCerts)
|
|
||||||
if err == nil {
|
return certsToRemove
|
||||||
validKeys[keyID] = rootSigned.Signed.Keys[keyID]
|
|
||||||
logrus.Debugf("found a CA path for %s with keyID: %s", dnsName, keyID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(validKeys) < 1 {
|
|
||||||
logrus.Debugf("wasn't able to trust any of the root keys")
|
|
||||||
return ErrValidationFail
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(david): change hardcoded minversion on TUF.
|
|
||||||
err = signed.VerifyRoot(root, 0, validKeys)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var newRootKey data.PublicKey
|
|
||||||
// VerifyRoot returns a non-nil value if there is a root key rotation happening.
|
|
||||||
// If this happens, we should replace the old root of trust with the new one
|
|
||||||
if newRootKey != nil {
|
|
||||||
logrus.Debugf("got a new root key to rotate to: %s", newRootKey.ID())
|
|
||||||
|
|
||||||
// Retrieve the certificate associated with the new root key and trust it
|
|
||||||
newRootKeyCert, ok := allCerts[newRootKey.ID()]
|
|
||||||
// Paranoid check for the certificate still being in the map
|
|
||||||
if !ok {
|
|
||||||
logrus.Debugf("error while retrieving new root certificate with keyID: %s, %v", newRootKey.ID(), err)
|
|
||||||
return ErrRootRotationFail
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the new root certificate to our certificate store
|
|
||||||
err := km.trustedCertificateStore.AddCert(newRootKeyCert)
|
|
||||||
if err != nil {
|
|
||||||
// Ignore the error if the certificate already exists
|
|
||||||
if _, ok := err.(*trustmanager.ErrCertExists); !ok {
|
|
||||||
logrus.Debugf("error while adding new root certificate with keyID: %s, %v", newRootKey.ID(), err)
|
|
||||||
return ErrRootRotationFail
|
|
||||||
}
|
|
||||||
logrus.Debugf("root certificate already exists in keystore: %s", newRootKey.ID())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the new root certificate from the certificate mapping so we
|
|
||||||
// can remove trust from all of the remaining ones
|
|
||||||
delete(allCerts, newRootKey.ID())
|
|
||||||
|
|
||||||
// Iterate over all old valid certificates and remove them, essentially
|
|
||||||
// finishing the rotation of the currently trusted root certificate
|
|
||||||
for _, cert := range allCerts {
|
|
||||||
err := km.trustedCertificateStore.RemoveCert(cert)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Debugf("error while removing old root certificate: %v", err)
|
|
||||||
return ErrRootRotationFail
|
|
||||||
}
|
|
||||||
logrus.Debugf("removed trust from old root certificate")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("Root validation succeeded")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,7 +121,7 @@ func (s *X509FileStore) RemoveCert(cert *x509.Certificate) error {
|
||||||
filename := s.fileMap[certID]
|
filename := s.fileMap[certID]
|
||||||
delete(s.fileMap, certID)
|
delete(s.fileMap, certID)
|
||||||
|
|
||||||
name := string(cert.RawSubject)
|
name := string(cert.Subject.CommonName)
|
||||||
|
|
||||||
// Filter the fingerprint out of this name entry
|
// Filter the fingerprint out of this name entry
|
||||||
fpList := s.nameMap[name]
|
fpList := s.nameMap[name]
|
||||||
|
|
|
@ -114,8 +114,6 @@ func fingerprintCert(cert *x509.Certificate) (CertID, error) {
|
||||||
// Create new TUF Key so we can compute the TUF-compliant CertID
|
// Create new TUF Key so we can compute the TUF-compliant CertID
|
||||||
tufKey := data.NewPublicKey(keyType, pemdata)
|
tufKey := data.NewPublicKey(keyType, pemdata)
|
||||||
|
|
||||||
logrus.Debugf("certificate fingerprint generated for key type %s: %s", keyType, tufKey.ID())
|
|
||||||
|
|
||||||
return CertID(tufKey.ID()), nil
|
return CertID(tufKey.ID()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,6 +187,17 @@ func GetLeafCerts(certs []*x509.Certificate) []*x509.Certificate {
|
||||||
return leafCerts
|
return leafCerts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIntermediateCerts parses a list of x509 Certificates and returns all of the
|
||||||
|
// ones marked as a CA, to be used as intermediates
|
||||||
|
func GetIntermediateCerts(certs []*x509.Certificate) (intCerts []*x509.Certificate) {
|
||||||
|
for _, cert := range certs {
|
||||||
|
if cert.IsCA {
|
||||||
|
intCerts = append(intCerts, cert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return intCerts
|
||||||
|
}
|
||||||
|
|
||||||
// ParsePEMPrivateKey returns a data.PrivateKey from a PEM encoded private key. It
|
// ParsePEMPrivateKey returns a data.PrivateKey from a PEM encoded private key. It
|
||||||
// only supports RSA (PKCS#1) and attempts to decrypt using the passphrase, if encrypted.
|
// only supports RSA (PKCS#1) and attempts to decrypt using the passphrase, if encrypted.
|
||||||
func ParsePEMPrivateKey(pemBytes []byte, passphrase string) (data.PrivateKey, error) {
|
func ParsePEMPrivateKey(pemBytes []byte, passphrase string) (data.PrivateKey, error) {
|
||||||
|
@ -420,6 +429,32 @@ func EncryptPrivateKey(key data.PrivateKey, passphrase string) ([]byte, error) {
|
||||||
return pem.EncodeToMemory(encryptedPEMBlock), nil
|
return pem.EncodeToMemory(encryptedPEMBlock), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CertsToKeys transforms each of the input certificates into it's corresponding
|
||||||
|
// PublicKey
|
||||||
|
func CertsToKeys(certs []*x509.Certificate) map[string]data.PublicKey {
|
||||||
|
keys := make(map[string]data.PublicKey)
|
||||||
|
for _, cert := range certs {
|
||||||
|
block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
|
||||||
|
pemdata := pem.EncodeToMemory(&block)
|
||||||
|
|
||||||
|
var keyType data.KeyAlgorithm
|
||||||
|
switch cert.PublicKeyAlgorithm {
|
||||||
|
case x509.RSA:
|
||||||
|
keyType = data.RSAx509Key
|
||||||
|
case x509.ECDSA:
|
||||||
|
keyType = data.ECDSAx509Key
|
||||||
|
default:
|
||||||
|
logrus.Debugf("unknown certificate type found, ignoring")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new the appropriate PublicKey
|
||||||
|
newKey := data.NewPublicKey(keyType, pemdata)
|
||||||
|
keys[newKey.ID()] = newKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
// NewCertificate returns an X509 Certificate following a template, given a GUN.
|
// NewCertificate returns an X509 Certificate following a template, given a GUN.
|
||||||
func NewCertificate(gun string) (*x509.Certificate, error) {
|
func NewCertificate(gun string) (*x509.Certificate, error) {
|
||||||
notBefore := time.Now()
|
notBefore := time.Now()
|
||||||
|
@ -436,7 +471,6 @@ func NewCertificate(gun string) (*x509.Certificate, error) {
|
||||||
return &x509.Certificate{
|
return &x509.Certificate{
|
||||||
SerialNumber: serialNumber,
|
SerialNumber: serialNumber,
|
||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
Organization: []string{gun},
|
|
||||||
CommonName: gun,
|
CommonName: gun,
|
||||||
},
|
},
|
||||||
NotBefore: notBefore,
|
NotBefore: notBefore,
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
package trustmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCertsToKeys(t *testing.T) {
|
||||||
|
// Get root certificate
|
||||||
|
rootCA, err := LoadCertFromFile("../fixtures/root-ca.crt")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Get intermediate certificate
|
||||||
|
intermediateCA, err := LoadCertFromFile("../fixtures/intermediate-ca.crt")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Get leaf certificate
|
||||||
|
leafCert, err := LoadCertFromFile("../fixtures/secure.example.com.crt")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Get our certList with Leaf Cert and Intermediate
|
||||||
|
certList := []*x509.Certificate{leafCert, intermediateCA, rootCA}
|
||||||
|
|
||||||
|
// Call CertsToKEys
|
||||||
|
keys := CertsToKeys(certList)
|
||||||
|
assert.NotNil(t, keys)
|
||||||
|
assert.Len(t, keys, 3)
|
||||||
|
|
||||||
|
// Call GetLeafCerts
|
||||||
|
newKeys := GetLeafCerts(certList)
|
||||||
|
assert.NotNil(t, newKeys)
|
||||||
|
assert.Len(t, newKeys, 1)
|
||||||
|
|
||||||
|
// Call GetIntermediateCerts (checks for certs with IsCA true)
|
||||||
|
newKeys = GetIntermediateCerts(certList)
|
||||||
|
assert.NotNil(t, newKeys)
|
||||||
|
assert.Len(t, newKeys, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCertificate(t *testing.T) {
|
||||||
|
cert, err := NewCertificate("docker.com/alpine")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, cert.Subject.CommonName, "docker.com/alpine")
|
||||||
|
assert.True(t, time.Now().Before(cert.NotAfter))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyOperations(t *testing.T) {
|
||||||
|
// Generate our ED25519 private key
|
||||||
|
edKey, err := GenerateED25519Key(rand.Reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Generate our EC private key
|
||||||
|
ecKey, err := GenerateECDSAKey(rand.Reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Generate our RSA private key
|
||||||
|
rsaKey, err := GenerateRSAKey(rand.Reader, 512)
|
||||||
|
|
||||||
|
// Encode our ED private key
|
||||||
|
edPEM, err := KeyToPEM(edKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Encode our EC private key
|
||||||
|
ecPEM, err := KeyToPEM(ecKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Encode our RSA private key
|
||||||
|
rsaPEM, err := KeyToPEM(rsaKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check to see if ED key it is encoded
|
||||||
|
stringEncodedEDKey := string(edPEM)
|
||||||
|
assert.True(t, strings.Contains(stringEncodedEDKey, "-----BEGIN ED25519 PRIVATE KEY-----"))
|
||||||
|
|
||||||
|
// Check to see if EC key it is encoded
|
||||||
|
stringEncodedECKey := string(ecPEM)
|
||||||
|
assert.True(t, strings.Contains(stringEncodedECKey, "-----BEGIN EC PRIVATE KEY-----"))
|
||||||
|
|
||||||
|
// Check to see if RSA key it is encoded
|
||||||
|
stringEncodedRSAKey := string(rsaPEM)
|
||||||
|
assert.True(t, strings.Contains(stringEncodedRSAKey, "-----BEGIN RSA PRIVATE KEY-----"))
|
||||||
|
|
||||||
|
// Decode our ED Key
|
||||||
|
decodedEDKey, err := ParsePEMPrivateKey(edPEM, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, edKey.Private(), decodedEDKey.Private())
|
||||||
|
|
||||||
|
// Decode our EC Key
|
||||||
|
decodedECKey, err := ParsePEMPrivateKey(ecPEM, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ecKey.Private(), decodedECKey.Private())
|
||||||
|
|
||||||
|
// Decode our RSA Key
|
||||||
|
decodedRSAKey, err := ParsePEMPrivateKey(rsaPEM, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, rsaKey.Private(), decodedRSAKey.Private())
|
||||||
|
|
||||||
|
// Encrypt our ED Key
|
||||||
|
encryptedEDKey, err := EncryptPrivateKey(edKey, "ponies")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Encrypt our EC Key
|
||||||
|
encryptedECKey, err := EncryptPrivateKey(ecKey, "ponies")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Encrypt our RSA Key
|
||||||
|
encryptedRSAKey, err := EncryptPrivateKey(rsaKey, "ponies")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check to see if ED key it is encrypted
|
||||||
|
stringEncryptedEDKey := string(encryptedEDKey)
|
||||||
|
assert.True(t, strings.Contains(stringEncryptedEDKey, "-----BEGIN ED25519 PRIVATE KEY-----"))
|
||||||
|
assert.True(t, strings.Contains(stringEncryptedEDKey, "Proc-Type: 4,ENCRYPTED"))
|
||||||
|
|
||||||
|
// Check to see if EC key it is encrypted
|
||||||
|
stringEncryptedECKey := string(encryptedECKey)
|
||||||
|
assert.True(t, strings.Contains(stringEncryptedECKey, "-----BEGIN EC PRIVATE KEY-----"))
|
||||||
|
assert.True(t, strings.Contains(stringEncryptedECKey, "Proc-Type: 4,ENCRYPTED"))
|
||||||
|
|
||||||
|
// Check to see if RSA key it is encrypted
|
||||||
|
stringEncryptedRSAKey := string(encryptedRSAKey)
|
||||||
|
assert.True(t, strings.Contains(stringEncryptedRSAKey, "-----BEGIN RSA PRIVATE KEY-----"))
|
||||||
|
assert.True(t, strings.Contains(stringEncryptedRSAKey, "Proc-Type: 4,ENCRYPTED"))
|
||||||
|
|
||||||
|
// Decrypt our ED Key
|
||||||
|
decryptedEDKey, err := ParsePEMPrivateKey(encryptedEDKey, "ponies")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, edKey.Private(), decryptedEDKey.Private())
|
||||||
|
|
||||||
|
// Decrypt our EC Key
|
||||||
|
decryptedECKey, err := ParsePEMPrivateKey(encryptedECKey, "ponies")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ecKey.Private(), decryptedECKey.Private())
|
||||||
|
|
||||||
|
// Decrypt our RSA Key
|
||||||
|
decryptedRSAKey, err := ParsePEMPrivateKey(encryptedRSAKey, "ponies")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, rsaKey.Private(), decryptedRSAKey.Private())
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue