1
0
mirror of https://github.com/docker/docs.git synced 2026-11-05 00:14:17 +03:00

Use most specific CA prefix for pinning

Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
This commit is contained in:
Riyaz Faizullabhoy
2016-04-06 15:03:09 -07:00
parent 86d9e081fb
commit 60faca0661
5 changed files with 106 additions and 97 deletions

View File

@@ -219,7 +219,7 @@ func TestValidateRootWithoutTOFUS(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
assert.NoError(t, err, "failed to create a temporary directory: %s", err)
require.NoError(t, err, "failed to create a temporary directory: %s", err)
// Create a X509Store
trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir)
@@ -227,7 +227,7 @@ func TestValidateRootWithoutTOFUS(t *testing.T) {
trustPath,
trustmanager.FilterCertsExpiredSha1,
)
assert.NoError(t, err)
require.NoError(t, err)
// Execute our template
templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate)
@@ -238,7 +238,7 @@ func TestValidateRootWithoutTOFUS(t *testing.T) {
// This call to ValidateRoot will fail since we are explicitly disabling TOFU and have no local certs
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{TOFU: false})
assert.Error(t, err)
require.Error(t, err)
}
func TestValidateRootWithPinnedCert(t *testing.T) {
@@ -248,7 +248,7 @@ func TestValidateRootWithPinnedCert(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
assert.NoError(t, err, "failed to create a temporary directory: %s", err)
require.NoError(t, err, "failed to create a temporary directory: %s", err)
// Create a X509Store
trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir)
@@ -256,7 +256,7 @@ func TestValidateRootWithPinnedCert(t *testing.T) {
trustPath,
trustmanager.FilterCertsExpiredSha1,
)
assert.NoError(t, err)
require.NoError(t, err)
// Execute our template
templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate)
@@ -267,7 +267,7 @@ func TestValidateRootWithPinnedCert(t *testing.T) {
// This call to ValidateRoot should succeed with the correct Cert ID (same as root public key ID)
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string]string{"docker.com/notary": rootPubKeyID}})
assert.NoError(t, err)
require.NoError(t, err)
}
func TestValidateRootFailuresWithPinnedCert(t *testing.T) {
@@ -277,7 +277,7 @@ func TestValidateRootFailuresWithPinnedCert(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
assert.NoError(t, err, "failed to create a temporary directory: %s", err)
require.NoError(t, err, "failed to create a temporary directory: %s", err)
// Create a X509Store
trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir)
@@ -285,7 +285,7 @@ func TestValidateRootFailuresWithPinnedCert(t *testing.T) {
trustPath,
trustmanager.FilterCertsExpiredSha1,
)
assert.NoError(t, err)
require.NoError(t, err)
// Execute our template
templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate)
@@ -296,23 +296,23 @@ func TestValidateRootFailuresWithPinnedCert(t *testing.T) {
// This call to ValidateRoot should fail due to an incorrect cert ID
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string]string{"docker.com/notary": "ABSOLUTELY NOT A CERT ID"}})
assert.Error(t, err)
require.Error(t, err)
// This call to ValidateRoot should fail due to an empty cert ID
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string]string{"docker.com/notary": ""}})
assert.Error(t, err)
require.Error(t, err)
// This call to ValidateRoot should fail due to an invalid GUN (even though the cert ID is correct), and TOFUS defaults to false
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string]string{"not_a_gun": rootPubKeyID}, TOFU: false})
assert.Error(t, err)
require.Error(t, err)
// This call to ValidateRoot should fail due to an invalid cert ID, even though it's a valid key ID for targets
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string]string{"docker.com/notary": targetsPubKeyID}})
assert.Error(t, err)
require.Error(t, err)
// This call to ValidateRoot should succeed because we fall through to TOFUS even though we have no matching GUNs under Certs
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string]string{"not_a_gun": rootPubKeyID}, TOFU: true})
assert.NoError(t, err)
require.NoError(t, err)
}
func TestValidateRootWithPinnedCA(t *testing.T) {
@@ -322,7 +322,7 @@ func TestValidateRootWithPinnedCA(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
assert.NoError(t, err, "failed to create a temporary directory: %s", err)
require.NoError(t, err, "failed to create a temporary directory: %s", err)
// Create a X509Store
trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir)
@@ -330,7 +330,7 @@ func TestValidateRootWithPinnedCA(t *testing.T) {
trustPath,
trustmanager.FilterCertsExpiredSha1,
)
assert.NoError(t, err)
require.NoError(t, err)
templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate)
templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validPEMEncodedRSARoot})
@@ -339,42 +339,42 @@ func TestValidateRootWithPinnedCA(t *testing.T) {
// This call to ValidateRoot will fail because we have an invalid path for the CA
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{CA: map[string]string{"docker.com/notary": filepath.Join(tempBaseDir, "nonexistent")}})
assert.Error(t, err)
require.Error(t, err)
// This call to ValidateRoot will fail because we have no valid GUNs to use, and TOFUS is disabled
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{CA: map[string]string{"othergun": filepath.Join(tempBaseDir, "nonexistent")}, TOFU: false})
assert.Error(t, err)
require.Error(t, err)
// Write an invalid CA cert (not even a PEM) to the tempDir and ensure validation fails when using it
invalidCAFilepath := filepath.Join(tempBaseDir, "invalid.ca")
assert.NoError(t, ioutil.WriteFile(invalidCAFilepath, []byte("ABSOLUTELY NOT A PEM"), 0644))
require.NoError(t, ioutil.WriteFile(invalidCAFilepath, []byte("ABSOLUTELY NOT A PEM"), 0644))
// Using this invalid CA cert should fail on ValidateRoot
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{CA: map[string]string{"docker.com/notary": invalidCAFilepath}, TOFU: false})
assert.Error(t, err)
require.Error(t, err)
validCAFilepath := "../fixtures/root-ca.crt"
// If we pass an invalid Certs entry in addition to this valid CA entry, since Certs has priority for pinning we will fail
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string]string{"docker.com/notary": "invalidID"}, CA: map[string]string{"docker.com/notary": validCAFilepath}, TOFU: false})
assert.Error(t, err)
require.Error(t, err)
// Now construct a new root with a valid cert chain, such that signatures are correct over the 'notary-signer' GUN. Pin the root-ca and validate
leafCert, err := trustmanager.LoadCertFromFile("../fixtures/notary-signer.crt")
assert.NoError(t, err)
require.NoError(t, err)
pemLeafBytes := trustmanager.CertToPEM(leafCert)
newRootLeafKey := data.NewPublicKey(data.RSAx509Key, pemLeafBytes)
intermediateCert, err := trustmanager.LoadCertFromFile("../fixtures/intermediate-ca.crt")
assert.NoError(t, err)
require.NoError(t, err)
pemChainBytes, err := trustmanager.CertChainToPEM([]*x509.Certificate{leafCert, intermediateCert})
assert.NoError(t, err)
require.NoError(t, err)
newRootKey := data.NewPublicKey(data.RSAx509Key, pemChainBytes)
rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{newRootKey.ID()}, nil)
assert.NoError(t, err)
require.NoError(t, err)
testRoot, err := data.NewRoot(
map[string]data.PublicKey{newRootKey.ID(): newRootKey},
@@ -385,31 +385,31 @@ func TestValidateRootWithPinnedCA(t *testing.T) {
data.CanonicalSnapshotRole: &rootRole.RootRole},
false,
)
assert.NoError(t, err, "Failed to create new root")
require.NoError(t, err, "Failed to create new root")
keyReader, err := os.Open("../fixtures/notary-signer.key")
assert.NoError(t, err, "could not open key file")
require.NoError(t, err, "could not open key file")
pemBytes, err := ioutil.ReadAll(keyReader)
assert.NoError(t, err, "could not read key file")
require.NoError(t, err, "could not read key file")
privKey, err := trustmanager.ParsePEMPrivateKey(pemBytes, "")
assert.NoError(t, err)
require.NoError(t, err)
store, err := trustmanager.NewKeyFileStore(tempBaseDir, passphraseRetriever)
assert.NoError(t, err)
require.NoError(t, err)
cs := cryptoservice.NewCryptoService(store)
err = store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: "notary-signer"}, privKey)
assert.NoError(t, err)
require.NoError(t, err)
newTestSignedRoot, err := testRoot.ToSigned()
assert.NoError(t, err)
require.NoError(t, err)
err = signed.Sign(cs, newTestSignedRoot, newRootLeafKey)
assert.NoError(t, err)
require.NoError(t, err)
// Check that we validate correctly against a pinned CA and provided bundle
err = ValidateRoot(certStore, newTestSignedRoot, "notary-signer", notary.TrustPinConfig{CA: map[string]string{"notary-signer": validCAFilepath}, TOFU: false})
assert.NoError(t, err)
require.NoError(t, err)
}
// TestValidateSuccessfulRootRotation runs through a full root certificate rotation

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"github.com/docker/notary"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/tuf/utils"
"strings"
)
@@ -32,36 +31,35 @@ func NewTrustPinChecker(trustPinConfig notary.TrustPinConfig, gun string) (Trust
// Determine the mode, and if it's even valid
if _, ok := trustPinConfig.Certs[gun]; ok {
trustPinChecker.mode = certs
} else if utils.ContainsKeyPrefix(trustPinConfig.CA, gun) {
trustPinChecker.mode = ca
for caGunPrefix, caFilepath := range trustPinConfig.CA {
if strings.HasPrefix(gun, caGunPrefix) {
// Try to add the CA cert to our certificate store,
// and use it to validate certs in the root.json later
caCert, err := trustmanager.LoadCertFromFile(caFilepath)
if err != nil {
return TrustPinChecker{}, fmt.Errorf("could not load root cert from CA path")
}
if err = trustmanager.ValidateCertificate(caCert); err != nil {
return TrustPinChecker{}, fmt.Errorf("invalid CA cert provided")
}
// Now only consider certificates that are direct children from this CA cert, overwriting allValidCerts
caRootPool := x509.NewCertPool()
caRootPool.AddCert(caCert)
if err != nil {
return TrustPinChecker{}, fmt.Errorf("unable to initialize CA cert pool")
}
trustPinChecker.pinnedCAPool = caRootPool
return trustPinChecker, nil
}
}
} else if trustPinConfig.TOFU {
trustPinChecker.mode = tofus
} else {
return TrustPinChecker{}, fmt.Errorf("no trust pinning specified")
return trustPinChecker, nil
}
return trustPinChecker, nil
if caFilepath, err := trustPinChecker.getCAFilepathByPrefix(gun); err == nil {
trustPinChecker.mode = ca
// Try to add the CA certs from its bundle file to our certificate store,
// and use it to validate certs in the root.json later
caCerts, err := trustmanager.LoadCertBundleFromFile(caFilepath)
if err != nil {
return TrustPinChecker{}, fmt.Errorf("could not load root cert from CA path")
}
if err = trustmanager.ValidateCertificate(caCerts); err != nil {
return TrustPinChecker{}, fmt.Errorf("invalid CA cert provided")
}
// Now only consider certificates that are direct children from this CA cert, overwriting allValidCerts
caRootPool := x509.NewCertPool()
caRootPool.AddCert(caCerts)
if err != nil {
return TrustPinChecker{}, fmt.Errorf("unable to initialize CA cert pool")
}
trustPinChecker.pinnedCAPool = caRootPool
return trustPinChecker, nil
}
if trustPinConfig.TOFU {
trustPinChecker.mode = tofus
return trustPinChecker, nil
}
return TrustPinChecker{}, fmt.Errorf("invalid trust pinning specified")
}
func (t TrustPinChecker) checkCert(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool {
@@ -88,3 +86,22 @@ func (t TrustPinChecker) checkCert(leafCert *x509.Certificate, intCerts []*x509.
}
return false
}
// Will return the CA filepath corresponding to the most specific (longest) entry in the map that is still a prefix
// of the provided gun. Returns an error if no entry matches this GUN as a prefix.
func (t TrustPinChecker) getCAFilepathByPrefix(gun string) (string, error) {
specificGUN := ""
specificCAFilepath := ""
foundCA := false
for gunPrefix, caFilepath := range t.config.CA {
if strings.HasPrefix(gun, gunPrefix) && len(gunPrefix) >= len(specificGUN) {
specificGUN = gunPrefix
specificCAFilepath = caFilepath
foundCA = true
}
}
if !foundCA {
return "", fmt.Errorf("could not find pinned CA for GUN: %s\n", gun)
}
return specificCAFilepath, nil
}

View File

@@ -384,7 +384,7 @@ func TestConfigFileTrustPinning(t *testing.T) {
CAFile: "../../fixtures/root-ca.crt",
ClientAuth: tls.RequireAndVerifyClientCert,
})
assert.NoError(t, err)
require.NoError(t, err)
s.StartTLS()
defer s.Close()
@@ -406,7 +406,7 @@ func TestConfigFileTrustPinning(t *testing.T) {
// set a config file, so it doesn't check ~/.notary/config.json by default,
// and execute a random command so that the flags are parsed
cwd, err := os.Getwd()
assert.NoError(t, err)
require.NoError(t, err)
cmd := commander.GetCommand()
cmd.SetArgs([]string{
@@ -416,17 +416,17 @@ func TestConfigFileTrustPinning(t *testing.T) {
"--tlskey", "../../fixtures/notary-server.key"})
cmd.SetOutput(new(bytes.Buffer)) // eat the output
err = cmd.Execute()
assert.Error(t, err, "there was no repository, so list should have failed")
assert.NotContains(t, err.Error(), "TLS", "there was no TLS error though!")
require.Error(t, err, "there was no repository, so list should have failed")
require.NotContains(t, err.Error(), "TLS", "there was no TLS error though!")
// Check that tofu was set correctly
config, err := commander.parseConfig()
assert.NoError(t, err)
assert.Equal(t, true, config.GetBool("trust_pinning.tofu"))
require.NoError(t, err)
require.Equal(t, true, config.GetBool("trust_pinning.tofu"))
// validate that we actually managed to connect and attempted to download the root though
assert.Len(t, m.gotten, 1)
assert.Equal(t, m.gotten[0], "repo.root")
require.Len(t, m.gotten, 1)
require.Equal(t, m.gotten[0], "repo.root")
tempDir2 := tempDirWithConfig(t, fmt.Sprintf(`{
"remote_server": {
@@ -447,15 +447,15 @@ func TestConfigFileTrustPinning(t *testing.T) {
"--tlskey", "../../fixtures/notary-server.key"})
cmd.SetOutput(new(bytes.Buffer)) // eat the output
err = cmd.Execute()
assert.Error(t, err, "there was no repository, so list should have failed")
assert.NotContains(t, err.Error(), "TLS", "there was no TLS error though!")
require.Error(t, err, "there was no repository, so list should have failed")
require.NotContains(t, err.Error(), "TLS", "there was no TLS error though!")
config, err = commander.parseConfig()
assert.NoError(t, err)
assert.Equal(t, false, config.GetBool("trust_pinning.tofu"))
require.NoError(t, err)
require.Equal(t, false, config.GetBool("trust_pinning.tofu"))
// Even though TOFUS will fail, we'll have successfully parsed the config and attempted to retrieve the root
assert.Len(t, m.gotten, 2)
require.Len(t, m.gotten, 2)
tempDir = tempDirWithConfig(t, fmt.Sprintf(`{
"remote_server": {
@@ -478,15 +478,15 @@ func TestConfigFileTrustPinning(t *testing.T) {
"--tlskey", "../../fixtures/notary-server.key"})
cmd.SetOutput(new(bytes.Buffer)) // eat the output
err = cmd.Execute()
assert.Error(t, err, "there was no repository, so list should have failed")
assert.NotContains(t, err.Error(), "TLS", "there was no TLS error though!")
require.Error(t, err, "there was no repository, so list should have failed")
require.NotContains(t, err.Error(), "TLS", "there was no TLS error though!")
config, err = commander.parseConfig()
assert.NoError(t, err)
assert.Equal(t, strings.Repeat("x", notary.Sha256HexSize), config.GetStringMap("trust_pinning.certs")["repo3"])
require.NoError(t, err)
require.Equal(t, strings.Repeat("x", notary.Sha256HexSize), config.GetStringMap("trust_pinning.certs")["repo3"])
// Even though specified certs will fail, we'll have successfully parsed the config and attempted to retrieve the root
assert.Len(t, m.gotten, 3)
require.Len(t, m.gotten, 3)
tempDir = tempDirWithConfig(t, fmt.Sprintf(`{
"remote_server": {
@@ -509,13 +509,13 @@ func TestConfigFileTrustPinning(t *testing.T) {
"--tlskey", "../../fixtures/notary-server.key"})
cmd.SetOutput(new(bytes.Buffer)) // eat the output
err = cmd.Execute()
assert.Error(t, err, "there was no repository, so list should have failed")
assert.NotContains(t, err.Error(), "TLS", "there was no TLS error though!")
require.Error(t, err, "there was no repository, so list should have failed")
require.NotContains(t, err.Error(), "TLS", "there was no TLS error though!")
config, err = commander.parseConfig()
assert.NoError(t, err)
assert.Equal(t, "root-ca.crt", config.GetStringMap("trust_pinning.ca")["repo4"])
require.NoError(t, err)
require.Equal(t, "root-ca.crt", config.GetStringMap("trust_pinning.ca")["repo4"])
// Even though specified ca will fail, we'll have successfully parsed the config and attempted to retrieve the root
assert.Len(t, m.gotten, 4)
require.Len(t, m.gotten, 4)
}

View File

@@ -154,7 +154,9 @@ bootstrapping of the repo.
<td valign="top"><code>ca</code></td>
<td valign="top">no</td>
<td valign="top"><p>Mapping of GUN prefixes to filepaths containing
the root CA with which to verify the certificates in the root file.
the root CA file with which to verify the certificates in the root file.
This file can contain multiple root certificates, bundled in separate
PEM blocks.
The path is relative to the directory of the configuration file.</p></td>
</tr>
<tr>

View File

@@ -67,16 +67,6 @@ func StrSliceContainsI(ss []string, s string) bool {
return false
}
// ContainsKeyPrefix checks if the given string map contains a key that prefixes the desired key
func ContainsKeyPrefix(smap map[string]string, s string) bool {
for k := range smap {
if strings.HasPrefix(s, k) {
return true
}
}
return false
}
// FileExists returns true if a file (or dir) exists at the given path,
// false otherwise
func FileExists(path string) bool {