package client import ( "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "os" "path/filepath" "github.com/Sirupsen/logrus" "github.com/docker/notary/client/changelist" "github.com/docker/notary/cryptoservice" "github.com/docker/notary/keystoremanager" "github.com/docker/notary/trustmanager" "github.com/endophage/gotuf" tufclient "github.com/endophage/gotuf/client" "github.com/endophage/gotuf/data" "github.com/endophage/gotuf/keys" "github.com/endophage/gotuf/signed" "github.com/endophage/gotuf/store" ) // ErrRepoNotInitialized is returned when trying to can publish on an uninitialized // notary repository type ErrRepoNotInitialized struct{} type passwordRetriever func() (string, error) // ErrRepoNotInitialized is returned when trying to can publish on an uninitialized // notary repository func (err *ErrRepoNotInitialized) Error() string { return "Repository has not been initialized" } const ( tufDir = "tuf" ) // ErrRepositoryNotExist gets returned when trying to make an action over a repository /// that doesn't exist. var ErrRepositoryNotExist = errors.New("repository does not exist") // NotaryRepository stores all the information needed to operate on a notary // repository. type NotaryRepository struct { baseDir string gun string baseURL string tufRepoPath string fileStore store.MetadataStore cryptoService signed.CryptoService tufRepo *tuf.TufRepo roundTrip http.RoundTripper KeyStoreManager *keystoremanager.KeyStoreManager } // Target represents a simplified version of the data TUF operates on, so external // applications don't have to depend on tuf data types. type Target struct { Name string Hashes data.Hashes Length int64 } // NewTarget is a helper method that returns a Target func NewTarget(targetName string, targetPath string) (*Target, error) { b, err := ioutil.ReadFile(targetPath) if err != nil { return nil, err } meta, err := data.NewFileMeta(bytes.NewBuffer(b)) if err != nil { return nil, err } return &Target{Name: targetName, Hashes: meta.Hashes, Length: meta.Length}, nil } // 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) (*NotaryRepository, error) { keyStoreManager, err := keystoremanager.NewKeyStoreManager(baseDir) if err != nil { return nil, err } cryptoService := cryptoservice.NewCryptoService(gun, keyStoreManager.NonRootKeyStore(), "") nRepo := &NotaryRepository{ gun: gun, baseDir: baseDir, baseURL: baseURL, tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)), cryptoService: cryptoService, roundTrip: rt, KeyStoreManager: keyStoreManager, } return nRepo, nil } // Initialize creates a new repository by using rootKey as the root Key for the // TUF repository. func (r *NotaryRepository) Initialize(uCryptoService *cryptoservice.UnlockedCryptoService) error { rootCert, err := uCryptoService.GenerateCertificate(r.gun) if err != nil { return err } r.KeyStoreManager.CertificateStore().AddCert(rootCert) // The root key gets stored in the TUF metadata X509 encoded, linking // the tuf root.json to our X509 PKI. // If the key is RSA, we store it as type RSAx509, if it is ECDSA we store it // as ECDSAx509 to allow the gotuf verifiers to correctly decode the // key on verification of signatures. var algorithmType data.KeyAlgorithm algorithm := uCryptoService.PrivKey.Algorithm() switch algorithm { case data.RSAKey: algorithmType = data.RSAx509Key case data.ECDSAKey: algorithmType = data.ECDSAx509Key default: return fmt.Errorf("invalid format for root key: %s", algorithm) } // Generate a x509Key using the rootCert as the public key rootKey := data.NewPublicKey(algorithmType, trustmanager.CertToPEM(rootCert)) // Creates a symlink between the certificate ID and the real public key it // is associated with. This is used to be able to retrieve the root private key // associated with a particular certificate logrus.Debugf("Linking %s to %s.", rootKey.ID(), uCryptoService.ID()) err = r.KeyStoreManager.RootKeyStore().Link(uCryptoService.ID(), rootKey.ID()) if err != nil { return err } // All the timestamp keys are generated by the remote server. remote, err := getRemoteStore(r.baseURL, r.gun, r.roundTrip) rawTSKey, err := remote.GetKey("timestamp") if err != nil { return err } parsedKey := &data.TUFKey{} err = json.Unmarshal(rawTSKey, parsedKey) if err != nil { return err } // Turn the JSON timestamp key from the remote server into a TUFKey timestampKey := data.NewPublicKey(parsedKey.Algorithm(), parsedKey.Public()) logrus.Debugf("got remote %s timestamp key with keyID: %s", parsedKey.Algorithm(), timestampKey.ID()) // This is currently hardcoding the targets and snapshots keys to ECDSA // Targets and snapshot keys are always generated locally. targetsKey, err := r.cryptoService.Create("targets", data.ECDSAKey) if err != nil { return err } snapshotKey, err := r.cryptoService.Create("snapshot", data.ECDSAKey) if err != nil { return err } kdb := keys.NewDB() kdb.AddKey(rootKey) kdb.AddKey(targetsKey) kdb.AddKey(snapshotKey) kdb.AddKey(timestampKey) rootRole, err := data.NewRole("root", 1, []string{rootKey.ID()}, nil, nil) if err != nil { return err } targetsRole, err := data.NewRole("targets", 1, []string{targetsKey.ID()}, nil, nil) if err != nil { return err } snapshotRole, err := data.NewRole("snapshot", 1, []string{snapshotKey.ID()}, nil, nil) if err != nil { return err } timestampRole, err := data.NewRole("timestamp", 1, []string{timestampKey.ID()}, nil, nil) if err != nil { return err } if err := kdb.AddRole(rootRole); err != nil { return err } if err := kdb.AddRole(targetsRole); err != nil { return err } if err := kdb.AddRole(snapshotRole); err != nil { return err } if err := kdb.AddRole(timestampRole); err != nil { return err } r.tufRepo = tuf.NewTufRepo(kdb, r.cryptoService) r.fileStore, err = store.NewFilesystemStore( r.tufRepoPath, "metadata", "json", "targets", ) if err != nil { return err } if err := r.tufRepo.InitRepo(false); err != nil { return err } if err := r.saveMetadata(uCryptoService.CryptoService); err != nil { return err } // Creates an empty snapshot return r.snapshot() } // AddTarget adds a new target to the repository, forcing a timestamps check from TUF func (r *NotaryRepository) AddTarget(target *Target) error { cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) if err != nil { return err } fmt.Printf("Adding target \"%s\" with sha256 \"%s\" and size %d bytes.\n", target.Name, target.Hashes["sha256"], target.Length) meta := data.FileMeta{Length: target.Length, Hashes: target.Hashes} metaJSON, err := json.Marshal(meta) if err != nil { return err } c := changelist.NewTufChange(changelist.ActionCreate, "targets", "target", target.Name, metaJSON) err = cl.Add(c) if err != nil { return err } return cl.Close() } // ListTargets lists all targets for the current repository func (r *NotaryRepository) ListTargets() ([]*Target, error) { c, err := r.bootstrapClient() if err != nil { return nil, err } err = c.Update() if err != nil { return nil, err } var targetList []*Target for name, meta := range r.tufRepo.Targets["targets"].Signed.Targets { target := &Target{Name: name, Hashes: meta.Hashes, Length: meta.Length} targetList = append(targetList, target) } return targetList, nil } // GetTargetByName returns a target given a name func (r *NotaryRepository) GetTargetByName(name string) (*Target, error) { c, err := r.bootstrapClient() if err != nil { return nil, err } err = c.Update() if err != nil { return nil, err } meta := c.TargetMeta(name) if meta == nil { return nil, errors.New("Meta is nil for target") } return &Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}, nil } // Publish pushes the local changes in signed material to the remote notary-server // Conceptually it performs an operation similar to a `git rebase` func (r *NotaryRepository) Publish(getPass passwordRetriever) error { var updateRoot bool var root *data.Signed // attempt to initialize the repo from the remote store c, err := r.bootstrapClient() if err != nil { if _, ok := err.(*store.ErrMetaNotFound); ok { // if the remote store return a 404 (translated into ErrMetaNotFound), // the repo hasn't been initialized yet. Attempt to load it from disk. err := r.bootstrapRepo() if err != nil { // Repo hasn't been initialized, It must be initialized before // it can be published. Return an error and let caller determine // what it wants to do. logrus.Debug("Repository not initialized during Publish") return &ErrRepoNotInitialized{} } // We had local data but the server doesn't know about the repo yet, // ensure we will push the initial root file root, err = r.tufRepo.Root.ToSigned() if err != nil { return err } updateRoot = true } else { // The remote store returned an error other than 404. We're // unable to determine if the repo has been initialized or not. logrus.Error("Could not publish Repository: ", err.Error()) return err } } else { // If we were successfully able to bootstrap the client (which only pulls // root.json), update it the rest of the tuf metadata in preparation for // applying the changelist. err = c.Update() if err != nil { return err } } // load the changelist for this repo cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) if err != nil { logrus.Debug("Error initializing changelist") return err } // apply the changelist to the repo err = applyChangelist(r.tufRepo, cl) if err != nil { logrus.Debug("Error applying changelist") return err } // check if our root file is nearing expiry. Resign if it is. if nearExpiry(r.tufRepo.Root) || r.tufRepo.Root.Dirty { passphrase, err := getPass() if err != nil { return err } rootKeyID := r.tufRepo.Root.Signed.Roles["root"].KeyIDs[0] rootCryptoService, err := r.KeyStoreManager.GetRootCryptoService(rootKeyID, passphrase) if err != nil { return err } root, err = r.tufRepo.SignRoot(data.DefaultExpires("root"), rootCryptoService.CryptoService) if err != nil { return err } updateRoot = true } // we will always resign targets and snapshots targets, err := r.tufRepo.SignTargets("targets", data.DefaultExpires("targets"), nil) if err != nil { return err } snapshot, err := r.tufRepo.SignSnapshot(data.DefaultExpires("snapshot"), nil) if err != nil { return err } remote, err := getRemoteStore(r.baseURL, r.gun, r.roundTrip) if err != nil { return err } // ensure we can marshal all the json before sending anything to remote targetsJSON, err := json.Marshal(targets) if err != nil { return err } snapshotJSON, err := json.Marshal(snapshot) if err != nil { return err } // if we need to update the root, marshal it and push the update to remote if updateRoot { rootJSON, err := json.Marshal(root) if err != nil { return err } err = remote.SetMeta("root", rootJSON) if err != nil { return err } } err = remote.SetMeta("targets", targetsJSON) if err != nil { return err } err = remote.SetMeta("snapshot", snapshotJSON) if err != nil { return err } return nil } func (r *NotaryRepository) bootstrapRepo() error { fileStore, err := store.NewFilesystemStore( r.tufRepoPath, "metadata", "json", "targets", ) if err != nil { return err } kdb := keys.NewDB() tufRepo := tuf.NewTufRepo(kdb, r.cryptoService) logrus.Debugf("Loading trusted collection.") rootJSON, err := fileStore.GetMeta("root", 0) if err != nil { return err } root := &data.Signed{} err = json.Unmarshal(rootJSON, root) if err != nil { return err } tufRepo.SetRoot(root) targetsJSON, err := fileStore.GetMeta("targets", 0) if err != nil { return err } targets := &data.Signed{} err = json.Unmarshal(targetsJSON, targets) if err != nil { return err } tufRepo.SetTargets("targets", targets) snapshotJSON, err := fileStore.GetMeta("snapshot", 0) if err != nil { return err } snapshot := &data.Signed{} err = json.Unmarshal(snapshotJSON, snapshot) if err != nil { return err } tufRepo.SetSnapshot(snapshot) r.tufRepo = tufRepo r.fileStore = fileStore return nil } func (r *NotaryRepository) saveMetadata(rootCryptoService signed.CryptoService) error { signedRoot, err := r.tufRepo.SignRoot(data.DefaultExpires("root"), rootCryptoService) if err != nil { return err } rootJSON, _ := json.Marshal(signedRoot) return r.fileStore.SetMeta("root", rootJSON) } func (r *NotaryRepository) snapshot() error { logrus.Debugf("Saving changes to Trusted Collection.") for t := range r.tufRepo.Targets { signedTargets, err := r.tufRepo.SignTargets(t, data.DefaultExpires("targets"), nil) if err != nil { return err } targetsJSON, _ := json.Marshal(signedTargets) parentDir := filepath.Dir(t) os.MkdirAll(parentDir, 0755) r.fileStore.SetMeta(t, targetsJSON) } signedSnapshot, err := r.tufRepo.SignSnapshot(data.DefaultExpires("snapshot"), nil) if err != nil { return err } snapshotJSON, _ := json.Marshal(signedSnapshot) return r.fileStore.SetMeta("snapshot", snapshotJSON) } func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) { remote, err := getRemoteStore(r.baseURL, r.gun, r.roundTrip) if err != nil { return nil, err } rootJSON, err := remote.GetMeta("root", 5<<20) if err != nil { return nil, err } root := &data.Signed{} err = json.Unmarshal(rootJSON, root) if err != nil { return nil, err } err = r.KeyStoreManager.ValidateRoot(root, r.gun) if err != nil { return nil, err } kdb := keys.NewDB() r.tufRepo = tuf.NewTufRepo(kdb, r.cryptoService) err = r.tufRepo.SetRoot(root) if err != nil { return nil, err } return tufclient.NewClient( r.tufRepo, remote, kdb, ), nil }