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

Merge pull request #624 from docker/trust-pinning

Trust pinning configuration
This commit is contained in:
Riyaz Faizullabhoy
2016-04-19 13:38:51 -07:00
26 changed files with 1005 additions and 98 deletions

View File

@@ -13,6 +13,7 @@ import (
"time"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/store"
"github.com/stretchr/testify/require"
@@ -90,7 +91,7 @@ func Test0Dot1RepoFormat(t *testing.T) {
defer ts.Close()
repo, err := NewNotaryRepository(tmpDir, gun, ts.URL, http.DefaultTransport,
passphrase.ConstantRetriever(passwd))
passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
// targets should have 1 target, and it should be readable offline
@@ -152,7 +153,7 @@ func TestDownloading0Dot1RepoFormat(t *testing.T) {
defer os.RemoveAll(repoDir)
repo, err := NewNotaryRepository(repoDir, gun, ts.URL, http.DefaultTransport,
passphrase.ConstantRetriever(passwd))
passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
err = repo.Update(true)

View File

@@ -15,10 +15,10 @@ import (
"github.com/Sirupsen/logrus"
"github.com/docker/notary"
"github.com/docker/notary/certs"
"github.com/docker/notary/client/changelist"
"github.com/docker/notary/cryptoservice"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf"
tufclient "github.com/docker/notary/tuf/client"
"github.com/docker/notary/tuf/data"
@@ -88,13 +88,14 @@ type NotaryRepository struct {
tufRepo *tuf.Repo
roundTrip http.RoundTripper
CertStore trustmanager.X509Store
trustPinning trustpinning.TrustPinConfig
}
// repositoryFromKeystores is a helper function for NewNotaryRepository that
// takes some basic NotaryRepository parameters as well as keystores (in order
// of usage preference), and returns a NotaryRepository.
func repositoryFromKeystores(baseDir, gun, baseURL string, rt http.RoundTripper,
keyStores []trustmanager.KeyStore) (*NotaryRepository, error) {
keyStores []trustmanager.KeyStore, trustPin trustpinning.TrustPinConfig) (*NotaryRepository, error) {
certPath := filepath.Join(baseDir, notary.TrustedCertsDir)
certStore, err := trustmanager.NewX509FilteredFileStore(
@@ -115,6 +116,7 @@ func repositoryFromKeystores(baseDir, gun, baseURL string, rt http.RoundTripper,
CryptoService: cryptoService,
roundTrip: rt,
CertStore: certStore,
trustPinning: trustPin,
}
fileStore, err := store.NewFilesystemStore(
@@ -873,7 +875,7 @@ func (r *NotaryRepository) validateRoot(rootJSON []byte) (*data.SignedRoot, erro
return nil, err
}
err = certs.ValidateRoot(r.CertStore, root, r.gun)
err = trustpinning.ValidateRoot(r.CertStore, root, r.gun, r.trustPinning)
if err != nil {
return nil, err
}

View File

@@ -5,7 +5,7 @@ import (
"testing"
"github.com/Sirupsen/logrus"
"github.com/docker/notary/certs"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/stretchr/testify/require"
)
@@ -72,7 +72,7 @@ func validateRootSuccessfully(t *testing.T, rootType string) {
// in the store for the dnsName docker.com/notary, so TOFUS doesn't apply
_, err = repo.ListTargets(data.CanonicalTargetsRole)
require.Error(t, err, "An error was expected")
require.Equal(t, err, &certs.ErrValidationFail{
require.Equal(t, err, &trustpinning.ErrValidationFail{
Reason: "failed to validate data with current trusted certificates",
})
}

View File

@@ -31,6 +31,7 @@ import (
"github.com/docker/notary/server"
"github.com/docker/notary/server/storage"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/signed"
"github.com/docker/notary/tuf/store"
@@ -191,7 +192,7 @@ func createRepoAndKey(t *testing.T, rootType, tempBaseDir, gun, url string) (
rec := newRoleRecorder()
repo, err := NewNotaryRepository(
tempBaseDir, gun, url, http.DefaultTransport, rec.retriever)
tempBaseDir, gun, url, http.DefaultTransport, rec.retriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
rootPubKey, err := repo.CryptoService.Create("root", repo.gun, rootType)
@@ -222,7 +223,7 @@ func newRepoToTestRepo(t *testing.T, existingRepo *NotaryRepository, newDir bool
rec := newRoleRecorder()
repo, err := NewNotaryRepository(
repoDir, existingRepo.gun, existingRepo.baseURL,
http.DefaultTransport, rec.retriever)
http.DefaultTransport, rec.retriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repository: %s", err)
if err != nil && newDir {
defer os.RemoveAll(repoDir)
@@ -574,7 +575,7 @@ func testInitRepoAttemptsExceeded(t *testing.T, rootType string) {
defer ts.Close()
retriever := passphrase.ConstantRetriever("password")
repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever)
repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
rootPubKey, err := repo.CryptoService.Create("root", repo.gun, rootType)
require.NoError(t, err, "error generating root key: %s", err)
@@ -582,7 +583,7 @@ func testInitRepoAttemptsExceeded(t *testing.T, rootType string) {
retriever = passphrase.ConstantRetriever("incorrect password")
// repo.CryptoService’s FileKeyStore caches the unlocked private key, so to test
// private key unlocking we need a new repo instance.
repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever)
repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
err = repo.Initialize(rootPubKey.ID())
require.EqualError(t, err, trustmanager.ErrAttemptsExceeded{}.Error())
@@ -612,14 +613,14 @@ func testInitRepoPasswordInvalid(t *testing.T, rootType string) {
defer ts.Close()
retriever := passphrase.ConstantRetriever("password")
repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever)
repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
rootPubKey, err := repo.CryptoService.Create("root", repo.gun, rootType)
require.NoError(t, err, "error generating root key: %s", err)
// repo.CryptoService’s FileKeyStore caches the unlocked private key, so to test
// private key unlocking we need a new repo instance.
repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, giveUpPassphraseRetriever)
repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, giveUpPassphraseRetriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
err = repo.Initialize(rootPubKey.ID())
require.EqualError(t, err, trustmanager.ErrPasswordInvalid{}.Error())
@@ -1641,7 +1642,7 @@ func TestPublishUninitializedRepo(t *testing.T) {
defer os.RemoveAll(tempBaseDir)
repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL,
http.DefaultTransport, passphraseRetriever)
http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repository: %s", err)
err = repo.Publish()
require.Error(t, err)
@@ -2005,7 +2006,7 @@ func TestPublishSnapshotLocalKeysCreatedFirst(t *testing.T) {
defer ts.Close()
repo, err := NewNotaryRepository(
tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever)
tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
cs := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever))
@@ -2863,7 +2864,7 @@ func TestRemoteServerUnavailableNoLocalCache(t *testing.T) {
defer ts.Close()
repo, err := NewNotaryRepository(tempBaseDir, "docker.com/notary",
ts.URL, http.DefaultTransport, passphraseRetriever)
ts.URL, http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
_, err = repo.ListTargets(data.CanonicalTargetsRole)
@@ -3173,6 +3174,7 @@ func TestBootstrapClientBadURL(t *testing.T) {
"http://localhost:9998",
http.DefaultTransport,
passphraseRetriever,
trustpinning.TrustPinConfig{},
)
require.NoError(t, err, "error creating repo: %s", err)
@@ -3202,6 +3204,7 @@ func TestBootstrapClientInvalidURL(t *testing.T) {
"#!*)&!)#*^%!#)%^!#",
http.DefaultTransport,
passphraseRetriever,
trustpinning.TrustPinConfig{},
)
require.NoError(t, err, "error creating repo: %s", err)

View File

@@ -14,8 +14,8 @@ import (
"testing"
"time"
"github.com/docker/notary/certs"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/client"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/signed"
@@ -31,7 +31,7 @@ func newBlankRepo(t *testing.T, url string) *NotaryRepository {
require.NoError(t, err, "failed to create a temporary directory: %s", err)
repo, err := NewNotaryRepository(tempBaseDir, "docker.com/notary", url,
http.DefaultTransport, passphrase.ConstantRetriever("pass"))
http.DefaultTransport, passphrase.ConstantRetriever("pass"), trustpinning.TrustPinConfig{})
require.NoError(t, err)
return repo
}
@@ -784,29 +784,29 @@ var waysToMessUpServer = []swizzleExpectations{
// for everything else, the errors come from tuf/signed
{desc: "invalid SignedMeta Type", expectErrs: []interface{}{
&certs.ErrValidationFail{}, signed.ErrWrongType, data.ErrInvalidMetadata{}},
&trustpinning.ErrValidationFail{}, signed.ErrWrongType, data.ErrInvalidMetadata{}},
swizzle: (*testutils.MetadataSwizzler).SetInvalidMetadataType},
{desc: "invalid signatures", expectErrs: []interface{}{
&certs.ErrValidationFail{}, signed.ErrRoleThreshold{}},
&trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}},
swizzle: (*testutils.MetadataSwizzler).InvalidateMetadataSignatures},
{desc: "meta signed by wrong key", expectErrs: []interface{}{
&certs.ErrValidationFail{}, signed.ErrRoleThreshold{}},
&trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}},
swizzle: (*testutils.MetadataSwizzler).SignMetadataWithInvalidKey},
{desc: "expired metadata", expectErrs: []interface{}{
&certs.ErrValidationFail{}, signed.ErrExpired{}},
&trustpinning.ErrValidationFail{}, signed.ErrExpired{}},
swizzle: (*testutils.MetadataSwizzler).ExpireMetadata},
{desc: "lower metadata version", expectErrs: []interface{}{
&certs.ErrValidationFail{}, signed.ErrLowVersion{}},
&trustpinning.ErrValidationFail{}, signed.ErrLowVersion{}},
swizzle: func(s *testutils.MetadataSwizzler, role string) error {
return s.OffsetMetadataVersion(role, -3)
}},
{desc: "insufficient signatures", expectErrs: []interface{}{
&certs.ErrValidationFail{}, signed.ErrRoleThreshold{}},
&trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}},
swizzle: func(s *testutils.MetadataSwizzler, role string) error {
return s.SetThreshold(role, 2)
}},
@@ -824,7 +824,7 @@ func waysToMessUpServerRoot() []swizzleExpectations {
swizzleExpectations{
desc: fmt.Sprintf("no %s keys", roleName),
expectErrs: []interface{}{
&certs.ErrValidationFail{}, signed.ErrRoleThreshold{}},
&trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}},
swizzle: func(s *testutils.MetadataSwizzler, role string) error {
return s.MutateRoot(func(r *data.Root) {
r.Roles[roleName].KeyIDs = []string{}

View File

@@ -8,13 +8,14 @@ import (
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustpinning"
)
// NewNotaryRepository is a helper method that returns a new notary repository.
// It takes the base directory under where all the trust files will be stored
// (usually ~/.docker/trust/).
func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
retriever passphrase.Retriever) (
retriever passphrase.Retriever, trustPinning trustpinning.TrustPinConfig) (
*NotaryRepository, error) {
fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever)
@@ -23,5 +24,5 @@ func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
}
return repositoryFromKeystores(baseDir, gun, baseURL, rt,
[]trustmanager.KeyStore{fileKeyStore})
[]trustmanager.KeyStore{fileKeyStore}, trustPinning)
}

View File

@@ -9,13 +9,14 @@ import (
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustmanager/yubikey"
"github.com/docker/notary/trustpinning"
)
// NewNotaryRepository is a helper method that returns a new notary repository.
// It takes the base directory under where all the trust files will be stored
// (usually ~/.docker/trust/).
func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
retriever passphrase.Retriever) (
retriever passphrase.Retriever, trustPinning trustpinning.TrustPinConfig) (
*NotaryRepository, error) {
fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever)
@@ -29,5 +30,5 @@ func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper,
keyStores = []trustmanager.KeyStore{yubiKeyStore, fileKeyStore}
}
return repositoryFromKeystores(baseDir, gun, baseURL, rt, keyStores)
return repositoryFromKeystores(baseDir, gun, baseURL, rt, keyStores, trustPinning)
}

View File

@@ -138,8 +138,12 @@ func (c *certCommander) certRemove(cmd *cobra.Command, args []string) error {
if removeTrustData {
// Remove all TUF data, so call RemoveTrustData on a NotaryRepository with the GUN
// no online operations are performed so the transport argument is nil
trustPin, err := getTrustPinning(config)
if err != nil {
return err
}
nRepo, err := notaryclient.NewNotaryRepository(
trustDir, c.certRemoveGUN, getRemoteTrustServer(config), nil, c.retriever)
trustDir, c.certRemoveGUN, getRemoteTrustServer(config), nil, c.retriever, trustPin)
if err != nil {
return fmt.Errorf("Could not establish trust data for GUN %s", c.certRemoveGUN)
}

View File

@@ -83,9 +83,14 @@ func (d *delegationCommander) delegationsList(cmd *cobra.Command, args []string)
return err
}
trustPin, err := getTrustPinning(config)
if err != nil {
return err
}
// initialize repo with transport to get latest state of the world before listing delegations
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, d.retriever)
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, d.retriever, trustPin)
if err != nil {
return err
}
@@ -137,10 +142,15 @@ func (d *delegationCommander) delegationRemove(cmd *cobra.Command, args []string
d.paths = nil
}
trustPin, err := getTrustPinning(config)
if err != nil {
return err
}
// no online operations are performed by add so the transport argument
// should be nil
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever)
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever, trustPin)
if err != nil {
return err
}
@@ -243,10 +253,15 @@ func (d *delegationCommander) delegationAdd(cmd *cobra.Command, args []string) e
d.paths = []string{""}
}
trustPin, err := getTrustPinning(config)
if err != nil {
return err
}
// no online operations are performed by add so the transport argument
// should be nil
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever)
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever, trustPin)
if err != nil {
return err
}

View File

@@ -418,9 +418,14 @@ func (k *keyCommander) keysRotate(cmd *cobra.Command, args []string) error {
return err
}
trustPin, err := getTrustPinning(config)
if err != nil {
return err
}
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config),
rt, k.getRetriever())
rt, k.getRetriever(), trustPin)
if err != nil {
return err
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/docker/notary/server"
"github.com/docker/notary/server/storage"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -329,7 +330,7 @@ func setUpRepo(t *testing.T, tempBaseDir, gun string, ret passphrase.Retriever)
ts := httptest.NewServer(server.RootHandler(nil, ctx, cryptoService, nil, nil))
repo, err := client.NewNotaryRepository(
tempBaseDir, gun, ts.URL, http.DefaultTransport, ret)
tempBaseDir, gun, ts.URL, http.DefaultTransport, ret, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
rootPubKey, err := repo.CryptoService.Create("root", "", data.ECDSAKey)
@@ -371,7 +372,7 @@ func TestRotateKeyRemoteServerManagesKey(t *testing.T) {
}
require.NoError(t, k.keysRotate(&cobra.Command{}, []string{gun, role, "-r"}))
repo, err := client.NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, ret)
repo, err := client.NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, ret, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
cl, err := repo.GetChangelist()
@@ -395,7 +396,6 @@ func TestRotateKeyRemoteServerManagesKey(t *testing.T) {
require.True(t, ok)
}
}
}
}
@@ -426,7 +426,7 @@ func TestRotateKeyBothKeys(t *testing.T) {
require.NoError(t, k.keysRotate(&cobra.Command{}, []string{gun, data.CanonicalTargetsRole}))
require.NoError(t, k.keysRotate(&cobra.Command{}, []string{gun, data.CanonicalSnapshotRole}))
repo, err := client.NewNotaryRepository(tempBaseDir, gun, ts.URL, nil, ret)
repo, err := client.NewNotaryRepository(tempBaseDir, gun, ts.URL, nil, ret, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
cl, err := repo.GetChangelist()

View File

@@ -13,6 +13,7 @@ import (
"time"
"github.com/docker/go-connections/tlsconfig"
"github.com/docker/notary"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/server/storage"
"github.com/stretchr/testify/require"
@@ -369,3 +370,108 @@ func TestConfigFileOverridenByCmdLineFlags(t *testing.T) {
require.Len(t, m.gotten, 1)
require.Equal(t, m.gotten[0], "repo.root")
}
// the config can specify trust pinning settings for TOFUs, as well as pinned Certs or CA
func TestConfigFileTrustPinning(t *testing.T) {
var err error
tempDir := tempDirWithConfig(t, `{
"trust_pinning": {
"disable_tofu": false
}
}`)
defer os.RemoveAll(tempDir)
commander := &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
configFile: filepath.Join(tempDir, "config.json"),
}
// Check that tofu was set correctly
config, err := commander.parseConfig()
require.NoError(t, err)
require.Equal(t, false, config.GetBool("trust_pinning.disable_tofu"))
trustPin, err := getTrustPinning(config)
require.NoError(t, err)
require.Equal(t, false, trustPin.DisableTOFU)
tempDir = tempDirWithConfig(t, `{
"remote_server": {
"url": "%s"
},
"trust_pinning": {
"disable_tofu": true
}
}`)
defer os.RemoveAll(tempDir)
commander = &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
configFile: filepath.Join(tempDir, "config.json"),
}
// Check that tofu was correctly disabled
config, err = commander.parseConfig()
require.NoError(t, err)
require.Equal(t, true, config.GetBool("trust_pinning.disable_tofu"))
trustPin, err = getTrustPinning(config)
require.NoError(t, err)
require.Equal(t, true, trustPin.DisableTOFU)
tempDir = tempDirWithConfig(t, fmt.Sprintf(`{
"trust_pinning": {
"certs": {
"repo3": ["%s"]
}
}
}`, strings.Repeat("x", notary.Sha256HexSize)))
defer os.RemoveAll(tempDir)
commander = &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
configFile: filepath.Join(tempDir, "config.json"),
}
config, err = commander.parseConfig()
require.NoError(t, err)
require.Equal(t, []interface{}{strings.Repeat("x", notary.Sha256HexSize)}, config.GetStringMap("trust_pinning.certs")["repo3"])
trustPin, err = getTrustPinning(config)
require.NoError(t, err)
require.Equal(t, strings.Repeat("x", notary.Sha256HexSize), trustPin.Certs["repo3"][0])
// Check that an invalid cert ID pinning format fails
tempDir = tempDirWithConfig(t, fmt.Sprintf(`{
"trust_pinning": {
"certs": {
"repo3": "%s"
}
}
}`, strings.Repeat("x", notary.Sha256HexSize)))
defer os.RemoveAll(tempDir)
commander = &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
configFile: filepath.Join(tempDir, "config.json"),
}
config, err = commander.parseConfig()
require.NoError(t, err)
trustPin, err = getTrustPinning(config)
require.Error(t, err)
tempDir = tempDirWithConfig(t, fmt.Sprintf(`{
"trust_pinning": {
"ca": {
"repo4": "%s"
}
}
}`, "root-ca.crt"))
defer os.RemoveAll(tempDir)
commander = &notaryCommander{
getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") },
configFile: filepath.Join(tempDir, "config.json"),
}
config, err = commander.parseConfig()
require.NoError(t, err)
require.Equal(t, "root-ca.crt", config.GetStringMap("trust_pinning.ca")["repo4"])
trustPin, err = getTrustPinning(config)
require.NoError(t, err)
require.Equal(t, "root-ca.crt", trustPin.CA["repo4"])
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/docker/go-connections/tlsconfig"
notaryclient "github.com/docker/notary/client"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/utils"
"github.com/spf13/cobra"
@@ -116,10 +117,15 @@ func (t *tufCommander) tufAdd(cmd *cobra.Command, args []string) error {
targetName := args[1]
targetPath := args[2]
trustPin, err := getTrustPinning(config)
if err != nil {
return err
}
// no online operations are performed by add so the transport argument
// should be nil
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever)
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, trustPin)
if err != nil {
return err
}
@@ -155,8 +161,13 @@ func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error {
return err
}
trustPin, err := getTrustPinning(config)
if err != nil {
return err
}
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever)
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin)
if err != nil {
return err
}
@@ -200,8 +211,13 @@ func (t *tufCommander) tufList(cmd *cobra.Command, args []string) error {
return err
}
trustPin, err := getTrustPinning(config)
if err != nil {
return err
}
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever)
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin)
if err != nil {
return err
}
@@ -235,8 +251,13 @@ func (t *tufCommander) tufLookup(cmd *cobra.Command, args []string) error {
return err
}
trustPin, err := getTrustPinning(config)
if err != nil {
return err
}
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever)
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin)
if err != nil {
return err
}
@@ -262,8 +283,13 @@ func (t *tufCommander) tufStatus(cmd *cobra.Command, args []string) error {
}
gun := args[0]
trustPin, err := getTrustPinning(config)
if err != nil {
return err
}
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever)
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, trustPin)
if err != nil {
return err
}
@@ -306,8 +332,13 @@ func (t *tufCommander) tufPublish(cmd *cobra.Command, args []string) error {
return err
}
trustPin, err := getTrustPinning(config)
if err != nil {
return err
}
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever)
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin)
if err != nil {
return err
}
@@ -330,10 +361,15 @@ func (t *tufCommander) tufRemove(cmd *cobra.Command, args []string) error {
gun := args[0]
targetName := args[1]
trustPin, err := getTrustPinning(config)
if err != nil {
return err
}
// no online operation are performed by remove so the transport argument
// should be nil.
repo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever)
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, trustPin)
if err != nil {
return err
}
@@ -371,8 +407,13 @@ func (t *tufCommander) tufVerify(cmd *cobra.Command, args []string) error {
return err
}
trustPin, err := getTrustPinning(config)
if err != nil {
return err
}
nRepo, err := notaryclient.NewNotaryRepository(
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever)
config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin)
if err != nil {
return err
}
@@ -546,3 +587,30 @@ func getRemoteTrustServer(config *viper.Viper) string {
}
return defaultServerURL
}
func getTrustPinning(config *viper.Viper) (trustpinning.TrustPinConfig, error) {
var ok bool
// Need to parse out Certs section from config
certMap := config.GetStringMap("trust_pinning.certs")
resultCertMap := make(map[string][]string)
for gun, certSlice := range certMap {
var castedCertSlice []interface{}
if castedCertSlice, ok = certSlice.([]interface{}); !ok {
return trustpinning.TrustPinConfig{}, fmt.Errorf("invalid format for trust_pinning.certs")
}
certsForGun := make([]string, len(castedCertSlice))
for idx, certIDInterface := range castedCertSlice {
if certID, ok := certIDInterface.(string); ok {
certsForGun[idx] = certID
} else {
return trustpinning.TrustPinConfig{}, fmt.Errorf("invalid format for trust_pinning.certs")
}
}
resultCertMap[gun] = certsForGun
}
return trustpinning.TrustPinConfig{
DisableTOFU: config.GetBool("trust_pinning.disable_tofu"),
CA: config.GetStringMapString("trust_pinning.ca"),
Certs: resultCertMap,
}, nil
}

View File

@@ -21,13 +21,6 @@ func GenerateCertificate(rootKey data.PrivateKey, gun string, startTime, endTime
return generateCertificate(signer, gun, startTime, endTime)
}
// GenerateTestingCertificate generates a non-expired X509 Certificate from a template, given a GUN.
// Good enough for tests where expiration does not really matter; do not use if you care about the policy.
func GenerateTestingCertificate(signer crypto.Signer, gun string) (*x509.Certificate, error) {
startTime := time.Now()
return generateCertificate(signer, gun, startTime, startTime.AddDate(10, 0, 0))
}
func generateCertificate(signer crypto.Signer, gun string, startTime, endTime time.Time) (*x509.Certificate, error) {
template, err := trustmanager.NewCertificate(gun, startTime, endTime)
if err != nil {

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -36,6 +36,11 @@ JSON keys to learn more about the configuration section corresponding to that ke
"tls_client_cert": "./fixtures/secure.example.com.crt",
"tls_client_key": "./fixtures/secure.example.com.crt"
}
<a href="#trust-pinning-section-optional">"trust_pinning"</a>: {
"certs": {
"docker.com/notary": ["49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292"]
}
}
}
</code></pre>
@@ -113,6 +118,56 @@ Remote server example:
</tr>
</table>
## trust_pinning section (optional)
The `trust_pinning` specifies how to bootstrap trust for the root of a
Notary client's trusted collection.
This section is optional, Notary will use TOFU over HTTPS by default and
trust certificates in the downloaded root file.
In this section, one can provide specific certificates to pin to, or a CA
to pin to as a root of trust for a GUN. Multiple sections can be specified,
but the pinned certificates will take highest priority for validation, followed
by the pinned CA, followed by TOFUS (TOFU over HTTPS). The diagram below
describes this validation flow:
<center><img src="../images/trust-pinning-flow.png" alt="Trust pinning flow" width="400px"/></center>
Only one trust pinning option will be used to validate a GUN even if multiple
sections are specified, and any validation failure will result in a failed
bootstrapping of the repo.
<table>
<tr>
<th>Parameter</th>
<th>Required</th>
<th>Description</th>
</tr>
<tr>
<td valign="top"><code>certs</code></td>
<td valign="top">no</td>
<td valign="top"><p>Mapping of GUN to certificate IDs to pin to.
Both are strings in the JSON object.</p></td>
</tr>
<tr>
<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 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>
<td valign="top"><code>disable_tofu</code></td>
<td valign="top">no</td>
<td valign="top"><p>Boolean value determining whether to use trust
on first use when bootstrapping validation on a collection's
root file. This keeps TOFUs on by default.</p></td>
</tr>
</table>
## Environment variables (optional)
The following environment variables containing signing key passphrases can

View File

@@ -246,7 +246,7 @@ func (s *X509FileStore) GetCertificatesByCN(cn string) ([]*x509.Certificate, err
// as part of the roots list. This never allows the use of system roots, returning
// an error if there are no root CAs.
func (s *X509FileStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) {
// If we have no Certificates loaded return error (we don't want to rever to using
// If we have no Certificates loaded return error (we don't want to revert to using
// system CAs).
if len(s.fingerprintMap) == 0 {
return x509.VerifyOptions{}, errors.New("no root CAs available")

View File

@@ -188,7 +188,7 @@ func (s *X509MemStore) GetCertificatesByCN(cn string) ([]*x509.Certificate, erro
// as part of the roots list. This never allows the use of system roots, returning
// an error if there are no root CAs.
func (s *X509MemStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) {
// If we have no Certificates loaded return error (we don't want to rever to using
// If we have no Certificates loaded return error (we don't want to revert to using
// system CAs).
if len(s.fingerprintMap) == 0 {
return x509.VerifyOptions{}, errors.New("no root CAs available")

View File

@@ -1,6 +1,7 @@
package trustmanager
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
@@ -57,13 +58,24 @@ func GetCertFromURL(urlStr string) (*x509.Certificate, error) {
return cert, nil
}
// CertToPEM is an utility function returns a PEM encoded x509 Certificate
// CertToPEM is a utility function returns a PEM encoded x509 Certificate
func CertToPEM(cert *x509.Certificate) []byte {
pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
return pemCert
}
// CertChainToPEM is a utility function returns a PEM encoded chain of x509 Certificates, in the order they are passed
func CertChainToPEM(certChain []*x509.Certificate) ([]byte, error) {
var pemBytes bytes.Buffer
for _, cert := range certChain {
if err := pem.Encode(&pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil {
return nil, err
}
}
return pemBytes.Bytes(), nil
}
// LoadCertFromPEM returns the first certificate found in a bunch of bytes or error
// if nothing is found. Taken from https://golang.org/src/crypto/x509/cert_pool.go#L85.
func LoadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) {
@@ -544,12 +556,34 @@ func CertToKey(cert *x509.Certificate) data.PublicKey {
}
}
// CertsToKeys transforms each of the input certificates into it's corresponding
// CertsToKeys transforms each of the input certificate chains into its corresponding
// PublicKey
func CertsToKeys(certs []*x509.Certificate) map[string]data.PublicKey {
func CertsToKeys(leafCerts []*x509.Certificate, intCerts map[string][]*x509.Certificate) map[string]data.PublicKey {
keys := make(map[string]data.PublicKey)
for _, cert := range certs {
newKey := CertToKey(cert)
for _, leafCert := range leafCerts {
certBundle := []*x509.Certificate{leafCert}
certID, err := FingerprintCert(leafCert)
if err != nil {
continue
}
if intCertsForLeafs, ok := intCerts[certID]; ok {
certBundle = append(certBundle, intCertsForLeafs...)
}
certChainPEM, err := CertChainToPEM(certBundle)
if err != nil {
continue
}
var newKey data.PublicKey
// Use the leaf cert's public key algorithm for typing
switch leafCert.PublicKeyAlgorithm {
case x509.RSA:
newKey = data.NewRSAx509PublicKey(certChainPEM)
case x509.ECDSA:
newKey = data.NewECDSAx509PublicKey(certChainPEM)
default:
logrus.Debugf("Unknown key type parsed from certificate: %v", leafCert.PublicKeyAlgorithm)
continue
}
keys[newKey.ID()] = newKey
}
return keys
@@ -581,6 +615,7 @@ func NewCertificate(gun string, startTime, endTime time.Time) (*x509.Certificate
// X509PublicKeyID returns a public key ID as a string, given a
// data.PublicKey that contains an X509 Certificate
func X509PublicKeyID(certPubKey data.PublicKey) (string, error) {
// Note that this only loads the first certificate from the public key
cert, err := LoadCertFromPEM(certPubKey.Public())
if err != nil {
return "", err

View File

@@ -30,8 +30,8 @@ func TestCertsToKeys(t *testing.T) {
// Get our certList with Leaf Cert and Intermediate
certList := []*x509.Certificate{leafCert, intermediateCA, rootCA}
// Call CertsToKEys
keys := CertsToKeys(certList)
// Call CertsToKeys
keys := CertsToKeys(certList, make(map[string][]*x509.Certificate))
require.NotNil(t, keys)
require.Len(t, keys, 3)
@@ -44,6 +44,13 @@ func TestCertsToKeys(t *testing.T) {
newKeys = GetIntermediateCerts(certList)
require.NotNil(t, newKeys)
require.Len(t, newKeys, 2)
// Try calling CertToKeys on a junk leaf cert that won't fingerprint
emptyCert := x509.Certificate{}
// Also try changing the pre-existing leaf cert into an invalid algorithm
leafCert.PublicKeyAlgorithm = x509.DSA
keys = CertsToKeys([]*x509.Certificate{&emptyCert, leafCert}, make(map[string][]*x509.Certificate))
require.Empty(t, keys)
}
func TestNewCertificate(t *testing.T) {

View File

@@ -1,4 +1,4 @@
package certs
package trustpinning
import (
"crypto/x509"
@@ -58,33 +58,44 @@ 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 succeeds, or in the case where we found no trusted
certificates for this particular GUN, we check the integrity of the root by
If the previous validation succeeds, 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.
However, if we do not have any current trusted certificates for this GUN, we
check if there are any pinned certificates specified in the trust_pinning section
of the notary client config. If this section specifies a Certs section with this
GUN, we attempt to validate that the certificates present in the downloaded root
file match the pinned ID.
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
a different certificate for that certificate, we return an ErrValidationFailed error.
If the Certs section is empty for this GUN, we check if the trust_pinning
section specifies a CA section specified in the config for this GUN. If so, we check
that the specified CA is valid and has signed a certificate included in the downloaded
root file. The specified CA can be a prefix for this GUN.
If both the Certs and CA configs do not match this GUN, we fall back to the TOFU
section in the config: if true, we trust certificates specified in the root for
this GUN. If later we see 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
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.
We shall call this: TOFUS.
Validation failure at any step will result in an ErrValidationFailed error.
*/
func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun string) error {
func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun string, trustPinning TrustPinConfig) error {
logrus.Debugf("entered ValidateRoot with dns: %s", gun)
signedRoot, err := data.RootFromSigned(root)
if err != nil {
return err
}
// Retrieve all the leaf certificates in root for which the CN matches the GUN
certsFromRoot, err := validRootLeafCerts(signedRoot, gun)
// Retrieve all the leaf and intermediate certificates in root for which the CN matches the GUN
allLeafCerts, allIntCerts := parseAllCerts(signedRoot)
certsFromRoot, err := validRootLeafCerts(allLeafCerts, gun)
if err != nil {
logrus.Debugf("error retrieving valid leaf certificates for: %s, %v", gun, err)
return &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}
@@ -101,23 +112,44 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin
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(trustedCerts) != 0 {
logrus.Debugf("found %d valid root certificates for %s: %s", len(trustedCerts), gun,
prettyFormatCertIDs(trustedCerts))
err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(trustedCerts))
err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(trustedCerts, allIntCerts))
if err != nil {
logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err)
return &ErrValidationFail{Reason: "failed to validate data with current trusted certificates"}
}
} else {
logrus.Debugf("found no currently valid root certificates for %s", gun)
logrus.Debugf("found no currently valid root certificates for %s, using trust_pinning config to bootstrap trust", gun)
trustPinCheckFunc, err := NewTrustPinChecker(trustPinning, gun)
if err != nil {
return &ErrValidationFail{Reason: err.Error()}
}
validPinnedCerts := []*x509.Certificate{}
for _, cert := range certsFromRoot {
certID, err := trustmanager.FingerprintCert(cert)
if err != nil {
continue
}
if ok := trustPinCheckFunc(cert, allIntCerts[certID]); !ok {
continue
}
validPinnedCerts = append(validPinnedCerts, cert)
}
if len(validPinnedCerts) == 0 {
return &ErrValidationFail{Reason: "unable to match any certificates to trust_pinning config"}
}
certsFromRoot = validPinnedCerts
}
// Validate the integrity of the new root (does it have valid signatures)
err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(certsFromRoot))
// Note that certsFromRoot is guaranteed to be unchanged only if we had prior cert data for this GUN or enabled TOFUS
// If we attempted to pin a certain certificate or CA, certsFromRoot could have been pruned accordingly
err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(certsFromRoot, allIntCerts))
if err != nil {
logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err)
return &ErrValidationFail{Reason: "failed to validate integrity of roots"}
@@ -165,9 +197,7 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin
// validRootLeafCerts returns a list of non-expired, non-sha1 certificates
// found in root whose Common-Names match the provided GUN. Note that this
// "validity" alone does not imply any measure of trust.
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)
func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string) ([]*x509.Certificate, error) {
var validLeafCerts []*x509.Certificate
// Go through every leaf certificate and check that the CN matches the gun
@@ -245,6 +275,11 @@ func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, m
logrus.Debugf("invalid chain due to leaf certificate missing or too many leaf certificates for keyID: %s", keyID)
continue
}
// If we found a leaf certificate, assert that the cert bundle started with a leaf
if decodedCerts[0].IsCA {
logrus.Debugf("invalid chain due to leaf certificate not being first certificate for keyID: %s", keyID)
continue
}
// Get the ID of the leaf certificate
leafCert := leafCertList[0]

View File

@@ -1,15 +1,23 @@
package certs
package trustpinning
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"io/ioutil"
"math/big"
"os"
"path/filepath"
"testing"
"text/template"
"time"
"github.com/docker/notary"
"github.com/docker/notary/cryptoservice"
"github.com/docker/notary/trustmanager"
@@ -32,6 +40,10 @@ const validIntermediateAndCertRSA = `LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUl
const signedRSARootTemplate = `{"signed":{"_type":"Root","consistent_snapshot":false,"expires":"2016-07-16T23:34:13.389129622-07:00","keys":{"1fc4fdc38f66558658c5c59b67f1716bdc6a74ef138b023ae5931db69f51d670":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8nIgzLigo5D47dWQe1IUjzHXxvyx0j/OL16VQymuloWsgVDxxT6+mH3CeviMAs+/McnEPE9exnm6SQGR5x3XMw=="}},"23c29cc372109c819e081bc953b7657d05e3f968f03c21d0d75ea457590f3d14":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEClUFVWkc85OQScfTQRS02VaLIEaeCmxdwYS/hcTLVoTxlFfRfs7HyalTwXGAGO79XZZS+koE6s8D0xGcCJQkLQ=="}},"49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292":{"keytype":"rsa-x509","keyval":{"private":null,"public":"{{.RootPem}}"}},"e3a5a4fdaf11ea1ec58f5efed6f3639b39cd4cfa1418c8b55c9a8c2447ace5d9":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgl3rzMPMEKhS1k/AX16MM4PdidpjJr+z4pj0Td+30QnpbOIARgpyR1PiFztU8BZlqG3cUazvFclr2q/xHvfrqw=="}}},"roles":{"root":{"keyids":["49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292"],"threshold":1},"snapshot":{"keyids":["23c29cc372109c819e081bc953b7657d05e3f968f03c21d0d75ea457590f3d14"],"threshold":1},"targets":{"keyids":["1fc4fdc38f66558658c5c59b67f1716bdc6a74ef138b023ae5931db69f51d670"],"threshold":1},"timestamp":{"keyids":["e3a5a4fdaf11ea1ec58f5efed6f3639b39cd4cfa1418c8b55c9a8c2447ace5d9"],"threshold":1}},"version":2},"signatures":[{"keyid":"49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292","method":"rsapss","sig":"YlZwtCj028Xc23+KHfj6govFEY6hMbBXO5HT20F0I5ZeIPb1l7OmkjEiwp9ZHusClY+QeqiP1CFh\n/AfCbv4tLanqMkXPtm8UJJ1hMZVq86coieB32PQDj9k6x1hErHzvPUbOzTRW2BQkFFMZFkLDAd06\npH8lmxyPLOhdkVE8qIT7sBCy/4bQIGfvEX6yCDz84MZdcLNX5B9mzGi9A7gDloh9IEZxA8UgoI18\nSYpv/fYeSZSqM/ws2G+kiELGgTWhcZ+gOlF7ArM/DOlcC/NYqcvY1ugE6Gn7G8opre6NOofdRp3w\n603A2rMMvYTwqKLY6oX/d+07A2+WGHXPUy5otCAybWOw2hIZ35Jjmh12g6Dc6Qk4K2zXwAgvWwBU\nWlT8MlP1Tf7f80jnGjh0aARlHI4LCxlYU5L/pCaYuHgynujvLuzoOuiiPfJv7sYvKoQ8UieE1w//\nHc8E6tWtV5G2FguKLurMoKZ9FBWcanDO0fg5AWuG3qcgUJdvh9acQ33EKer1fqBxs6LSAUWo8rDt\nQkg+b55AW0YBukAW9IAfMySQGAS2e3mHZ8nK/ijaygCRu7/P+NgKY9/zpmfL2xgcNslLcANcSOOt\nhiJS6yqYM9i9G0af0yw/TxAT4ntwjVm8u52UyR/hXIiUc/mjZcYRbSmJOHws902+i+Z/qv72knk="}]}`
const rootPubKeyID = "49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292"
const targetsPubKeyID = "1fc4fdc38f66558658c5c59b67f1716bdc6a74ef138b023ae5931db69f51d670"
func TestCertsToRemove(t *testing.T) {
// Get a few certificates to test with
cert1, err := trustmanager.LoadCertFromFile("../fixtures/secure.example.com.crt")
@@ -138,12 +150,12 @@ func TestValidateRoot(t *testing.T) {
// This call to ValidateRoot will succeed since we are using a valid PEM
// encoded certificate, and have no other certificates for this CN
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary")
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{})
require.NoError(t, err)
// This call to ValidateRoot will fail since we are passing in a dnsName that
// doesn't match the CN of the certificate.
err = ValidateRoot(certStore, &testSignedRoot, "diogomonica.com/notary")
err = ValidateRoot(certStore, &testSignedRoot, "diogomonica.com/notary", TrustPinConfig{})
require.Error(t, err, "An error was expected")
require.Equal(t, err, &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"})
@@ -157,7 +169,7 @@ func TestValidateRoot(t *testing.T) {
// Unmarshal our signedroot
json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot)
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary")
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{})
require.Error(t, err, "illegal base64 data at input byte")
//
@@ -170,7 +182,7 @@ func TestValidateRoot(t *testing.T) {
// Unmarshal our signedroot
json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot)
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary")
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{})
require.Error(t, err, "An error was expected")
require.Equal(t, err, &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"})
@@ -185,15 +197,16 @@ func TestValidateRoot(t *testing.T) {
// Unmarshal our signedroot
json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot)
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary")
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{})
require.Error(t, err, "An error was expected")
require.Equal(t, err, &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"})
//
// This call to ValidateRoot will succeed in getting to the TUF validation, since
// This call to ValidateRoot could succeed in getting to the TUF validation, since
// we are using a valid PEM encoded certificate chain of intermediate + leaf cert
// that are signed by a trusted root authority and the leaf cert has a correct CN.
// It will, however, fail to validate, because it has an invalid TUF signature
// It will, however, fail to validate, because the leaf cert does not precede the
// intermediate in the certificate bundle
//
// Execute our template deleting the old buffer first
signedRootBytes.Reset()
@@ -203,9 +216,445 @@ func TestValidateRoot(t *testing.T) {
// Unmarshal our signedroot
json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot)
err = ValidateRoot(certStore, &testSignedRoot, "secure.example.com")
err = ValidateRoot(certStore, &testSignedRoot, "secure.example.com", TrustPinConfig{})
require.Error(t, err, "An error was expected")
require.Equal(t, err, &ErrValidationFail{Reason: "failed to validate integrity of roots"})
require.Equal(t, err, &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"})
}
func TestValidateRootWithoutTOFUS(t *testing.T) {
var testSignedRoot data.Signed
var signedRootBytes bytes.Buffer
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
require.NoError(t, err, "failed to create a temporary directory: %s", err)
// Create a X509Store
trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir)
certStore, err := trustmanager.NewX509FilteredFileStore(
trustPath,
trustmanager.FilterCertsExpiredSha1,
)
require.NoError(t, err)
// Execute our template
templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate)
templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validPEMEncodedRSARoot})
// Unmarshal our signedroot
json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot)
// This call to ValidateRoot will fail since we are explicitly disabling TOFU and have no local certs
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{DisableTOFU: true})
require.Error(t, err)
}
func TestValidateRootWithPinnedCert(t *testing.T) {
var testSignedRoot data.Signed
var signedRootBytes bytes.Buffer
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
require.NoError(t, err, "failed to create a temporary directory: %s", err)
// Create a X509Store
trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir)
certStore, err := trustmanager.NewX509FilteredFileStore(
trustPath,
trustmanager.FilterCertsExpiredSha1,
)
require.NoError(t, err)
// Execute our template
templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate)
templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validPEMEncodedRSARoot})
// Unmarshal our signedroot
json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot)
// This call to ValidateRoot should succeed with the correct Cert ID (same as root public key ID)
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {rootPubKeyID}}, DisableTOFU: true})
require.NoError(t, err)
// purge the cert store to check another valid certs trust pinning
certStore.RemoveAll()
// This call to ValidateRoot should also succeed with the correct Cert ID (same as root public key ID), even though we passed an extra bad one
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {rootPubKeyID, "invalidID"}}, DisableTOFU: true})
require.NoError(t, err)
}
func TestValidateRootWithPinnerCertAndIntermediates(t *testing.T) {
now := time.Now()
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
pass := func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) {
return "password", false, nil
}
memStore := trustmanager.NewKeyMemoryStore(pass)
cs := cryptoservice.NewCryptoService(memStore)
// generate CA cert
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
require.NoError(t, err)
caTmpl := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: "notary testing CA",
},
NotBefore: now.Add(-time.Hour),
NotAfter: now.Add(time.Hour),
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 3,
}
caPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
_, err = x509.CreateCertificate(
rand.Reader,
&caTmpl,
&caTmpl,
caPrivKey.Public(),
caPrivKey,
)
// generate intermediate
intTmpl := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: "notary testing intermediate",
},
NotBefore: now.Add(-time.Hour),
NotAfter: now.Add(time.Hour),
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 2,
}
intPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
intCert, err := x509.CreateCertificate(
rand.Reader,
&intTmpl,
&caTmpl,
intPrivKey.Public(),
caPrivKey,
)
require.NoError(t, err)
// generate leaf
serialNumber, err = rand.Int(rand.Reader, serialNumberLimit)
require.NoError(t, err)
leafTmpl := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: "docker.io/notary/test",
},
NotBefore: now.Add(-time.Hour),
NotAfter: now.Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
BasicConstraintsValid: true,
}
leafPubKey, err := cs.Create("root", "docker.io/notary/test", data.ECDSAKey)
require.NoError(t, err)
leafPrivKey, _, err := cs.GetPrivateKey(leafPubKey.ID())
require.NoError(t, err)
signer := leafPrivKey.CryptoSigner()
leafCert, err := x509.CreateCertificate(
rand.Reader,
&leafTmpl,
&intTmpl,
signer.Public(),
intPrivKey,
)
rootBundleWriter := bytes.NewBuffer(nil)
pem.Encode(
rootBundleWriter,
&pem.Block{
Type: "CERTIFICATE",
Bytes: leafCert,
},
)
pem.Encode(
rootBundleWriter,
&pem.Block{
Type: "CERTIFICATE",
Bytes: intCert,
},
)
rootBundle := rootBundleWriter.Bytes()
ecdsax509Key := data.NewECDSAx509PublicKey(rootBundle)
otherKey, err := cs.Create("targets", "docker.io/notary/test", data.ED25519Key)
require.NoError(t, err)
root := data.SignedRoot{
Signatures: make([]data.Signature, 0),
Signed: data.Root{
SignedCommon: data.SignedCommon{
Type: "Root",
Expires: now.Add(time.Hour),
Version: 1,
},
Keys: map[string]data.PublicKey{
ecdsax509Key.ID(): ecdsax509Key,
otherKey.ID(): otherKey,
},
Roles: map[string]*data.RootRole{
"root": {
KeyIDs: []string{ecdsax509Key.ID()},
Threshold: 1,
},
"targets": {
KeyIDs: []string{otherKey.ID()},
Threshold: 1,
},
"snapshot": {
KeyIDs: []string{otherKey.ID()},
Threshold: 1,
},
"timestamp": {
KeyIDs: []string{otherKey.ID()},
Threshold: 1,
},
},
},
Dirty: true,
}
signedRoot, err := root.ToSigned()
require.NoError(t, err)
err = signed.Sign(cs, signedRoot, []data.PublicKey{ecdsax509Key}, 1, nil)
require.NoError(t, err)
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
require.NoError(t, err, "failed to create a temporary directory: %s", err)
// Create a X509Store
trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir)
certStore, err := trustmanager.NewX509FilteredFileStore(
trustPath,
trustmanager.FilterCertsExpiredSha1,
)
require.NoError(t, err)
err = ValidateRoot(
certStore,
signedRoot,
"docker.io/notary/test",
TrustPinConfig{
Certs: map[string][]string{
"docker.io/notary/test": {ecdsax509Key.ID()},
},
DisableTOFU: true,
},
)
require.NoError(t, err, "failed to validate certID with intermediate")
}
func TestValidateRootFailuresWithPinnedCert(t *testing.T) {
var testSignedRoot data.Signed
var signedRootBytes bytes.Buffer
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
require.NoError(t, err, "failed to create a temporary directory: %s", err)
// Create a X509Store
trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir)
certStore, err := trustmanager.NewX509FilteredFileStore(
trustPath,
trustmanager.FilterCertsExpiredSha1,
)
require.NoError(t, err)
// Execute our template
templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate)
templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validPEMEncodedRSARoot})
// Unmarshal our signedroot
json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot)
// This call to ValidateRoot should fail due to an incorrect cert ID
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {"ABSOLUTELY NOT A CERT ID"}}, DisableTOFU: true})
require.Error(t, err)
// This call to ValidateRoot should fail due to an empty cert ID
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {""}}, DisableTOFU: true})
require.Error(t, err)
// This call to ValidateRoot should fail due to an invalid GUN (even though the cert ID is correct), and TOFUS is set to false
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"not_a_gun": {rootPubKeyID}}, DisableTOFU: true})
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", TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {targetsPubKeyID}}, DisableTOFU: true})
require.Error(t, err)
// This call to ValidateRoot should succeed because we fall through to TOFUS because we have no matching GUNs under Certs
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"not_a_gun": {rootPubKeyID}}, DisableTOFU: false})
require.NoError(t, err)
}
func TestValidateRootWithPinnedCA(t *testing.T) {
var testSignedRoot data.Signed
var signedRootBytes bytes.Buffer
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
defer os.RemoveAll(tempBaseDir)
require.NoError(t, err, "failed to create a temporary directory: %s", err)
// Create a X509Store
trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir)
certStore, err := trustmanager.NewX509FilteredFileStore(
trustPath,
trustmanager.FilterCertsExpiredSha1,
)
require.NoError(t, err)
templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate)
templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validPEMEncodedRSARoot})
// Unmarshal our signedRoot
json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot)
// This call to ValidateRoot will fail because we have an invalid path for the CA
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{CA: map[string]string{"docker.com/notary": filepath.Join(tempBaseDir, "nonexistent")}})
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", TrustPinConfig{CA: map[string]string{"othergun": filepath.Join(tempBaseDir, "nonexistent")}, DisableTOFU: true})
require.Error(t, err)
// This call to ValidateRoot will succeed because we have no valid GUNs to use and we fall back to enabled TOFUS
err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{CA: map[string]string{"othergun": filepath.Join(tempBaseDir, "nonexistent")}, DisableTOFU: false})
require.NoError(t, err)
require.NoError(t, certStore.RemoveAll())
// 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")
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", TrustPinConfig{CA: map[string]string{"docker.com/notary": invalidCAFilepath}, DisableTOFU: true})
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", TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {"invalidID"}}, CA: map[string]string{"docker.com/notary": validCAFilepath}, DisableTOFU: true})
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")
require.NoError(t, err)
intermediateCert, err := trustmanager.LoadCertFromFile("../fixtures/intermediate-ca.crt")
require.NoError(t, err)
pemChainBytes, err := trustmanager.CertChainToPEM([]*x509.Certificate{leafCert, intermediateCert})
require.NoError(t, err)
newRootKey := data.NewPublicKey(data.RSAx509Key, pemChainBytes)
rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{newRootKey.ID()}, nil)
require.NoError(t, err)
testRoot, err := data.NewRoot(
map[string]data.PublicKey{newRootKey.ID(): newRootKey},
map[string]*data.RootRole{
data.CanonicalRootRole: &rootRole.RootRole,
data.CanonicalTimestampRole: &rootRole.RootRole,
data.CanonicalTargetsRole: &rootRole.RootRole,
data.CanonicalSnapshotRole: &rootRole.RootRole},
false,
)
require.NoError(t, err, "Failed to create new root")
keyReader, err := os.Open("../fixtures/notary-signer.key")
require.NoError(t, err, "could not open key file")
pemBytes, err := ioutil.ReadAll(keyReader)
require.NoError(t, err, "could not read key file")
privKey, err := trustmanager.ParsePEMPrivateKey(pemBytes, "")
require.NoError(t, err)
store, err := trustmanager.NewKeyFileStore(tempBaseDir, passphraseRetriever)
require.NoError(t, err)
cs := cryptoservice.NewCryptoService(store)
err = store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: "notary-signer"}, privKey)
require.NoError(t, err)
newTestSignedRoot, err := testRoot.ToSigned()
require.NoError(t, err)
err = signed.Sign(cs, newTestSignedRoot, []data.PublicKey{newRootKey}, 1, nil)
require.NoError(t, err)
// Check that we validate correctly against a pinned CA and provided bundle
err = ValidateRoot(certStore, newTestSignedRoot, "notary-signer", TrustPinConfig{CA: map[string]string{"notary-signer": validCAFilepath}, DisableTOFU: true})
require.NoError(t, err)
// Add an expired CA for the same gun to our previous pinned bundle, ensure that we still validate correctly
certStore.RemoveAll()
goodRootCABundle, err := trustmanager.LoadCertBundleFromFile(validCAFilepath)
require.NoError(t, err)
memKeyStore := trustmanager.NewKeyMemoryStore(passphraseRetriever)
cryptoService := cryptoservice.NewCryptoService(memKeyStore)
testPubKey, err := cryptoService.Create("root", "notary-signer", data.ECDSAKey)
require.NoError(t, err)
testPrivKey, _, err := memKeyStore.GetKey(testPubKey.ID())
require.NoError(t, err)
expiredCert, err := generateExpiredTestingCertificate(testPrivKey, "notary-signer")
require.NoError(t, err)
bundleWithExpiredCert, err := trustmanager.CertChainToPEM(append(goodRootCABundle, expiredCert))
require.NoError(t, err)
bundleWithExpiredCertPath := filepath.Join(tempBaseDir, "bundle_with_expired_cert.pem")
require.NoError(t, ioutil.WriteFile(bundleWithExpiredCertPath, bundleWithExpiredCert, 0644))
// Check that we validate correctly against a pinned CA and provided bundle
err = ValidateRoot(certStore, newTestSignedRoot, "notary-signer", TrustPinConfig{CA: map[string]string{"notary-signer": bundleWithExpiredCertPath}, DisableTOFU: true})
require.NoError(t, err)
certStore.RemoveAll()
testPubKey2, err := cryptoService.Create("root", "notary-signer", data.ECDSAKey)
require.NoError(t, err)
testPrivKey2, _, err := memKeyStore.GetKey(testPubKey2.ID())
require.NoError(t, err)
expiredCert2, err := generateExpiredTestingCertificate(testPrivKey2, "notary-signer")
require.NoError(t, err)
allExpiredCertBundle, err := trustmanager.CertChainToPEM([]*x509.Certificate{expiredCert, expiredCert2})
require.NoError(t, err)
allExpiredCertPath := filepath.Join(tempBaseDir, "all_expired_cert.pem")
require.NoError(t, ioutil.WriteFile(allExpiredCertPath, allExpiredCertBundle, 0644))
// Now only use expired certs in the bundle, we should fail
err = ValidateRoot(certStore, newTestSignedRoot, "notary-signer", TrustPinConfig{CA: map[string]string{"notary-signer": allExpiredCertPath}, DisableTOFU: true})
require.Error(t, err)
// Add a CA cert for a that won't validate against the root leaf certificate
certStore.RemoveAll()
testPubKey3, err := cryptoService.Create("root", "notary-signer", data.ECDSAKey)
require.NoError(t, err)
testPrivKey3, _, err := memKeyStore.GetKey(testPubKey3.ID())
require.NoError(t, err)
validCert, err := cryptoservice.GenerateCertificate(testPrivKey3, "notary-signer", time.Now(), time.Now().AddDate(1, 0, 0))
require.NoError(t, err)
bundleWithWrongCert, err := trustmanager.CertChainToPEM([]*x509.Certificate{validCert})
require.NoError(t, err)
bundleWithWrongCertPath := filepath.Join(tempBaseDir, "bundle_with_expired_cert.pem")
require.NoError(t, ioutil.WriteFile(bundleWithWrongCertPath, bundleWithWrongCert, 0644))
err = ValidateRoot(certStore, newTestSignedRoot, "notary-signer", TrustPinConfig{CA: map[string]string{"notary-signer": bundleWithWrongCertPath}, DisableTOFU: true})
require.Error(t, err)
}
// TestValidateSuccessfulRootRotation runs through a full root certificate rotation
@@ -246,7 +695,7 @@ func filestoreWithTwoCerts(t *testing.T, gun, keyAlg string) (
key, _, err := fileKeyStore.GetKey(pubKey.ID())
require.NoError(t, err)
cert, err := cryptoservice.GenerateTestingCertificate(key.CryptoSigner(), gun)
cert, err := generateTestingCertificate(key, gun)
require.NoError(t, err)
certificates[i] = cert
@@ -296,7 +745,7 @@ func testValidateSuccessfulRootRotation(t *testing.T, keyAlg, rootKeyType string
// This call to ValidateRoot will succeed since we are using a valid PEM
// encoded certificate, and have no other certificates for this CN
err = ValidateRoot(certStore, signedTestRoot, gun)
err = ValidateRoot(certStore, signedTestRoot, gun, TrustPinConfig{})
require.NoError(t, err)
// Finally, validate the only trusted certificate that exists is the new one
@@ -357,7 +806,7 @@ func testValidateRootRotationMissingOrigSig(t *testing.T, keyAlg, rootKeyType st
// This call to ValidateRoot will succeed since we are using a valid PEM
// encoded certificate, and have no other certificates for this CN
err = ValidateRoot(certStore, signedTestRoot, gun)
err = ValidateRoot(certStore, signedTestRoot, gun, TrustPinConfig{})
require.Error(t, err, "insuficient signatures on root")
// Finally, validate the only trusted certificate that exists is still
@@ -421,7 +870,7 @@ func testValidateRootRotationMissingNewSig(t *testing.T, keyAlg, rootKeyType str
// This call to ValidateRoot will succeed since we are using a valid PEM
// encoded certificate, and have no other certificates for this CN
err = ValidateRoot(certStore, signedTestRoot, gun)
err = ValidateRoot(certStore, signedTestRoot, gun, TrustPinConfig{})
require.Error(t, err, "insuficient signatures on root")
// Finally, validate the only trusted certificate that exists is still
@@ -430,3 +879,13 @@ func testValidateRootRotationMissingNewSig(t *testing.T, keyAlg, rootKeyType str
require.Len(t, certificates, 1)
require.Equal(t, certificates[0], origRootCert)
}
func generateTestingCertificate(rootKey data.PrivateKey, gun string) (*x509.Certificate, error) {
startTime := time.Now()
return cryptoservice.GenerateCertificate(rootKey, gun, startTime, startTime.AddDate(10, 0, 0))
}
func generateExpiredTestingCertificate(rootKey data.PrivateKey, gun string) (*x509.Certificate, error) {
startTime := time.Now().AddDate(-10, 0, 0)
return cryptoservice.GenerateCertificate(rootKey, gun, startTime, startTime.AddDate(1, 0, 0))
}

118
trustpinning/trustpin.go Normal file
View File

@@ -0,0 +1,118 @@
package trustpinning
import (
"crypto/x509"
"fmt"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/tuf/utils"
"strings"
)
// TrustPinConfig represents the configuration under the trust_pinning section of the config file
// This struct represents the preferred way to bootstrap trust for this repository
type TrustPinConfig struct {
CA map[string]string
Certs map[string][]string
DisableTOFU bool
}
type trustPinChecker struct {
gun string
config TrustPinConfig
pinnedCAPool *x509.CertPool
pinnedCertIDs []string
}
// CertChecker is a function type that will be used to check leaf certs against pinned trust
type CertChecker func(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool
// NewTrustPinChecker returns a new certChecker function from a TrustPinConfig for a GUN
func NewTrustPinChecker(trustPinConfig TrustPinConfig, gun string) (CertChecker, error) {
t := trustPinChecker{gun: gun, config: trustPinConfig}
// Determine the mode, and if it's even valid
if pinnedCerts, ok := trustPinConfig.Certs[gun]; ok {
t.pinnedCertIDs = pinnedCerts
return t.certsCheck, nil
}
if caFilepath, err := getPinnedCAFilepathByPrefix(gun, trustPinConfig); err == nil {
// 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 nil, fmt.Errorf("could not load root cert from CA path")
}
// Now only consider certificates that are direct children from this CA cert chain
caRootPool := x509.NewCertPool()
for _, caCert := range caCerts {
if err = trustmanager.ValidateCertificate(caCert); err != nil {
continue
}
caRootPool.AddCert(caCert)
}
// If we didn't have any valid CA certs, error out
if len(caRootPool.Subjects()) == 0 {
return nil, fmt.Errorf("invalid CA certs provided")
}
t.pinnedCAPool = caRootPool
return t.caCheck, nil
}
if !trustPinConfig.DisableTOFU {
return t.tofusCheck, nil
}
return nil, fmt.Errorf("invalid trust pinning specified")
}
func (t trustPinChecker) certsCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool {
// reconstruct the leaf + intermediate cert chain, which is bundled as {leaf, intermediates...},
// in order to get the matching id in the root file
leafCertID, err := trustmanager.FingerprintCert(leafCert)
if err != nil {
return false
}
rootKeys := trustmanager.CertsToKeys([]*x509.Certificate{leafCert}, map[string][]*x509.Certificate{leafCertID: intCerts})
for keyID := range rootKeys {
if utils.StrSliceContains(t.pinnedCertIDs, keyID) {
return true
}
}
return false
}
func (t trustPinChecker) caCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool {
// Use intermediate certificates included in the root TUF metadata for our validation
caIntPool := x509.NewCertPool()
for _, intCert := range intCerts {
caIntPool.AddCert(intCert)
}
// Attempt to find a valid certificate chain from the leaf cert to CA root
// Use this certificate if such a valid chain exists (possibly using intermediates)
if _, err := leafCert.Verify(x509.VerifyOptions{Roots: t.pinnedCAPool, Intermediates: caIntPool}); err == nil {
return true
}
return false
}
func (t trustPinChecker) tofusCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool {
return true
}
// 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 getPinnedCAFilepathByPrefix(gun string, t TrustPinConfig) (string, error) {
specificGUN := ""
specificCAFilepath := ""
foundCA := false
for gunPrefix, caFilepath := range t.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

@@ -6,6 +6,7 @@ import (
"encoding/pem"
"io"
"testing"
"time"
"github.com/docker/go/canonical/json"
"github.com/docker/notary/cryptoservice"
@@ -211,7 +212,7 @@ func TestSignWithX509(t *testing.T) {
require.NoError(t, err)
// make a RSA x509 key
cert, err := cryptoservice.GenerateTestingCertificate(privKey.CryptoSigner(), "test")
cert, err := cryptoservice.GenerateCertificate(privKey, "test", time.Now(), time.Now().AddDate(10, 0, 0))
require.NoError(t, err)
tufRSAx509Key := trustmanager.CertToKey(cert)

View File

@@ -36,7 +36,6 @@ func VerifyRoot(s *data.Signed, minVersion int, keys map[string]data.PublicKey)
if err != nil {
return err
}
for _, sig := range s.Signatures {
// method lookup is consistent due to Unmarshal JSON doing lower case for us.
method := sig.Method
@@ -45,7 +44,6 @@ func VerifyRoot(s *data.Signed, minVersion int, keys map[string]data.PublicKey)
logrus.Debugf("continuing b/c signing method is not supported for verify root: %s\n", sig.Method)
continue
}
key, ok := keys[sig.KeyID]
if !ok {
logrus.Debugf("continuing b/c signing key isn't present in keys: %s\n", sig.KeyID)

View File

@@ -98,7 +98,7 @@ func HashedPaths(path string, hashes data.Hashes) []string {
// CanonicalKeyID returns the ID of the public bytes version of a TUF key.
// On regular RSA/ECDSA TUF keys, this is just the key ID. On X509 RSA/ECDSA
// TUF keys, this is the key ID of the public key part of the key.
// TUF keys, this is the key ID of the public key part of the key in the leaf cert
func CanonicalKeyID(k data.PublicKey) (string, error) {
switch k.Algorithm() {
case data.ECDSAx509Key, data.RSAx509Key: