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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user