package tuf import ( "fmt" "github.com/docker/go/canonical/json" "github.com/docker/notary" "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" "github.com/docker/notary/tuf/utils" ) // ErrBuildDone is returned when any functions are called on RepoBuilder, and it // is already finished building var ErrBuildDone = fmt.Errorf( "the builder has finished building and cannot accept any more input or produce any more output") // ErrInvalidBuilderInput is returned when RepoBuilder.Load is called // with the wrong type of metadata for the state that it's in type ErrInvalidBuilderInput struct{ msg string } func (e ErrInvalidBuilderInput) Error() string { return e.msg } // ConsistentInfo is the consistent name and size of a role, or just the name // of the role and a -1 if no file metadata for the role is known type ConsistentInfo struct { RoleName data.RoleName fileMeta data.FileMeta } // ChecksumKnown determines whether or not we know enough to provide a size and // consistent name func (c ConsistentInfo) ChecksumKnown() bool { // empty hash, no size : this is the zero value return len(c.fileMeta.Hashes) > 0 || c.fileMeta.Length != 0 } // ConsistentName returns the consistent name (rolename.sha256) for the role // given this consistent information func (c ConsistentInfo) ConsistentName() string { return utils.ConsistentName(c.RoleName.String(), c.fileMeta.Hashes[notary.SHA256]) } // Length returns the expected length of the role as per this consistent // information - if no checksum information is known, the size is -1. func (c ConsistentInfo) Length() int64 { if c.ChecksumKnown() { return c.fileMeta.Length } return -1 } // RepoBuilder is an interface for an object which builds a tuf.Repo type RepoBuilder interface { Load(roleName data.RoleName, content []byte, minVersion int, allowExpired bool) error LoadRootForUpdate(content []byte, minVersion int, isFinal bool) error GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, error) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, int, error) Finish() (*Repo, *Repo, error) BootstrapNewBuilder() RepoBuilder BootstrapNewBuilderWithNewTrustpin(trustpin trustpinning.TrustPinConfig) RepoBuilder // informative functions IsLoaded(roleName data.RoleName) bool GetLoadedVersion(roleName data.RoleName) int GetConsistentInfo(roleName data.RoleName) ConsistentInfo } // finishedBuilder refuses any more input or output type finishedBuilder struct{} func (f finishedBuilder) Load(roleName data.RoleName, content []byte, minVersion int, allowExpired bool) error { return ErrBuildDone } func (f finishedBuilder) LoadRootForUpdate(content []byte, minVersion int, isFinal bool) error { return ErrBuildDone } func (f finishedBuilder) GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, error) { return nil, 0, ErrBuildDone } func (f finishedBuilder) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, int, error) { return nil, 0, ErrBuildDone } func (f finishedBuilder) Finish() (*Repo, *Repo, error) { return nil, nil, ErrBuildDone } func (f finishedBuilder) BootstrapNewBuilder() RepoBuilder { return f } func (f finishedBuilder) BootstrapNewBuilderWithNewTrustpin(trustpin trustpinning.TrustPinConfig) RepoBuilder { return f } func (f finishedBuilder) IsLoaded(roleName data.RoleName) bool { return false } func (f finishedBuilder) GetLoadedVersion(roleName data.RoleName) int { return 0 } func (f finishedBuilder) GetConsistentInfo(roleName data.RoleName) ConsistentInfo { return ConsistentInfo{RoleName: roleName} } // NewRepoBuilder is the only way to get a pre-built RepoBuilder func NewRepoBuilder(gun data.GUN, cs signed.CryptoService, trustpin trustpinning.TrustPinConfig) RepoBuilder { return NewBuilderFromRepo(gun, NewRepo(cs), trustpin) } // NewBuilderFromRepo allows us to bootstrap a builder given existing repo data. // YOU PROBABLY SHOULDN'T BE USING THIS OUTSIDE OF TESTING CODE!!! func NewBuilderFromRepo(gun data.GUN, repo *Repo, trustpin trustpinning.TrustPinConfig) RepoBuilder { return &repoBuilderWrapper{ RepoBuilder: &repoBuilder{ repo: repo, invalidRoles: NewRepo(nil), gun: gun, trustpin: trustpin, loadedNotChecksummed: make(map[data.RoleName][]byte), }, } } // repoBuilderWrapper embeds a repoBuilder, but once Finish is called, swaps // the embed out with a finishedBuilder type repoBuilderWrapper struct { RepoBuilder } func (rbw *repoBuilderWrapper) Finish() (*Repo, *Repo, error) { switch rbw.RepoBuilder.(type) { case finishedBuilder: return rbw.RepoBuilder.Finish() default: old := rbw.RepoBuilder rbw.RepoBuilder = finishedBuilder{} return old.Finish() } } // repoBuilder actually builds a tuf.Repo type repoBuilder struct { repo *Repo invalidRoles *Repo // needed for root trust pininng verification gun data.GUN trustpin trustpinning.TrustPinConfig // in case we load root and/or targets before snapshot and timestamp ( // or snapshot and not timestamp), so we know what to verify when the // data with checksums come in loadedNotChecksummed map[data.RoleName][]byte // bootstrapped values to validate a new root prevRoot *data.SignedRoot bootstrappedRootChecksum *data.FileMeta // for bootstrapping the next builder nextRootChecksum *data.FileMeta } func (rb *repoBuilder) Finish() (*Repo, *Repo, error) { return rb.repo, rb.invalidRoles, nil } func (rb *repoBuilder) BootstrapNewBuilder() RepoBuilder { return &repoBuilderWrapper{RepoBuilder: &repoBuilder{ repo: NewRepo(rb.repo.cryptoService), invalidRoles: NewRepo(nil), gun: rb.gun, loadedNotChecksummed: make(map[data.RoleName][]byte), trustpin: rb.trustpin, prevRoot: rb.repo.Root, bootstrappedRootChecksum: rb.nextRootChecksum, }} } func (rb *repoBuilder) BootstrapNewBuilderWithNewTrustpin(trustpin trustpinning.TrustPinConfig) RepoBuilder { return &repoBuilderWrapper{RepoBuilder: &repoBuilder{ repo: NewRepo(rb.repo.cryptoService), gun: rb.gun, loadedNotChecksummed: make(map[data.RoleName][]byte), trustpin: trustpin, prevRoot: rb.repo.Root, bootstrappedRootChecksum: rb.nextRootChecksum, }} } // IsLoaded returns whether a particular role has already been loaded func (rb *repoBuilder) IsLoaded(roleName data.RoleName) bool { switch roleName { case data.CanonicalRootRole: return rb.repo.Root != nil case data.CanonicalSnapshotRole: return rb.repo.Snapshot != nil case data.CanonicalTimestampRole: return rb.repo.Timestamp != nil default: return rb.repo.Targets[roleName] != nil } } // GetLoadedVersion returns the metadata version, if it is loaded, or 1 (the // minimum valid version number) otherwise func (rb *repoBuilder) GetLoadedVersion(roleName data.RoleName) int { switch { case roleName == data.CanonicalRootRole && rb.repo.Root != nil: return rb.repo.Root.Signed.Version case roleName == data.CanonicalSnapshotRole && rb.repo.Snapshot != nil: return rb.repo.Snapshot.Signed.Version case roleName == data.CanonicalTimestampRole && rb.repo.Timestamp != nil: return rb.repo.Timestamp.Signed.Version default: if tgts, ok := rb.repo.Targets[roleName]; ok { return tgts.Signed.Version } } return 1 } // GetConsistentInfo returns the consistent name and size of a role, if it is known, // otherwise just the rolename and a -1 for size (both of which are inside a // ConsistentInfo object) func (rb *repoBuilder) GetConsistentInfo(roleName data.RoleName) ConsistentInfo { info := ConsistentInfo{RoleName: roleName} // starts out with unknown filemeta switch roleName { case data.CanonicalTimestampRole: // we do not want to get a consistent timestamp, but we do want to // limit its size info.fileMeta.Length = notary.MaxTimestampSize case data.CanonicalSnapshotRole: if rb.repo.Timestamp != nil { info.fileMeta = rb.repo.Timestamp.Signed.Meta[roleName.String()] } case data.CanonicalRootRole: switch { case rb.bootstrappedRootChecksum != nil: info.fileMeta = *rb.bootstrappedRootChecksum case rb.repo.Snapshot != nil: info.fileMeta = rb.repo.Snapshot.Signed.Meta[roleName.String()] } default: if rb.repo.Snapshot != nil { info.fileMeta = rb.repo.Snapshot.Signed.Meta[roleName.String()] } } return info } func (rb *repoBuilder) Load(roleName data.RoleName, content []byte, minVersion int, allowExpired bool) error { return rb.loadOptions(roleName, content, minVersion, allowExpired, false, false) } // LoadRootForUpdate adds additional flags for updating the root.json file func (rb *repoBuilder) LoadRootForUpdate(content []byte, minVersion int, isFinal bool) error { if err := rb.loadOptions(data.CanonicalRootRole, content, minVersion, !isFinal, !isFinal, true); err != nil { return err } if !isFinal { rb.prevRoot = rb.repo.Root } return nil } // loadOptions adds additional flags that should only be used for updating the root.json func (rb *repoBuilder) loadOptions(roleName data.RoleName, content []byte, minVersion int, allowExpired, skipChecksum, allowLoaded bool) error { if !data.ValidRole(roleName) { return ErrInvalidBuilderInput{msg: fmt.Sprintf("%s is an invalid role", roleName)} } if !allowLoaded && rb.IsLoaded(roleName) { return ErrInvalidBuilderInput{msg: fmt.Sprintf("%s has already been loaded", roleName)} } var err error switch roleName { case data.CanonicalRootRole: break case data.CanonicalTimestampRole, data.CanonicalSnapshotRole, data.CanonicalTargetsRole: err = rb.checkPrereqsLoaded([]data.RoleName{data.CanonicalRootRole}) default: // delegations err = rb.checkPrereqsLoaded([]data.RoleName{data.CanonicalRootRole, data.CanonicalTargetsRole}) } if err != nil { return err } switch roleName { case data.CanonicalRootRole: return rb.loadRoot(content, minVersion, allowExpired, skipChecksum) case data.CanonicalSnapshotRole: return rb.loadSnapshot(content, minVersion, allowExpired) case data.CanonicalTimestampRole: return rb.loadTimestamp(content, minVersion, allowExpired) case data.CanonicalTargetsRole: return rb.loadTargets(content, minVersion, allowExpired) default: return rb.loadDelegation(roleName, content, minVersion, allowExpired) } } func (rb *repoBuilder) checkPrereqsLoaded(prereqRoles []data.RoleName) error { for _, req := range prereqRoles { if !rb.IsLoaded(req) { return ErrInvalidBuilderInput{msg: fmt.Sprintf("%s must be loaded first", req)} } } return nil } // GenerateSnapshot generates a new snapshot given a previous (optional) snapshot // We can't just load the previous snapshot, because it may have been signed by a different // snapshot key (maybe from a previous root version). Note that we need the root role and // targets role to be loaded, because we need to generate metadata for both (and we need // the root to be loaded so we can get the snapshot role to sign with) func (rb *repoBuilder) GenerateSnapshot(prev *data.SignedSnapshot) ([]byte, int, error) { switch { case rb.repo.cryptoService == nil: return nil, 0, ErrInvalidBuilderInput{msg: "cannot generate snapshot without a cryptoservice"} case rb.IsLoaded(data.CanonicalSnapshotRole): return nil, 0, ErrInvalidBuilderInput{msg: "snapshot has already been loaded"} case rb.IsLoaded(data.CanonicalTimestampRole): return nil, 0, ErrInvalidBuilderInput{msg: "cannot generate snapshot if timestamp has already been loaded"} } if err := rb.checkPrereqsLoaded([]data.RoleName{data.CanonicalRootRole}); err != nil { return nil, 0, err } // If there is no previous snapshot, we need to generate one, and so the targets must // have already been loaded. Otherwise, so long as the previous snapshot structure is // valid (it has a targets meta), we're good. switch prev { case nil: if err := rb.checkPrereqsLoaded([]data.RoleName{data.CanonicalTargetsRole}); err != nil { return nil, 0, err } if err := rb.repo.InitSnapshot(); err != nil { rb.repo.Snapshot = nil return nil, 0, err } default: if err := data.IsValidSnapshotStructure(prev.Signed); err != nil { return nil, 0, err } rb.repo.Snapshot = prev } sgnd, err := rb.repo.SignSnapshot(data.DefaultExpires(data.CanonicalSnapshotRole)) if err != nil { rb.repo.Snapshot = nil return nil, 0, err } sgndJSON, err := json.Marshal(sgnd) if err != nil { rb.repo.Snapshot = nil return nil, 0, err } // loadedNotChecksummed should currently contain the root awaiting checksumming, // since it has to have been loaded. Since the snapshot was generated using // the root and targets data (there may not be any) that that have been loaded, // remove all of them from rb.loadedNotChecksummed for tgtName := range rb.repo.Targets { delete(rb.loadedNotChecksummed, data.RoleName(tgtName)) } delete(rb.loadedNotChecksummed, data.CanonicalRootRole) // The timestamp can't have been loaded yet, so we want to cache the snapshot // bytes so we can validate the checksum when a timestamp gets generated or // loaded later. rb.loadedNotChecksummed[data.CanonicalSnapshotRole] = sgndJSON return sgndJSON, rb.repo.Snapshot.Signed.Version, nil } // GenerateTimestamp generates a new timestamp given a previous (optional) timestamp // We can't just load the previous timestamp, because it may have been signed by a different // timestamp key (maybe from a previous root version) func (rb *repoBuilder) GenerateTimestamp(prev *data.SignedTimestamp) ([]byte, int, error) { switch { case rb.repo.cryptoService == nil: return nil, 0, ErrInvalidBuilderInput{msg: "cannot generate timestamp without a cryptoservice"} case rb.IsLoaded(data.CanonicalTimestampRole): return nil, 0, ErrInvalidBuilderInput{msg: "timestamp has already been loaded"} } // SignTimestamp always serializes the loaded snapshot and signs in the data, so we must always // have the snapshot loaded first if err := rb.checkPrereqsLoaded([]data.RoleName{data.CanonicalRootRole, data.CanonicalSnapshotRole}); err != nil { return nil, 0, err } switch prev { case nil: if err := rb.repo.InitTimestamp(); err != nil { rb.repo.Timestamp = nil return nil, 0, err } default: if err := data.IsValidTimestampStructure(prev.Signed); err != nil { return nil, 0, err } rb.repo.Timestamp = prev } sgnd, err := rb.repo.SignTimestamp(data.DefaultExpires(data.CanonicalTimestampRole)) if err != nil { rb.repo.Timestamp = nil return nil, 0, err } sgndJSON, err := json.Marshal(sgnd) if err != nil { rb.repo.Timestamp = nil return nil, 0, err } // The snapshot should have been loaded (and not checksummed, since a timestamp // cannot have been loaded), so it is awaiting checksumming. Since this // timestamp was generated using the snapshot awaiting checksumming, we can // remove it from rb.loadedNotChecksummed. There should be no other items // awaiting checksumming now since loading/generating a snapshot should have // cleared out everything else in `loadNotChecksummed`. delete(rb.loadedNotChecksummed, data.CanonicalSnapshotRole) return sgndJSON, rb.repo.Timestamp.Signed.Version, nil } // loadRoot loads a root if one has not been loaded func (rb *repoBuilder) loadRoot(content []byte, minVersion int, allowExpired, skipChecksum bool) error { roleName := data.CanonicalRootRole signedObj, err := rb.bytesToSigned(content, data.CanonicalRootRole, skipChecksum) if err != nil { return err } // ValidateRoot validates against the previous root's role, as well as validates that the root // itself is self-consistent with its own signatures and thresholds. // This assumes that ValidateRoot calls data.RootFromSigned, which validates // the metadata, rather than just unmarshalling signedObject into a SignedRoot object itself. signedRoot, err := trustpinning.ValidateRoot(rb.prevRoot, signedObj, rb.gun, rb.trustpin) if err != nil { return err } if err := signed.VerifyVersion(&(signedRoot.Signed.SignedCommon), minVersion); err != nil { return err } if !allowExpired { // check must go at the end because all other validation should pass if err := signed.VerifyExpiry(&(signedRoot.Signed.SignedCommon), roleName); err != nil { return err } } rootRole, err := signedRoot.BuildBaseRole(data.CanonicalRootRole) if err != nil { // this should never happen since the root has been validated return err } rb.repo.Root = signedRoot rb.repo.originalRootRole = rootRole return nil } func (rb *repoBuilder) loadTimestamp(content []byte, minVersion int, allowExpired bool) error { roleName := data.CanonicalTimestampRole timestampRole, err := rb.repo.Root.BuildBaseRole(roleName) if err != nil { // this should never happen, since it's already been validated return err } signedObj, err := rb.bytesToSignedAndValidateSigs(timestampRole, content) if err != nil { return err } signedTimestamp, err := data.TimestampFromSigned(signedObj) if err != nil { return err } if err := signed.VerifyVersion(&(signedTimestamp.Signed.SignedCommon), minVersion); err != nil { return err } if !allowExpired { // check must go at the end because all other validation should pass if err := signed.VerifyExpiry(&(signedTimestamp.Signed.SignedCommon), roleName); err != nil { return err } } if err := rb.validateChecksumsFromTimestamp(signedTimestamp); err != nil { return err } rb.repo.Timestamp = signedTimestamp return nil } func (rb *repoBuilder) loadSnapshot(content []byte, minVersion int, allowExpired bool) error { roleName := data.CanonicalSnapshotRole snapshotRole, err := rb.repo.Root.BuildBaseRole(roleName) if err != nil { // this should never happen, since it's already been validated return err } signedObj, err := rb.bytesToSignedAndValidateSigs(snapshotRole, content) if err != nil { return err } signedSnapshot, err := data.SnapshotFromSigned(signedObj) if err != nil { return err } if err := signed.VerifyVersion(&(signedSnapshot.Signed.SignedCommon), minVersion); err != nil { return err } if !allowExpired { // check must go at the end because all other validation should pass if err := signed.VerifyExpiry(&(signedSnapshot.Signed.SignedCommon), roleName); err != nil { return err } } // at this point, the only thing left to validate is existing checksums - we can use // this snapshot to bootstrap the next builder if needed - and we don't need to do // the 2-value assignment since we've already validated the signedSnapshot, which MUST // have root metadata rootMeta := signedSnapshot.Signed.Meta[data.CanonicalRootRole.String()] rb.nextRootChecksum = &rootMeta if err := rb.validateChecksumsFromSnapshot(signedSnapshot); err != nil { return err } rb.repo.Snapshot = signedSnapshot return nil } func (rb *repoBuilder) loadTargets(content []byte, minVersion int, allowExpired bool) error { roleName := data.CanonicalTargetsRole targetsRole, err := rb.repo.Root.BuildBaseRole(roleName) if err != nil { // this should never happen, since it's already been validated return err } signedObj, err := rb.bytesToSignedAndValidateSigs(targetsRole, content) if err != nil { return err } signedTargets, err := data.TargetsFromSigned(signedObj, roleName) if err != nil { return err } if err := signed.VerifyVersion(&(signedTargets.Signed.SignedCommon), minVersion); err != nil { return err } if !allowExpired { // check must go at the end because all other validation should pass if err := signed.VerifyExpiry(&(signedTargets.Signed.SignedCommon), roleName); err != nil { return err } } signedTargets.Signatures = signedObj.Signatures rb.repo.Targets[roleName] = signedTargets return nil } func (rb *repoBuilder) loadDelegation(roleName data.RoleName, content []byte, minVersion int, allowExpired bool) error { delegationRole, err := rb.repo.GetDelegationRole(roleName) if err != nil { return err } // bytesToSigned checks checksum signedObj, err := rb.bytesToSigned(content, roleName, false) if err != nil { return err } signedTargets, err := data.TargetsFromSigned(signedObj, roleName) if err != nil { return err } if err := signed.VerifyVersion(&(signedTargets.Signed.SignedCommon), minVersion); err != nil { // don't capture in invalidRoles because the role we received is a rollback return err } // verify signature if err := signed.VerifySignatures(signedObj, delegationRole.BaseRole); err != nil { rb.invalidRoles.Targets[roleName] = signedTargets return err } if !allowExpired { // check must go at the end because all other validation should pass if err := signed.VerifyExpiry(&(signedTargets.Signed.SignedCommon), roleName); err != nil { rb.invalidRoles.Targets[roleName] = signedTargets return err } } signedTargets.Signatures = signedObj.Signatures rb.repo.Targets[roleName] = signedTargets return nil } func (rb *repoBuilder) validateChecksumsFromTimestamp(ts *data.SignedTimestamp) error { sn, ok := rb.loadedNotChecksummed[data.CanonicalSnapshotRole] if ok { // by this point, the SignedTimestamp has been validated so it must have a snapshot hash snMeta := ts.Signed.Meta[data.CanonicalSnapshotRole.String()].Hashes if err := data.CheckHashes(sn, data.CanonicalSnapshotRole.String(), snMeta); err != nil { return err } delete(rb.loadedNotChecksummed, data.CanonicalSnapshotRole) } return nil } func (rb *repoBuilder) validateChecksumsFromSnapshot(sn *data.SignedSnapshot) error { var goodRoles []data.RoleName for roleName, loadedBytes := range rb.loadedNotChecksummed { switch roleName { case data.CanonicalSnapshotRole, data.CanonicalTimestampRole: break default: if err := data.CheckHashes(loadedBytes, roleName.String(), sn.Signed.Meta[roleName.String()].Hashes); err != nil { return err } goodRoles = append(goodRoles, roleName) } } for _, roleName := range goodRoles { delete(rb.loadedNotChecksummed, roleName) } return nil } func (rb *repoBuilder) validateChecksumFor(content []byte, roleName data.RoleName) error { // validate the bootstrap checksum for root, if provided if roleName == data.CanonicalRootRole && rb.bootstrappedRootChecksum != nil { if err := data.CheckHashes(content, roleName.String(), rb.bootstrappedRootChecksum.Hashes); err != nil { return err } } // but we also want to cache the root content, so that when the snapshot is // loaded it is validated (to make sure everything in the repo is self-consistent) checksums := rb.getChecksumsFor(roleName) if checksums != nil { // as opposed to empty, in which case hash check should fail if err := data.CheckHashes(content, roleName.String(), *checksums); err != nil { return err } } else if roleName != data.CanonicalTimestampRole { // timestamp is the only role which does not need to be checksummed, but // for everything else, cache the contents in the list of roles that have // not been checksummed by the snapshot/timestamp yet rb.loadedNotChecksummed[roleName] = content } return nil } // Checksums the given bytes, and if they validate, convert to a data.Signed object. // If a checksums are nil (as opposed to empty), adds the bytes to the list of roles that // haven't been checksummed (unless it's a timestamp, which has no checksum reference). func (rb *repoBuilder) bytesToSigned(content []byte, roleName data.RoleName, skipChecksum bool) (*data.Signed, error) { if !skipChecksum { if err := rb.validateChecksumFor(content, roleName); err != nil { return nil, err } } // unmarshal to signed signedObj := &data.Signed{} if err := json.Unmarshal(content, signedObj); err != nil { return nil, err } return signedObj, nil } func (rb *repoBuilder) bytesToSignedAndValidateSigs(role data.BaseRole, content []byte) (*data.Signed, error) { signedObj, err := rb.bytesToSigned(content, role.Name, false) if err != nil { return nil, err } // verify signature if err := signed.VerifySignatures(signedObj, role); err != nil { return nil, err } return signedObj, nil } // If the checksum reference (the loaded timestamp for the snapshot role, and // the loaded snapshot for every other role except timestamp and snapshot) is nil, // then return nil for the checksums, meaning that the checksum is not yet // available. If the checksum reference *is* loaded, then always returns the // Hashes object for the given role - if it doesn't exist, returns an empty Hash // object (against which any checksum validation would fail). func (rb *repoBuilder) getChecksumsFor(role data.RoleName) *data.Hashes { var hashes data.Hashes switch role { case data.CanonicalTimestampRole: return nil case data.CanonicalSnapshotRole: if rb.repo.Timestamp == nil { return nil } hashes = rb.repo.Timestamp.Signed.Meta[data.CanonicalSnapshotRole.String()].Hashes default: if rb.repo.Snapshot == nil { return nil } hashes = rb.repo.Snapshot.Signed.Meta[role.String()].Hashes } return &hashes }