Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for initializing repository with root certificate #1144

Merged
merged 8 commits into from
May 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 121 additions & 22 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ func NewTarget(targetName, targetPath string, targetCustom *canonicaljson.RawMes
return &Target{Name: targetName, Hashes: meta.Hashes, Length: meta.Length, Custom: targetCustom}, nil
}

// rootCertKey generates the corresponding certificate for the private key given the privKey and repo's GUN
func rootCertKey(gun data.GUN, privKey data.PrivateKey) (data.PublicKey, error) {
// Hard-coded policy: the generated certificate expires in 10 years.
startTime := time.Now()
Expand All @@ -168,24 +169,14 @@ func rootCertKey(gun data.GUN, privKey data.PrivateKey) (data.PublicKey, error)

x509PublicKey := utils.CertToKey(cert)
if x509PublicKey == nil {
return nil, fmt.Errorf(
"cannot use regenerated certificate: format %s", cert.PublicKeyAlgorithm)
return nil, fmt.Errorf("cannot generate public key from private key with id: %v and algorithm: %v", privKey.ID(), privKey.Algorithm())
}

return x509PublicKey, nil
}

// Initialize creates a new repository by using rootKey as the root Key for the
// TUF repository. The server must be reachable (and is asked to generate a
// timestamp key and possibly other serverManagedRoles), but the created repository
// result is only stored on local disk, not published to the server. To do that,
// use r.Publish() eventually.
func (r *NotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error {

privKeys, err := getAllPrivKeys(rootKeyIDs, r.CryptoService)
if err != nil {
return err
}
// initialize initializes the notary repository with a set of rootkeys, root certificates and roles.
func (r *NotaryRepository) initialize(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error {

// currently we only support server managing timestamps and snapshots, and
// nothing else - timestamps are always managed by the server, and implicit
Expand Down Expand Up @@ -214,17 +205,21 @@ func (r *NotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ..
}
}

rootKeys := make([]data.PublicKey, 0, len(privKeys))
for _, privKey := range privKeys {
rootKey, err := rootCertKey(r.gun, privKey)
if err != nil {
return err
}
rootKeys = append(rootKeys, rootKey)
// gets valid public keys corresponding to the rootKeyIDs or generate if necessary
var publicKeys []data.PublicKey
var err error
if len(rootCerts) == 0 {
publicKeys, err = r.createNewPublicKeyFromKeyIDs(rootKeyIDs)
} else {
publicKeys, err = r.publicKeysOfKeyIDs(rootKeyIDs, rootCerts)
}
if err != nil {
return err
}

//initialize repo with public keys
rootRole, targetsRole, snapshotRole, timestampRole, err := r.initializeRoles(
rootKeys,
publicKeys,
locallyManagedKeys,
remotelyManagedKeys,
)
Expand Down Expand Up @@ -256,6 +251,111 @@ func (r *NotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ..
return r.saveMetadata(serverManagesSnapshot)
}

// createNewPublicKeyFromKeyIDs generates a set of public keys corresponding to the given list of
// key IDs existing in the repository's CryptoService.
// the public keys returned are ordered to correspond to the keyIDs
func (r *NotaryRepository) createNewPublicKeyFromKeyIDs(keyIDs []string) ([]data.PublicKey, error) {
publicKeys := []data.PublicKey{}

privKeys, err := getAllPrivKeys(keyIDs, r.CryptoService)
if err != nil {
return nil, err
}

for _, privKey := range privKeys {
rootKey, err := rootCertKey(r.gun, privKey)
if err != nil {
return nil, err
}
publicKeys = append(publicKeys, rootKey)
}
return publicKeys, nil
}

// publicKeysOfKeyIDs confirms that the public key and private keys (by Key IDs) forms valid, strictly ordered key pairs
// (eg. keyIDs[0] must match pubKeys[0] and keyIDs[1] must match certs[1] and so on).
// Or throw error when they mismatch.
func (r *NotaryRepository) publicKeysOfKeyIDs(keyIDs []string, pubKeys []data.PublicKey) ([]data.PublicKey, error) {
if len(keyIDs) != len(pubKeys) {
err := fmt.Errorf("require matching number of keyIDs and public keys but got %d IDs and %d public keys", len(keyIDs), len(pubKeys))
return nil, err
}

if err := matchKeyIdsWithPubKeys(r, keyIDs, pubKeys); err != nil {
return nil, fmt.Errorf("could not obtain public key from IDs: %v", err)
}
return pubKeys, nil
}

// matchKeyIdsWithPubKeys validates that the private keys (represented by their IDs) and the public keys
// forms matching key pairs
func matchKeyIdsWithPubKeys(r *NotaryRepository, ids []string, pubKeys []data.PublicKey) error {
for i := 0; i < len(ids); i++ {
privKey, _, err := r.CryptoService.GetPrivateKey(ids[i])
if err != nil {
return fmt.Errorf("could not get the private key matching id %v: %v", ids[i], err)
}

pubKey := pubKeys[i]
err = signed.VerifyPublicKeyMatchesPrivateKey(privKey, pubKey)
if err != nil {
return err
}
}
return nil
}

// Initialize creates a new repository by using rootKey as the root Key for the
// TUF repository. The server must be reachable (and is asked to generate a
// timestamp key and possibly other serverManagedRoles), but the created repository
// result is only stored on local disk, not published to the server. To do that,
// use r.Publish() eventually.
func (r *NotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error {
return r.initialize(rootKeyIDs, nil, serverManagedRoles...)
}

type errKeyNotFound struct{}

func (errKeyNotFound) Error() string {
return fmt.Sprintf("cannot find matching private key id")
}

// keyExistsInList returns the id of the private key in ids that matches the public key
// otherwise return empty string
func keyExistsInList(cert data.PublicKey, ids map[string]bool) error {
pubKeyID, err := utils.CanonicalKeyID(cert)
if err != nil {
return fmt.Errorf("failed to obtain the public key id from the given certificate: %v", err)
}
if _, ok := ids[pubKeyID]; ok {
return nil
}
return errKeyNotFound{}
}

// InitializeWithCertificate initializes the repository with root keys and their corresponding certificates
func (r *NotaryRepository) InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey,
nRepo *NotaryRepository, serverManagedRoles ...data.RoleName) error {

// If we explicitly pass in certificate(s) but not key, then look keys up using certificate
if len(rootKeyIDs) == 0 && len(rootCerts) != 0 {
rootKeyIDs = []string{}
availableRootKeyIDs := make(map[string]bool)
for _, k := range nRepo.CryptoService.ListKeys(data.CanonicalRootRole) {
availableRootKeyIDs[k] = true
}

for _, cert := range rootCerts {
if err := keyExistsInList(cert, availableRootKeyIDs); err != nil {
return fmt.Errorf("error initializing repository with certificate: %v", err)
}
keyID, _ := utils.CanonicalKeyID(cert)
rootKeyIDs = append(rootKeyIDs, keyID)
}
}
return r.initialize(rootKeyIDs, rootCerts, serverManagedRoles...)
}

func (r *NotaryRepository) initializeRoles(rootKeys []data.PublicKey, localRoles, remoteRoles []data.RoleName) (
root, targets, snapshot, timestamp data.BaseRole, err error) {
root = data.NewBaseRole(
Expand Down Expand Up @@ -356,7 +456,6 @@ func addChange(cl changelist.Changelist, c changelist.Change, roles ...data.Role
// in the repository when the changelist gets applied at publish time.
// If roles are unspecified, the default role is "targets"
func (r *NotaryRepository) AddTarget(target *Target, roles ...data.RoleName) error {

if len(target.Hashes) == 0 {
return fmt.Errorf("no hashes specified for target \"%s\"", target.Name)
}
Expand Down
159 changes: 139 additions & 20 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package client

import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"fmt"
"io/ioutil"
Expand All @@ -14,15 +16,13 @@ import (
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
"time"

"github.com/Sirupsen/logrus"
ctxu "github.com/docker/distribution/context"
"github.com/docker/go/canonical/json"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"

"github.com/docker/notary"
"github.com/docker/notary/client/changelist"
"github.com/docker/notary/cryptoservice"
Expand All @@ -36,6 +36,7 @@ import (
"github.com/docker/notary/tuf/signed"
"github.com/docker/notary/tuf/utils"
"github.com/docker/notary/tuf/validation"
"github.com/stretchr/testify/require"
)

const password = "passphrase"
Expand Down Expand Up @@ -186,8 +187,7 @@ func initializeRepo(t *testing.T, rootType, gun, url string,
}

// Creates a new repository and adds a root key. Returns the repo and key ID.
func createRepoAndKey(t *testing.T, rootType, tempBaseDir, gun, url string) (
*NotaryRepository, *passRoleRecorder, string) {
func createRepoAndKey(t *testing.T, rootType, tempBaseDir, gun, url string) (*NotaryRepository, *passRoleRecorder, string) {

rec := newRoleRecorder()
repo, err := NewFileCachedNotaryRepository(
Expand Down Expand Up @@ -305,28 +305,147 @@ func TestInitRepositoryManagedRolesIncludingTimestamp(t *testing.T) {
rec.requireCreated(t, []string{data.CanonicalTargetsRole.String(), data.CanonicalSnapshotRole.String()})
}

func TestInitRepositoryMultipleRootKeys(t *testing.T) {
func TestInitRepositoryWithCerts(t *testing.T) {
testCases := []struct {
name string
extraKeys int // the number of extra keys in addition the the first key
numberOfCerts int // initializing with certificates ?
expectedError string // error message
requiredSigningRootKeys int
unmatchedKeyPair bool // true when testing unmatched key pairs
noKeys bool // true when supplying only certificates
}{
{
name: "init with multiple root keys",
extraKeys: 1,
numberOfCerts: 0,
requiredSigningRootKeys: 2,
},
{
name: "1 key and 1 cert",
extraKeys: 0,
numberOfCerts: 1,
requiredSigningRootKeys: 1,
},
{
name: "unmatched key pairs: 1 key and 1 cert",
extraKeys: 1,
numberOfCerts: 2,
expectedError: "should not be able to initialize with non-matching keys",
unmatchedKeyPair: true,
},
{
name: "different number of keys and certs: 2 key, 1 certs",
extraKeys: 1,
numberOfCerts: 1,
expectedError: "should not be able to initialize with different number of keys and certs",
},
{
name: "testing with 1 cert with its private key in cryptoservice",
noKeys: true,
extraKeys: 0,
numberOfCerts: 1,
},
}

gun := "docker.com/notary"

for _, tc := range testCases {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory")
defer os.RemoveAll(tempBaseDir)
ts, _, _ := simpleTestServer(t)
defer ts.Close()

// create repo and first key
repo, rec, kid := createRepoAndKey(t, data.ECDSAKey, tempBaseDir, gun, ts.URL)
pubKeyIDs := []string{kid}

//create extra key pairs if necessary
for i := 0; i < tc.extraKeys; i++ {
key, err := repo.CryptoService.Create(data.CanonicalRootRole, repo.gun, data.ECDSAKey)
require.NoError(t, err, "error creating %v-th key: %v", i, err)
pubKeyIDs = append(pubKeyIDs, key.ID())
}

// assign pubKeys if necessary
var pubKeys []data.PublicKey
for i := 0; i < tc.numberOfCerts; i++ {
pubKeys = append(pubKeys, repo.CryptoService.GetKey(pubKeyIDs[i]))
}

if !strings.Contains(tc.name, "unmatched key pairs") {
iDs := pubKeyIDs[:1+tc.extraKeys] // use only the correct number of root key ids

if tc.noKeys { // case : 0 keys 1 cert
iDs = []string{}
}

err = repo.initialize(iDs, pubKeys, data.CanonicalTimestampRole)
if len(iDs) == len(pubKeys) || // case: 2 keys 2 certs
(len(iDs) != 0 && len(pubKeys) == 0) || // case: 1 key and 0 cert
(len(iDs) == 0 && len(pubKeys) != 0) { // case: 0 keys and 1 cert

require.NoError(t, err, "initialize returns an error")
rec.requireCreated(t, []string{data.CanonicalTargetsRole.String(), data.CanonicalSnapshotRole.String()})
require.Len(t, repo.tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs, tc.requiredSigningRootKeys)
return
}
// implicit else case: 2 keys 1 cert
} else { // unmatched key pairs case
err = repo.initialize(pubKeyIDs[1:], pubKeys[:1])
}
require.Error(t, err, tc.expectedError, tc.name)
require.Nil(t, repo.tufRepo)
}
}

func TestMatchKeyIDsWithPublicKeys(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory")
defer os.RemoveAll(tempBaseDir)

ts, _, _ := simpleTestServer(t)
ts, _, _ := simpleTestServer(t, data.CanonicalSnapshotRole.String())
defer ts.Close()

repo, rec, rootPubKeyID := createRepoAndKey(
t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL)
rootPubKey2, err := repo.CryptoService.Create(data.CanonicalRootRole, repo.gun, data.ECDSAKey)
require.NoError(t, err, "error generating second root key: %s", err)

err = repo.Initialize([]string{rootPubKeyID, rootPubKey2.ID()}, data.CanonicalTimestampRole)
require.NoError(t, err)

// generates the target role, the snapshot role
rec.requireCreated(t, []string{data.CanonicalTargetsRole.String(), data.CanonicalSnapshotRole.String()})

// has two root keys
require.Len(t, repo.tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs, 2)
// set up repo and keys
repo, _, keyID := createRepoAndKey(t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL)
publicKey := repo.CryptoService.GetKey(keyID)
privateKey, _, err := repo.CryptoService.GetPrivateKey(keyID)
require.NoError(t, err, "private key should exist in keystore")

// 1. create a repository and obtain its root key id, use the key id to get the corresponding
// public key. Match this public key with a false key . expect an error.

err = matchKeyIdsWithPubKeys(repo, []string{"fake id"}, []data.PublicKey{publicKey})
require.Error(t, err, "the public key should not be matched with the given id.")

// 2. match a correct public key (non x509) with its corresponding key id
err = matchKeyIdsWithPubKeys(repo, []string{publicKey.ID()}, []data.PublicKey{publicKey})
require.NoError(t, err, "public key should be matched with its corresponding private key ")

// 3. match a correct x509 public key with its corresponding private key id
// create x509 pubkey: create template -> use template to create a cert in PEM form -> convert to Certificate -> convert to pub key
startTime := time.Now()
template, err := utils.NewCertificate(data.CanonicalRootRole.String(), startTime, startTime.AddDate(10, 0, 0))
require.NoError(t, err, "failed to create the certificate template: %v", err)
signer := privateKey.CryptoSigner()
certPEM, err := x509.CreateCertificate(rand.Reader, template, template, signer.Public(), signer)
require.NoError(t, err, "error when generating certificate with public key %v", err)
cert, err := x509.ParseCertificate(certPEM)
require.NoError(t, err, "parsing PEM to certificate but encountered an error: %v", err)
certKey := utils.CertToKey(cert)

err = matchKeyIdsWithPubKeys(repo, []string{publicKey.ID()}, []data.PublicKey{certKey})
require.NoError(t, err, "public key should be matched with its corresponding private key")

// 4. match a non matching key pair, expect error
pub2, err := repo.CryptoService.Create(data.CanonicalRootRole, "docker.com/notary", data.ECDSAKey)
require.NoError(t, err, "error generating root key: %s", err)
err = matchKeyIdsWithPubKeys(repo, []string{pub2.ID()}, []data.PublicKey{publicKey})
require.Error(t, err, "validating a non-matching key pair should fail but didn't")
}

// Initializing a new repo fails if unable to get the timestamp key, even if
Expand Down
Loading