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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 := ¬aryCommander{
|
||||
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 = ¬aryCommander{
|
||||
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 = ¬aryCommander{
|
||||
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 = ¬aryCommander{
|
||||
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 = ¬aryCommander{
|
||||
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"])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
BIN
docs/images/trust-pinning-flow.png
Normal file
BIN
docs/images/trust-pinning-flow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]
|
||||
@@ -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
118
trustpinning/trustpin.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user