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

Implement root certificate rotation in NotaryRepository

NotaryRepository can now list root certificates, and
generate new versions (as changelists to be applied
on Publish).

This is a pretty mechanical encapsulation of the
root certificate rotation support in Repo.AddBaseKeys
and Repo.RemoveBaseKeys. The only slightly interesting
part is ListRootCert, which requires on-line access
to ensure fresh data, and depends on CertStore doing
some verification for us.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
This commit is contained in:
Miloslav Trmač
2016-02-01 03:07:38 +01:00
committed by Ying Li
parent 34aa149cbc
commit d835fbbca2
4 changed files with 243 additions and 5 deletions

View File

@@ -38,8 +38,10 @@ type TufChange struct {
// TufRootData represents a modification of the keys associated
// with a role that appears in the root.json
type TufRootData struct {
Keys data.KeyList `json:"keys"`
RoleName string `json:"role"`
ReplaceKeys data.KeyList `json:"keys"`
AddKeys data.KeyList `json:"add_keys, omitempty"`
RemoveKeys []string `json:"remove_keys,omitempty"`
RoleName string `json:"role"`
}
// NewTufChange initializes a tufChange object

View File

@@ -2,6 +2,7 @@ package client
import (
"bytes"
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
@@ -936,8 +937,8 @@ func (r *NotaryRepository) rootFileKeyChange(cl changelist.Changelist, role, act
kl := make(data.KeyList, 0, 1)
kl = append(kl, key)
meta := changelist.TufRootData{
RoleName: role,
Keys: kl,
RoleName: role,
ReplaceKeys: kl,
}
metaJSON, err := json.Marshal(meta)
if err != nil {
@@ -977,3 +978,92 @@ func (r *NotaryRepository) DeleteTrustData() error {
}
return nil
}
// ListRootCerts returns current trusted root certificates.
// (In particular, not old certificates which we have already
// rotated from but are still using for signing to allow rollover,
// even if they are still trusted locally.)
func (r *NotaryRepository) ListRootCerts() ([]*x509.Certificate, error) {
if err := r.Update(false); err != nil {
return nil, err
}
keyIDs := r.tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs
certs := make([]*x509.Certificate, 0, len(keyIDs))
for _, keyID := range keyIDs {
// r.tufRepo contains all data from root.json, which may include irrelevant
// certificates. This is handled by certs.ValidateRoot, which adds
// only the real trusted certificates into the store. So, this lookup by ID is not
// just a dumb way to not parse the certificate, the handling of ErrNoCertificatesFound
// ensures we only get the intersection of (certificates in current root.json) with
// (locally trusted certificates). The rotation code in certs.ValidateRoot ensures
// that we locally store a copy of all relevant certificates.
cert, err := r.CertStore.GetCertificateByCertID(keyID)
if err == nil {
certs = append(certs, cert)
} else if _, ok := err.(*trustmanager.ErrNoCertificatesFound); !ok {
return nil, err
}
}
return certs, nil
}
// RotateRootCert generates a new certificate to replace oldCert and adds
// a changelist entry to update the root role with this certificate replacement
// shen r.Publish() is called.
func (r *NotaryRepository) RotateRootCert(oldCert *x509.Certificate) error {
oldCertX509PublicKey := trustmanager.CertToKey(oldCert)
if oldCertX509PublicKey == nil {
return fmt.Errorf("invalid format for root key: %s", oldCert.PublicKeyAlgorithm)
}
rootKeyID, err := trustmanager.X509PublicKeyID(oldCertX509PublicKey)
if err != nil {
return err
}
privKey, _, err := r.CryptoService.GetPrivateKey(rootKeyID)
if err != nil {
return err
}
// Hard-coded policy: the generated certificate expires in 10 years.
startTime := time.Now()
newCert, err := cryptoservice.GenerateCertificate(privKey, r.gun, startTime, startTime.AddDate(10, 0, 0))
if err != nil {
return err
}
newCertX509PublicKey := trustmanager.CertToKey(newCert)
if newCertX509PublicKey == nil {
return fmt.Errorf("cannot use regenerated certificate?! format %s", newCert.PublicKeyAlgorithm)
}
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
defer cl.Close()
// Not changelist.ActionCreate/ReplaceKeys, which would drop other certificates.
meta := changelist.TufRootData{
RoleName: data.CanonicalRootRole,
AddKeys: []data.PublicKey{newCertX509PublicKey},
RemoveKeys: []string{oldCertX509PublicKey.ID()},
}
metaJSON, err := json.Marshal(meta)
if err != nil {
return err
}
c := changelist.NewTufChange(
changelist.ActionUpdate,
changelist.ScopeRoot,
changelist.TypeRootRole,
data.CanonicalRootRole,
metaJSON,
)
return cl.Add(c)
// Note that we don't need to worry about updating r.CertStore (and synchronizing
// this with the r.Publish() call; after we push the new root.json to the server,
// calls to r.Update() will cause this client to rotate certificates in r.CertStore
// just as other clients do.
}

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
regJson "encoding/json"
"fmt"
@@ -3339,3 +3340,134 @@ func TestListRoles(t *testing.T) {
require.NoError(t, err)
require.Len(t, rolesWithSigs, len(data.BaseRoles)+2)
}
func logRepoTrustRoot(t *testing.T, prefix string, repo *NotaryRepository) {
logrus.Debugf("==== %s", prefix)
root := repo.tufRepo.Root
logrus.Debugf("Root signatures:")
for _, s := range root.Signatures {
logrus.Debugf("\t%s", s.KeyID)
}
logrus.Debugf("Valid root keys:")
for _, k := range root.Signed.Roles[data.CanonicalRootRole].KeyIDs {
logrus.Debugf("\t%s", k)
}
logrus.Debugf("All trusted certs:")
certs, err := repo.CertStore.GetCertificatesByCN(repo.gun)
require.NoError(t, err)
for _, cert := range certs {
id, err := trustmanager.FingerprintCert(cert)
require.NoError(t, err)
logrus.Debugf("\t%s", id)
}
}
// ID of the (only) certificate trusted by the root role metadata
func rootRoleCertID(t *testing.T, repo *NotaryRepository) string {
rootKeys := repo.tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs
require.Len(t, rootKeys, 1)
return rootKeys[0]
}
func verifyOnlyTrustedCertificate(t *testing.T, repo *NotaryRepository, certID string) {
certs, err := repo.CertStore.GetCertificatesByCN(repo.gun)
require.NoError(t, err)
require.Len(t, certs, 1)
id, err := trustmanager.FingerprintCert(certs[0])
require.NoError(t, err)
require.Equal(t, certID, id)
}
func TestRotateRootCert(t *testing.T) {
ts := fullTestServer(t)
defer ts.Close()
// Set up author's view of the repo and publish first version.
authorRepo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(authorRepo.baseDir)
err := authorRepo.Publish()
require.NoError(t, err)
oldRootCertID := rootRoleCertID(t, authorRepo)
// Initialize an user, using the original certificate.
userRepo, _ := newRepoToTestRepo(t, authorRepo, true)
defer os.RemoveAll(userRepo.baseDir)
err = userRepo.Update(false)
require.NoError(t, err)
// Rotate root certificate.
certs, err := authorRepo.ListRootCerts()
require.NoError(t, err)
var cert *x509.Certificate
require.Len(t, certs, 1)
// Just get the only certificate in there.
for _, c := range certs {
cert = c
}
logRepoTrustRoot(t, "original", authorRepo)
err = authorRepo.RotateRootCert(cert)
require.NoError(t, err)
logRepoTrustRoot(t, "post-rotate", authorRepo)
// Set up a target to verify the repo is actually usable.
publishedTarget := addTarget(t, authorRepo, "current", "../fixtures/intermediate-ca.crt")
// Publish the rotation
logRepoTrustRoot(t, "pre-publish", authorRepo)
err = authorRepo.Publish()
require.NoError(t, err)
logRepoTrustRoot(t, "post-publish", authorRepo)
newRootCertID := rootRoleCertID(t, authorRepo)
require.NotEqual(t, oldRootCertID, newRootCertID)
// Verify the user can use the rotated repo, and see the added target.
receivedTarget, err := userRepo.GetTargetByName("current")
require.NoError(t, err)
require.Equal(t, newRootCertID, rootRoleCertID(t, userRepo))
require.True(t, reflect.DeepEqual(*publishedTarget, receivedTarget.Target), "target does not match")
logRepoTrustRoot(t, "client", userRepo)
// Verify that clients initialized post-rotation can use the repo, and use
// the new certificate immediately.
freshUserRepo, _ := newRepoToTestRepo(t, authorRepo, true)
defer os.RemoveAll(freshUserRepo.baseDir)
receivedTarget, err = freshUserRepo.GetTargetByName("current")
require.NoError(t, err)
require.Equal(t, newRootCertID, rootRoleCertID(t, freshUserRepo))
verifyOnlyTrustedCertificate(t, freshUserRepo, newRootCertID)
require.True(t, reflect.DeepEqual(*publishedTarget, receivedTarget.Target), "target does not match")
logRepoTrustRoot(t, "fresh client", freshUserRepo)
// NotaryRepository.Update's handling of certificate rotation is weird:
//
// On every run, NotaryRepository.bootstrapClient rotates the trusted certificates
// based on CACHED root data.
// Then the client calls Repo.Update, which fetches a new timestmap,
// notices an updated root.json, validates it and stores it into the cache.
//
// So, the locally trusted certificates are rotated only on the SECOND call of
// NotaryRepository.Update after the rotation is pushed to the server.
//
// This would be nice to fix eventually (breaking down the NotaryRepository.Update
// / Repo.Update separation which causes this), but for now, just ensure that the
// second update does result in updated certificates.
// Verify that the author eventually rotates to the new certificate.
err = authorRepo.Update(false)
require.NoError(t, err)
logRepoTrustRoot(t, "author refresh 1", authorRepo)
require.Equal(t, newRootCertID, rootRoleCertID(t, authorRepo))
// verifyOnlyTrustedCertificate(t, authorRepo, newRootCertID)
err = authorRepo.Update(false)
require.NoError(t, err)
logRepoTrustRoot(t, "author refresh 2", authorRepo)
require.Equal(t, newRootCertID, rootRoleCertID(t, authorRepo))
verifyOnlyTrustedCertificate(t, authorRepo, newRootCertID)
// Verify that the user initialized with the original certificate eventually
// rotates to the new certificate.
err = userRepo.Update(false)
require.NoError(t, err)
logRepoTrustRoot(t, "user refresh 1", userRepo)
require.Equal(t, newRootCertID, rootRoleCertID(t, userRepo))
}

View File

@@ -180,7 +180,21 @@ func applyRootRoleChange(repo *tuf.Repo, c changelist.Change) error {
if err != nil {
return err
}
err = repo.ReplaceBaseKeys(d.RoleName, d.Keys...)
err = repo.ReplaceBaseKeys(d.RoleName, d.ReplaceKeys...)
if err != nil {
return err
}
case changelist.ActionUpdate:
d := &changelist.TufRootData{}
err := json.Unmarshal(c.Content(), d)
if err != nil {
return err
}
err = repo.AddBaseKeys(d.RoleName, d.AddKeys...)
if err != nil {
return err
}
err = repo.RemoveBaseKeys(d.RoleName, d.RemoveKeys...)
if err != nil {
return err
}