diff options
Diffstat (limited to 'repo.go')
-rw-r--r-- | repo.go | 1695 |
1 files changed, 1695 insertions, 0 deletions
@@ -0,0 +1,1695 @@ +package tuf + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "path" + "sort" + "strings" + "time" + + "github.com/secure-systems-lab/go-securesystemslib/cjson" + "github.com/theupdateframework/go-tuf/data" + "github.com/theupdateframework/go-tuf/internal/roles" + "github.com/theupdateframework/go-tuf/internal/sets" + "github.com/theupdateframework/go-tuf/internal/signer" + "github.com/theupdateframework/go-tuf/pkg/keys" + "github.com/theupdateframework/go-tuf/pkg/targets" + "github.com/theupdateframework/go-tuf/sign" + "github.com/theupdateframework/go-tuf/util" + "github.com/theupdateframework/go-tuf/verify" +) + +const ( + // The maximum number of delegations to visit while traversing the delegations graph. + defaultMaxDelegations = 32 +) + +// topLevelMetadata determines the order signatures are verified when committing. +var topLevelMetadata = []string{ + "root.json", + "targets.json", + "snapshot.json", + "timestamp.json", +} + +// TargetsWalkFunc is a function of a target path name and a target payload used to +// execute some function on each staged target file. For example, it may normalize path +// names and generate target file metadata with additional custom metadata. +type TargetsWalkFunc func(path string, target io.Reader) error + +type Repo struct { + local LocalStore + hashAlgorithms []string + meta map[string]json.RawMessage + prefix string + indent string + logger *log.Logger +} + +type RepoOpts func(r *Repo) + +func WithLogger(logger *log.Logger) RepoOpts { + return func(r *Repo) { + r.logger = logger + } +} + +func WithHashAlgorithms(hashAlgorithms ...string) RepoOpts { + return func(r *Repo) { + r.hashAlgorithms = hashAlgorithms + } +} + +func WithPrefix(prefix string) RepoOpts { + return func(r *Repo) { + r.prefix = prefix + } +} + +func WithIndex(indent string) RepoOpts { + return func(r *Repo) { + r.indent = indent + } +} + +func NewRepo(local LocalStore, hashAlgorithms ...string) (*Repo, error) { + return NewRepoIndent(local, "", "", hashAlgorithms...) +} + +func NewRepoIndent(local LocalStore, prefix string, indent string, + hashAlgorithms ...string) (*Repo, error) { + r := &Repo{ + local: local, + hashAlgorithms: hashAlgorithms, + prefix: prefix, + indent: indent, + logger: log.New(io.Discard, "", 0), + } + + var err error + r.meta, err = local.GetMeta() + if err != nil { + return nil, err + } + return r, nil +} + +func NewRepoWithOpts(local LocalStore, opts ...RepoOpts) (*Repo, error) { + r, err := NewRepo(local) + if err != nil { + return nil, err + } + for _, opt := range opts { + opt(r) + } + return r, nil +} + +func (r *Repo) Init(consistentSnapshot bool) error { + t, err := r.topLevelTargets() + if err != nil { + return err + } + if len(t.Targets) > 0 { + return ErrInitNotAllowed + } + root := data.NewRoot() + root.ConsistentSnapshot = consistentSnapshot + // Set root version to 1 for a new root. + root.Version = 1 + if err = r.setMeta("root.json", root); err != nil { + return err + } + + t.Version = 1 + if err = r.setMeta("targets.json", t); err != nil { + return err + } + + r.logger.Println("Repository initialized") + return nil +} + +func (r *Repo) topLevelKeysDB() (*verify.DB, error) { + db := verify.NewDB() + root, err := r.root() + if err != nil { + return nil, err + } + for id, k := range root.Keys { + if err := db.AddKey(id, k); err != nil { + return nil, err + } + } + for name, role := range root.Roles { + if err := db.AddRole(name, role); err != nil { + return nil, err + } + } + return db, nil +} + +func (r *Repo) root() (*data.Root, error) { + rootJSON, ok := r.meta["root.json"] + if !ok { + return data.NewRoot(), nil + } + s := &data.Signed{} + if err := json.Unmarshal(rootJSON, s); err != nil { + return nil, err + } + root := &data.Root{} + if err := json.Unmarshal(s.Signed, root); err != nil { + return nil, err + } + return root, nil +} + +func (r *Repo) snapshot() (*data.Snapshot, error) { + snapshotJSON, ok := r.meta["snapshot.json"] + if !ok { + return data.NewSnapshot(), nil + } + s := &data.Signed{} + if err := json.Unmarshal(snapshotJSON, s); err != nil { + return nil, err + } + snapshot := &data.Snapshot{} + if err := json.Unmarshal(s.Signed, snapshot); err != nil { + return nil, err + } + return snapshot, nil +} + +func (r *Repo) RootVersion() (int64, error) { + root, err := r.root() + if err != nil { + return -1, err + } + return root.Version, nil +} + +func (r *Repo) GetThreshold(keyRole string) (int, error) { + if roles.IsDelegatedTargetsRole(keyRole) { + // The signature threshold for a delegated targets role + // depends on the incoming delegation edge. + return -1, ErrInvalidRole{keyRole, "only thresholds for top-level roles supported"} + } + root, err := r.root() + if err != nil { + return -1, err + } + role, ok := root.Roles[keyRole] + if !ok { + return -1, ErrInvalidRole{keyRole, "role missing from root metadata"} + } + + return role.Threshold, nil +} + +func (r *Repo) SetThreshold(keyRole string, t int) error { + if roles.IsDelegatedTargetsRole(keyRole) { + // The signature threshold for a delegated targets role + // depends on the incoming delegation edge. + return ErrInvalidRole{keyRole, "only thresholds for top-level roles supported"} + } + root, err := r.root() + if err != nil { + return err + } + role, ok := root.Roles[keyRole] + if !ok { + return ErrInvalidRole{keyRole, "role missing from root metadata"} + } + if role.Threshold == t { + // Change was a no-op. + return nil + } + role.Threshold = t + if !r.local.FileIsStaged("root.json") { + root.Version++ + } + return r.setMeta("root.json", root) +} + +func (r *Repo) Targets() (data.TargetFiles, error) { + targets, err := r.topLevelTargets() + if err != nil { + return nil, err + } + return targets.Targets, nil +} + +func (r *Repo) SetTargetsVersion(v int64) error { + t, err := r.topLevelTargets() + if err != nil { + return err + } + t.Version = v + return r.setMeta("targets.json", t) +} + +func (r *Repo) TargetsVersion() (int64, error) { + t, err := r.topLevelTargets() + if err != nil { + return -1, err + } + return t.Version, nil +} + +func (r *Repo) SetTimestampVersion(v int64) error { + ts, err := r.timestamp() + if err != nil { + return err + } + ts.Version = v + return r.setMeta("timestamp.json", ts) +} + +func (r *Repo) TimestampVersion() (int64, error) { + ts, err := r.timestamp() + if err != nil { + return -1, err + } + return ts.Version, nil +} + +func (r *Repo) SetSnapshotVersion(v int64) error { + s, err := r.snapshot() + if err != nil { + return err + } + + s.Version = v + return r.setMeta("snapshot.json", s) +} + +func (r *Repo) SnapshotVersion() (int64, error) { + s, err := r.snapshot() + if err != nil { + return -1, err + } + return s.Version, nil +} + +func (r *Repo) topLevelTargets() (*data.Targets, error) { + return r.targets("targets") +} + +func (r *Repo) targets(metaName string) (*data.Targets, error) { + targetsJSON, ok := r.meta[metaName+".json"] + if !ok { + return data.NewTargets(), nil + } + s := &data.Signed{} + if err := json.Unmarshal(targetsJSON, s); err != nil { + return nil, fmt.Errorf("error unmarshalling for targets %q: %w", metaName, err) + } + targets := &data.Targets{} + if err := json.Unmarshal(s.Signed, targets); err != nil { + return nil, fmt.Errorf("error unmarshalling signed data for targets %q: %w", metaName, err) + } + return targets, nil +} + +func (r *Repo) timestamp() (*data.Timestamp, error) { + timestampJSON, ok := r.meta["timestamp.json"] + if !ok { + return data.NewTimestamp(), nil + } + s := &data.Signed{} + if err := json.Unmarshal(timestampJSON, s); err != nil { + return nil, err + } + timestamp := &data.Timestamp{} + if err := json.Unmarshal(s.Signed, timestamp); err != nil { + return nil, err + } + return timestamp, nil +} + +func (r *Repo) ChangePassphrase(keyRole string) error { + if p, ok := r.local.(PassphraseChanger); ok { + return p.ChangePassphrase(keyRole) + } + + return ErrChangePassphraseNotSupported +} + +func (r *Repo) GenKey(role string) ([]string, error) { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + + return r.GenKeyWithExpires(role, data.DefaultExpires(role)) +} + +func (r *Repo) GenKeyWithExpires(keyRole string, expires time.Time) (keyids []string, err error) { + return r.GenKeyWithSchemeAndExpires(keyRole, expires, data.KeySchemeEd25519) +} + +func (r *Repo) GenKeyWithSchemeAndExpires(role string, expires time.Time, keyScheme data.KeyScheme) ([]string, error) { + var signer keys.Signer + var err error + switch keyScheme { + case data.KeySchemeEd25519: + signer, err = keys.GenerateEd25519Key() + case data.KeySchemeECDSA_SHA2_P256: + signer, err = keys.GenerateEcdsaKey() + case data.KeySchemeRSASSA_PSS_SHA256: + signer, err = keys.GenerateRsaKey() + default: + return nil, errors.New("unknown key type") + } + if err != nil { + return nil, err + } + + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + + if err = r.AddPrivateKeyWithExpires(role, signer, expires); err != nil { + return nil, err + } + return signer.PublicData().IDs(), nil +} + +func (r *Repo) AddPrivateKey(role string, signer keys.Signer) error { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + + return r.AddPrivateKeyWithExpires(role, signer, data.DefaultExpires(role)) +} + +func (r *Repo) AddPrivateKeyWithExpires(keyRole string, signer keys.Signer, expires time.Time) error { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + + if roles.IsDelegatedTargetsRole(keyRole) { + return ErrInvalidRole{keyRole, "only support adding keys for top-level roles"} + } + + if !validExpires(expires) { + return ErrInvalidExpires{expires} + } + + // Must add signer before adding verification key, so + // root.json can be signed when a new root key is added. + if err := r.local.SaveSigner(keyRole, signer); err != nil { + return err + } + + if err := r.AddVerificationKeyWithExpiration(keyRole, signer.PublicData(), expires); err != nil { + return err + } + + return nil +} + +func (r *Repo) AddVerificationKey(keyRole string, pk *data.PublicKey) error { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + + return r.AddVerificationKeyWithExpiration(keyRole, pk, data.DefaultExpires(keyRole)) +} + +func (r *Repo) AddVerificationKeyWithExpiration(keyRole string, pk *data.PublicKey, expires time.Time) error { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + + if roles.IsDelegatedTargetsRole(keyRole) { + return ErrInvalidRole{ + Role: keyRole, + Reason: "only top-level targets roles are supported", + } + } + + if !validExpires(expires) { + return ErrInvalidExpires{expires} + } + + root, err := r.root() + if err != nil { + return err + } + + role, ok := root.Roles[keyRole] + if !ok { + role = &data.Role{KeyIDs: []string{}, Threshold: 1} + root.Roles[keyRole] = role + } + changed := false + if role.AddKeyIDs(pk.IDs()) { + changed = true + } + + if root.AddKey(pk) { + changed = true + } + + if !changed { + return nil + } + + root.Expires = expires.Round(time.Second) + if !r.local.FileIsStaged("root.json") { + root.Version++ + } + + return r.setMeta("root.json", root) +} + +func validExpires(expires time.Time) bool { + return time.Until(expires) > 0 +} + +func (r *Repo) RootKeys() ([]*data.PublicKey, error) { + root, err := r.root() + if err != nil { + return nil, err + } + role, ok := root.Roles["root"] + if !ok { + return nil, nil + } + + // We might have multiple key ids that correspond to the same key, so + // make sure we only return unique keys. + seen := make(map[string]struct{}) + rootKeys := []*data.PublicKey{} + for _, id := range role.KeyIDs { + key, ok := root.Keys[id] + if !ok { + return nil, fmt.Errorf("tuf: invalid root metadata") + } + found := false + if _, ok := seen[id]; ok { + found = true + break + } + if !found { + for _, id := range key.IDs() { + seen[id] = struct{}{} + } + rootKeys = append(rootKeys, key) + } + } + return rootKeys, nil +} + +func (r *Repo) RevokeKey(role, id string) error { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + + return r.RevokeKeyWithExpires(role, id, data.DefaultExpires("root")) +} + +func (r *Repo) RevokeKeyWithExpires(keyRole, id string, expires time.Time) error { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + + if roles.IsDelegatedTargetsRole(keyRole) { + return ErrInvalidRole{keyRole, "only revocations for top-level roles supported"} + } + + if !validExpires(expires) { + return ErrInvalidExpires{expires} + } + + root, err := r.root() + if err != nil { + return err + } + + key, ok := root.Keys[id] + if !ok { + return ErrKeyNotFound{keyRole, id} + } + + role, ok := root.Roles[keyRole] + if !ok { + return ErrKeyNotFound{keyRole, id} + } + + // Create a list of filtered key IDs that do not contain the revoked key IDs. + filteredKeyIDs := make([]string, 0, len(role.KeyIDs)) + + // There may be multiple keyids that correspond to this key, so + // filter all of them out. + for _, keyID := range role.KeyIDs { + if !key.ContainsID(keyID) { + filteredKeyIDs = append(filteredKeyIDs, keyID) + } + } + if len(filteredKeyIDs) == len(role.KeyIDs) { + return ErrKeyNotFound{keyRole, id} + } + role.KeyIDs = filteredKeyIDs + root.Roles[keyRole] = role + + // Only delete the key from root.Keys if the key is no longer in use by + // any other role. + key_in_use := false + for _, role := range root.Roles { + for _, keyID := range role.KeyIDs { + if key.ContainsID(keyID) { + key_in_use = true + } + } + } + if !key_in_use { + for _, keyID := range key.IDs() { + delete(root.Keys, keyID) + } + } + root.Expires = expires.Round(time.Second) + if !r.local.FileIsStaged("root.json") { + root.Version++ + } + + err = r.setMeta("root.json", root) + if err == nil { + r.logger.Println("Revoked", keyRole, "key with ID", id, "in root metadata") + } + return err +} + +// AddDelegatedRole is equivalent to AddDelegatedRoleWithExpires, but +// with a default expiration time. +func (r *Repo) AddDelegatedRole(delegator string, delegatedRole data.DelegatedRole, keys []*data.PublicKey) error { + return r.AddDelegatedRoleWithExpires(delegator, delegatedRole, keys, data.DefaultExpires("targets")) +} + +// AddDelegatedRoleWithExpires adds a delegation from the delegator to the +// role specified in the role argument. Key IDs referenced in role.KeyIDs +// should have corresponding Key entries in the keys argument. New metadata is +// written with the given expiration time. +func (r *Repo) AddDelegatedRoleWithExpires(delegator string, delegatedRole data.DelegatedRole, keys []*data.PublicKey, expires time.Time) error { + expires = expires.Round(time.Second) + + t, err := r.targets(delegator) + if err != nil { + return fmt.Errorf("error getting delegator (%q) metadata: %w", delegator, err) + } + + if t.Delegations == nil { + t.Delegations = &data.Delegations{} + t.Delegations.Keys = make(map[string]*data.PublicKey) + } + + for _, keyID := range delegatedRole.KeyIDs { + for _, key := range keys { + if key.ContainsID(keyID) { + t.Delegations.Keys[keyID] = key + break + } + } + } + + for _, r := range t.Delegations.Roles { + if r.Name == delegatedRole.Name { + return fmt.Errorf("role: %s is already delegated to by %s", delegatedRole.Name, r.Name) + } + } + t.Delegations.Roles = append(t.Delegations.Roles, delegatedRole) + t.Expires = expires + + delegatorFile := delegator + ".json" + if !r.local.FileIsStaged(delegatorFile) { + t.Version++ + } + + err = r.setMeta(delegatorFile, t) + if err != nil { + return fmt.Errorf("error setting metadata for %q: %w", delegatorFile, err) + } + + delegatee := delegatedRole.Name + dt, err := r.targets(delegatee) + if err != nil { + return fmt.Errorf("error getting delegatee (%q) metadata: %w", delegatee, err) + } + dt.Expires = expires + + delegateeFile := delegatee + ".json" + if !r.local.FileIsStaged(delegateeFile) { + dt.Version++ + } + + err = r.setMeta(delegateeFile, dt) + if err != nil { + return fmt.Errorf("error setting metadata for %q: %w", delegateeFile, err) + } + + return nil +} + +// AddDelegatedRolesForPathHashBins is equivalent to +// AddDelegatedRolesForPathHashBinsWithExpires, but with a default +// expiration time. +func (r *Repo) AddDelegatedRolesForPathHashBins(delegator string, bins *targets.HashBins, keys []*data.PublicKey, threshold int) error { + return r.AddDelegatedRolesForPathHashBinsWithExpires(delegator, bins, keys, threshold, data.DefaultExpires("targets")) +} + +// AddDelegatedRolesForPathHashBinsWithExpires adds delegations to the +// delegator role for the given hash bins configuration. New metadata is +// written with the given expiration time. +func (r *Repo) AddDelegatedRolesForPathHashBinsWithExpires(delegator string, bins *targets.HashBins, keys []*data.PublicKey, threshold int, expires time.Time) error { + keyIDs := []string{} + for _, key := range keys { + keyIDs = append(keyIDs, key.IDs()...) + } + + n := bins.NumBins() + for i := uint64(0); i < n; i += 1 { + bin := bins.GetBin(i) + name := bin.RoleName() + err := r.AddDelegatedRoleWithExpires(delegator, data.DelegatedRole{ + Name: name, + KeyIDs: sets.DeduplicateStrings(keyIDs), + PathHashPrefixes: bin.HashPrefixes(), + Threshold: threshold, + }, keys, expires) + if err != nil { + return fmt.Errorf("error adding delegation from %v to %v: %w", delegator, name, err) + } + } + + return nil +} + +// ResetTargetsDelegation is equivalent to ResetTargetsDelegationsWithExpires +// with a default expiry time. +func (r *Repo) ResetTargetsDelegations(delegator string) error { + return r.ResetTargetsDelegationsWithExpires(delegator, data.DefaultExpires("targets")) +} + +// ResetTargetsDelegationsWithExpires removes all targets delegations from the +// given delegator role. New metadata is written with the given expiration +// time. +func (r *Repo) ResetTargetsDelegationsWithExpires(delegator string, expires time.Time) error { + t, err := r.targets(delegator) + if err != nil { + return fmt.Errorf("error getting delegator (%q) metadata: %w", delegator, err) + } + + t.Delegations = nil + + t.Expires = expires.Round(time.Second) + + delegatorFile := delegator + ".json" + if !r.local.FileIsStaged(delegatorFile) { + t.Version++ + } + + err = r.setMeta(delegatorFile, t) + if err != nil { + return fmt.Errorf("error setting metadata for %q: %w", delegatorFile, err) + } + + return nil +} + +func (r *Repo) jsonMarshal(v interface{}) ([]byte, error) { + if r.prefix == "" && r.indent == "" { + return json.Marshal(v) + } + return json.MarshalIndent(v, r.prefix, r.indent) +} + +func (r *Repo) dbsForRole(role string) ([]*verify.DB, error) { + dbs := []*verify.DB{} + + if roles.IsTopLevelRole(role) { + db, err := r.topLevelKeysDB() + if err != nil { + return nil, err + } + dbs = append(dbs, db) + } else { + ddbs, err := r.delegatorDBs(role) + if err != nil { + return nil, err + } + + dbs = append(dbs, ddbs...) + } + + return dbs, nil +} + +func (r *Repo) signersForRole(role string) ([]keys.Signer, error) { + dbs, err := r.dbsForRole(role) + if err != nil { + return nil, err + } + + signers := []keys.Signer{} + for _, db := range dbs { + ss, err := r.getSignersInDB(role, db) + if err != nil { + return nil, err + } + + signers = append(signers, ss...) + } + + return signers, nil +} + +func (r *Repo) setMeta(roleFilename string, meta interface{}) error { + role := strings.TrimSuffix(roleFilename, ".json") + + signers, err := r.signersForRole(role) + if err != nil { + return err + } + + s, err := sign.Marshal(meta, signers...) + if err != nil { + return err + } + b, err := r.jsonMarshal(s) + if err != nil { + return err + } + r.meta[roleFilename] = b + return r.local.SetMeta(roleFilename, b) +} + +// CanonicalizeAndSign canonicalizes the signed portion of signed, then signs it using the key(s) associated with role. +// +// It appends the signature to signed. +// +// It returns the total number of keys used for signing, 0 (along with +// ErrNoKeys) if no keys were found, or -1 (along with an error) in error cases. +func (r *Repo) CanonicalizeAndSign(role string, signed *data.Signed) (int, error) { + keys, err := r.signersForRole(role) + if err != nil { + return -1, err + } + if len(keys) == 0 { + return 0, ErrNoKeys{role} + } + for _, k := range keys { + if err = sign.Sign(signed, k); err != nil { + return -1, err + } + } + return len(keys), nil +} + +// SignPayload canonicalizes the signed portion of payload, then signs it using the key(s) associated with role. +// +// It returns the total number of keys used for signing, 0 (along with +// ErrNoKeys) if no keys were found, or -1 (along with an error) in error cases. +// +// DEPRECATED: please use CanonicalizeAndSign instead. +func (r *Repo) SignPayload(role string, payload *data.Signed) (int, error) { + return r.CanonicalizeAndSign(role, payload) +} + +// SignRaw signs the given (pre-canonicalized) payload using the key(s) associated with role. +// +// It returns the new data.Signatures. +func (r *Repo) SignRaw(role string, payload []byte) ([]data.Signature, error) { + keys, err := r.signersForRole(role) + if err != nil { + return nil, err + } + if len(keys) == 0 { + return nil, ErrNoKeys{role} + } + + allSigs := make([]data.Signature, 0, len(keys)) + for _, k := range keys { + sigs, err := sign.MakeSignatures(payload, k) + if err != nil { + return nil, err + } + allSigs = append(allSigs, sigs...) + } + return allSigs, nil +} + +func (r *Repo) Sign(roleFilename string) error { + signed, err := r.SignedMeta(roleFilename) + if err != nil { + return err + } + + role := strings.TrimSuffix(roleFilename, ".json") + numKeys, err := r.SignPayload(role, signed) + if errors.Is(err, ErrNoKeys{role}) { + return ErrNoKeys{roleFilename} + } else if err != nil { + return err + } + + b, err := r.jsonMarshal(signed) + if err != nil { + return err + } + r.meta[roleFilename] = b + err = r.local.SetMeta(roleFilename, b) + if err == nil { + r.logger.Println("Signed", roleFilename, "with", numKeys, "key(s)") + } + return err +} + +// AddOrUpdateSignature allows users to add or update a signature generated with an external tool. +// The name must be a valid metadata file name, like root.json. +func (r *Repo) AddOrUpdateSignature(roleFilename string, signature data.Signature) error { + role := strings.TrimSuffix(roleFilename, ".json") + + // Check key ID is in valid for the role. + dbs, err := r.dbsForRole(role) + if err != nil { + return err + } + + if len(dbs) == 0 { + return ErrInvalidRole{role, "no trusted keys for role"} + } + + s, err := r.SignedMeta(roleFilename) + if err != nil { + return err + } + + keyInDB := false + validSig := false + for _, db := range dbs { + roleData := db.GetRole(role) + if roleData == nil { + return ErrInvalidRole{role, "role is not in verifier DB"} + } + if roleData.ValidKey(signature.KeyID) { + keyInDB = true + + verifier, err := db.GetVerifier(signature.KeyID) + if err != nil { + continue + } + + // Now check if this validly signed the metadata. + if err := verify.VerifySignature(s.Signed, signature.Signature, + verifier); err == nil { + validSig = true + break + } + } + } + if !keyInDB { + // This key was not delegated for the role in any delegatee. + return verify.ErrInvalidKey + } + if !validSig { + // The signature was invalid. + return verify.ErrInvalid + } + + // Add or update signature. + signatures := make([]data.Signature, 0, len(s.Signatures)+1) + for _, sig := range s.Signatures { + if sig.KeyID != signature.KeyID { + signatures = append(signatures, sig) + } + } + signatures = append(signatures, signature) + s.Signatures = signatures + + b, err := r.jsonMarshal(s) + if err != nil { + return err + } + r.meta[roleFilename] = b + + return r.local.SetMeta(roleFilename, b) +} + +// getSignersInDB returns available signing interfaces, sorted by key ID. +// +// Only keys contained in the keys db are returned (i.e. local keys which have +// been revoked are omitted), except for the root role in which case all local +// keys are returned (revoked root keys still need to sign new root metadata so +// clients can verify the new root.json and update their keys db accordingly). +func (r *Repo) getSignersInDB(roleName string, db *verify.DB) ([]keys.Signer, error) { + signers, err := r.local.GetSigners(roleName) + if err != nil { + return nil, err + } + + if roleName == "root" { + sorted := make([]keys.Signer, len(signers)) + copy(sorted, signers) + sort.Sort(signer.ByIDs(sorted)) + return sorted, nil + } + + role := db.GetRole(roleName) + if role == nil { + return nil, nil + } + if len(role.KeyIDs) == 0 { + return nil, nil + } + + signersInDB := []keys.Signer{} + for _, s := range signers { + for _, id := range s.PublicData().IDs() { + if _, ok := role.KeyIDs[id]; ok { + signersInDB = append(signersInDB, s) + } + } + } + + sort.Sort(signer.ByIDs(signersInDB)) + + return signersInDB, nil +} + +// Used to retrieve the signable portion of the metadata when using an external signing tool. +func (r *Repo) SignedMeta(roleFilename string) (*data.Signed, error) { + b, ok := r.meta[roleFilename] + if !ok { + return nil, ErrMissingMetadata{roleFilename} + } + s := &data.Signed{} + if err := json.Unmarshal(b, s); err != nil { + return nil, err + } + return s, nil +} + +// delegatorDBs returns a list of key DBs for all incoming delegations. +func (r *Repo) delegatorDBs(delegateeRole string) ([]*verify.DB, error) { + delegatorDBs := []*verify.DB{} + for metaName := range r.meta { + if roles.IsTopLevelManifest(metaName) && metaName != "targets.json" { + continue + } + roleName := strings.TrimSuffix(metaName, ".json") + + t, err := r.targets(roleName) + if err != nil { + return nil, err + } + + if t.Delegations == nil { + continue + } + + delegatesToRole := false + for _, d := range t.Delegations.Roles { + if d.Name == delegateeRole { + delegatesToRole = true + break + } + } + if !delegatesToRole { + continue + } + + db, err := verify.NewDBFromDelegations(t.Delegations) + if err != nil { + return nil, err + } + + delegatorDBs = append(delegatorDBs, db) + } + + return delegatorDBs, nil +} + +// targetDelegationForPath finds the targets metadata for the role that should +// sign the given path. The final delegation that led to the returned target +// metadata is also returned. +// +// Since there may be multiple targets roles that are able to sign a specific +// path, we must choose which roles's metadata to return. If preferredRole is +// specified (non-empty string) and eligible to sign the given path by way of +// some delegation chain, targets metadata for that role is returned. If +// preferredRole is not specified (""), we return targets metadata for the +// final role visited in the depth-first delegation traversal. +func (r *Repo) targetDelegationForPath(path string, preferredRole string) (*data.Targets, *targets.Delegation, error) { + topLevelKeysDB, err := r.topLevelKeysDB() + if err != nil { + return nil, nil, err + } + + iterator, err := targets.NewDelegationsIterator(path, topLevelKeysDB) + if err != nil { + return nil, nil, err + } + d, ok := iterator.Next() + if !ok { + return nil, nil, ErrNoDelegatedTarget{Path: path} + } + + for i := 0; i < defaultMaxDelegations; i++ { + targetsMeta, err := r.targets(d.Delegatee.Name) + if err != nil { + return nil, nil, err + } + + if preferredRole != "" && d.Delegatee.Name == preferredRole { + // The preferredRole is eligible to sign for the given path, and we've + // found its metadata. Return it. + return targetsMeta, &d, nil + } + + if targetsMeta.Delegations != nil && len(targetsMeta.Delegations.Roles) > 0 { + db, err := verify.NewDBFromDelegations(targetsMeta.Delegations) + if err != nil { + return nil, nil, err + } + + // Add delegations to the iterator that are eligible to sign for the + // given path (there may be none). + iterator.Add(targetsMeta.Delegations.Roles, d.Delegatee.Name, db) + } + + next, ok := iterator.Next() + if !ok { // No more roles to traverse. + if preferredRole == "" { + // No preferredRole was given, so return metadata for the final role in the traversal. + return targetsMeta, &d, nil + } else { + // There are no more roles to traverse, so preferredRole is either an + // invalid role, or not eligible to sign the given path. + return nil, nil, ErrNoDelegatedTarget{Path: path} + } + } + + d = next + } + + return nil, nil, ErrNoDelegatedTarget{Path: path} +} + +func (r *Repo) AddTarget(path string, custom json.RawMessage) error { + return r.AddTargets([]string{path}, custom) +} + +func (r *Repo) AddTargetToPreferredRole(path string, custom json.RawMessage, preferredRole string) error { + return r.AddTargetsToPreferredRole([]string{path}, custom, preferredRole) +} + +func (r *Repo) AddTargets(paths []string, custom json.RawMessage) error { + return r.AddTargetsToPreferredRole(paths, custom, "") +} + +func (r *Repo) AddTargetsToPreferredRole(paths []string, custom json.RawMessage, preferredRole string) error { + return r.AddTargetsWithExpiresToPreferredRole(paths, custom, data.DefaultExpires("targets"), preferredRole) +} + +func (r *Repo) AddTargetsWithDigest(digest string, digestAlg string, length int64, path string, custom json.RawMessage) error { + // TODO: Rename this to AddTargetWithDigest + // https://github.com/theupdateframework/go-tuf/issues/242 + + expires := data.DefaultExpires("targets") + path = util.NormalizeTarget(path) + + targetsMeta, delegation, err := r.targetDelegationForPath(path, "") + if err != nil { + return err + } + // This is the targets role that needs to sign the target file. + targetsRoleName := delegation.Delegatee.Name + + meta := data.TargetFileMeta{FileMeta: data.FileMeta{Length: length, Hashes: make(data.Hashes, 1)}} + meta.Hashes[digestAlg], err = hex.DecodeString(digest) + if err != nil { + return err + } + + // If custom is provided, set custom, otherwise maintain existing custom + // metadata + if len(custom) > 0 { + meta.Custom = &custom + } else if t, ok := targetsMeta.Targets[path]; ok { + meta.Custom = t.Custom + } + + // What does G2 mean? Copying and pasting this comment from elsewhere in this file. + // G2 -> we no longer desire any readers to ever observe non-prefix targets. + delete(targetsMeta.Targets, "/"+path) + targetsMeta.Targets[path] = meta + + targetsMeta.Expires = expires.Round(time.Second) + + manifestName := targetsRoleName + ".json" + if !r.local.FileIsStaged(manifestName) { + targetsMeta.Version++ + } + + err = r.setMeta(manifestName, targetsMeta) + if err != nil { + return fmt.Errorf("error setting metadata for %q: %w", manifestName, err) + } + + return nil +} + +func (r *Repo) AddTargetWithExpires(path string, custom json.RawMessage, expires time.Time) error { + return r.AddTargetsWithExpires([]string{path}, custom, expires) +} + +func (r *Repo) AddTargetsWithExpires(paths []string, custom json.RawMessage, expires time.Time) error { + return r.AddTargetsWithExpiresToPreferredRole(paths, custom, expires, "") +} + +func (r *Repo) AddTargetWithExpiresToPreferredRole(path string, custom json.RawMessage, expires time.Time, preferredRole string) error { + return r.AddTargetsWithExpiresToPreferredRole([]string{path}, custom, expires, preferredRole) +} + +// AddTargetsWithExpiresToPreferredRole signs the staged targets at `paths`. +// +// If preferredRole is not the empty string, the target is added to the given +// role's manifest if delegations allow it. If delegations do not allow the +// preferredRole to sign the given path, an error is returned. +func (r *Repo) AddTargetsWithExpiresToPreferredRole(paths []string, custom json.RawMessage, expires time.Time, preferredRole string) error { + if !validExpires(expires) { + return ErrInvalidExpires{expires} + } + + normalizedPaths := make([]string, len(paths)) + for i, path := range paths { + normalizedPaths[i] = util.NormalizeTarget(path) + } + + // As we iterate through staged targets files, we accumulate changes to their + // corresponding targets metadata. + updatedTargetsMeta := map[string]*data.Targets{} + + if err := r.local.WalkStagedTargets(normalizedPaths, func(path string, target io.Reader) (err error) { + originalMeta, delegation, err := r.targetDelegationForPath(path, preferredRole) + if err != nil { + return err + } + + // This is the targets role that needs to sign the target file. + targetsRoleName := delegation.Delegatee.Name + + targetsMeta := originalMeta + if tm, ok := updatedTargetsMeta[targetsRoleName]; ok { + // Metadata in updatedTargetsMeta overrides staged/commited metadata. + targetsMeta = tm + } + + fileMeta, err := util.GenerateTargetFileMeta(target, r.hashAlgorithms...) + if err != nil { + return err + } + + // If we have custom metadata, set it, otherwise maintain + // existing metadata if present + if len(custom) > 0 { + fileMeta.Custom = &custom + } else if tf, ok := targetsMeta.Targets[path]; ok { + fileMeta.Custom = tf.Custom + } + + // G2 -> we no longer desire any readers to ever observe non-prefix targets. + delete(targetsMeta.Targets, "/"+path) + targetsMeta.Targets[path] = fileMeta + + updatedTargetsMeta[targetsRoleName] = targetsMeta + + return nil + }); err != nil { + return err + } + + if len(updatedTargetsMeta) == 0 { + // This is potentially unexpected behavior kept for backwards compatibility. + // See https://github.com/theupdateframework/go-tuf/issues/243 + t, err := r.topLevelTargets() + if err != nil { + return err + } + + updatedTargetsMeta["targets"] = t + } + + exp := expires.Round(time.Second) + for roleName, targetsMeta := range updatedTargetsMeta { + targetsMeta.Expires = exp + + manifestName := roleName + ".json" + if !r.local.FileIsStaged(manifestName) { + targetsMeta.Version++ + } + + err := r.setMeta(manifestName, targetsMeta) + if err != nil { + return fmt.Errorf("error setting metadata for %q: %w", manifestName, err) + } + } + + return nil +} + +func (r *Repo) RemoveTarget(path string) error { + return r.RemoveTargets([]string{path}) +} + +func (r *Repo) RemoveTargets(paths []string) error { + return r.RemoveTargetsWithExpires(paths, data.DefaultExpires("targets")) +} + +func (r *Repo) RemoveTargetWithExpires(path string, expires time.Time) error { + return r.RemoveTargetsWithExpires([]string{path}, expires) +} + +// If paths is empty, all targets will be removed. +func (r *Repo) RemoveTargetsWithExpires(paths []string, expires time.Time) error { + if !validExpires(expires) { + return ErrInvalidExpires{expires} + } + + for metaName := range r.meta { + if metaName != "targets.json" && !roles.IsDelegatedTargetsManifest(metaName) { + continue + } + + err := r.removeTargetsWithExpiresFromMeta(metaName, paths, expires) + if err != nil { + return fmt.Errorf("could not remove %v from %v: %w", paths, metaName, err) + } + } + + return nil +} + +func (r *Repo) removeTargetsWithExpiresFromMeta(metaName string, paths []string, expires time.Time) error { + roleName := strings.TrimSuffix(metaName, ".json") + t, err := r.targets(roleName) + if err != nil { + return err + } + removed_targets := []string{} + if len(paths) == 0 { + for rt := range t.Targets { + removed_targets = append(removed_targets, rt) + } + t.Targets = make(data.TargetFiles) + } else { + removed := false + for _, path := range paths { + path = util.NormalizeTarget(path) + if _, ok := t.Targets[path]; !ok { + r.logger.Printf("[%v] The following target is not present: %v\n", metaName, path) + continue + } + removed = true + // G2 -> we no longer desire any readers to ever observe non-prefix targets. + delete(t.Targets, "/"+path) + delete(t.Targets, path) + removed_targets = append(removed_targets, path) + } + if !removed { + return nil + } + } + t.Expires = expires.Round(time.Second) + if !r.local.FileIsStaged(metaName) { + t.Version++ + } + + err = r.setMeta(metaName, t) + if err == nil { + r.logger.Printf("[%v] Removed targets:\n", metaName) + for _, v := range removed_targets { + r.logger.Println("*", v) + } + if len(t.Targets) != 0 { + r.logger.Printf("[%v] Added/staged targets:\n", metaName) + for k := range t.Targets { + r.logger.Println("*", k) + } + } else { + r.logger.Printf("[%v] There are no added/staged targets\n", metaName) + } + } + return err +} + +func (r *Repo) Snapshot() error { + return r.SnapshotWithExpires(data.DefaultExpires("snapshot")) +} + +func (r *Repo) snapshotMetadata() []string { + ret := []string{"targets.json"} + + for name := range r.meta { + if !roles.IsVersionedManifest(name) && + roles.IsDelegatedTargetsManifest(name) { + ret = append(ret, name) + } + } + + return ret +} + +func (r *Repo) SnapshotWithExpires(expires time.Time) error { + if !validExpires(expires) { + return ErrInvalidExpires{expires} + } + + snapshot, err := r.snapshot() + if err != nil { + return err + } + + // Verify root metadata before verifying signatures on role metadata. + if err := r.verifySignatures("root.json"); err != nil { + return err + } + + for _, metaName := range r.snapshotMetadata() { + if err := r.verifySignatures(metaName); err != nil { + return err + } + var err error + snapshot.Meta[metaName], err = r.snapshotFileMeta(metaName) + if err != nil { + return err + } + } + snapshot.Expires = expires.Round(time.Second) + if !r.local.FileIsStaged("snapshot.json") { + snapshot.Version++ + } + err = r.setMeta("snapshot.json", snapshot) + if err == nil { + r.logger.Println("Staged snapshot.json metadata with expiration date:", snapshot.Expires) + } + return err +} + +func (r *Repo) Timestamp() error { + return r.TimestampWithExpires(data.DefaultExpires("timestamp")) +} + +func (r *Repo) TimestampWithExpires(expires time.Time) error { + if !validExpires(expires) { + return ErrInvalidExpires{expires} + } + + if err := r.verifySignatures("snapshot.json"); err != nil { + return err + } + timestamp, err := r.timestamp() + if err != nil { + return err + } + timestamp.Meta["snapshot.json"], err = r.timestampFileMeta("snapshot.json") + if err != nil { + return err + } + timestamp.Expires = expires.Round(time.Second) + if !r.local.FileIsStaged("timestamp.json") { + timestamp.Version++ + } + + err = r.setMeta("timestamp.json", timestamp) + if err == nil { + r.logger.Println("Staged timestamp.json metadata with expiration date:", timestamp.Expires) + } + return err +} + +func (r *Repo) fileVersions() (map[string]int64, error) { + versions := make(map[string]int64) + + for fileName := range r.meta { + if roles.IsVersionedManifest(fileName) { + continue + } + + roleName := strings.TrimSuffix(fileName, ".json") + + var version int64 + + switch roleName { + case "root": + root, err := r.root() + if err != nil { + return nil, err + } + version = root.Version + case "snapshot": + snapshot, err := r.snapshot() + if err != nil { + return nil, err + } + version = snapshot.Version + case "timestamp": + continue + default: + // Targets or delegated targets manifest. + targets, err := r.targets(roleName) + if err != nil { + return nil, err + } + + version = targets.Version + } + + versions[fileName] = version + } + + return versions, nil +} + +func (r *Repo) fileHashes() (map[string]data.Hashes, error) { + hashes := make(map[string]data.Hashes) + + for fileName := range r.meta { + if roles.IsVersionedManifest(fileName) { + continue + } + + roleName := strings.TrimSuffix(fileName, ".json") + + switch roleName { + case "snapshot": + timestamp, err := r.timestamp() + if err != nil { + return nil, err + } + + if m, ok := timestamp.Meta[fileName]; ok { + hashes[fileName] = m.Hashes + } + case "timestamp": + continue + default: + snapshot, err := r.snapshot() + if err != nil { + return nil, err + } + if m, ok := snapshot.Meta[fileName]; ok { + hashes[fileName] = m.Hashes + } + + if roleName != "root" { + // Scalability issue: Commit/fileHashes loads all targets metadata into memory + // https://github.com/theupdateframework/go-tuf/issues/245 + t, err := r.targets(roleName) + if err != nil { + return nil, err + } + for name, m := range t.Targets { + hashes[path.Join("targets", name)] = m.Hashes + } + } + + } + + } + + return hashes, nil +} + +func (r *Repo) Commit() error { + // check we have all the metadata + for _, name := range topLevelMetadata { + if _, ok := r.meta[name]; !ok { + return ErrMissingMetadata{name} + } + } + + // check roles are valid + root, err := r.root() + if err != nil { + return err + } + for name, role := range root.Roles { + if len(role.KeyIDs) < role.Threshold { + return ErrNotEnoughKeys{name, len(role.KeyIDs), role.Threshold} + } + } + + // verify hashes in snapshot.json are up to date + snapshot, err := r.snapshot() + if err != nil { + return err + } + for _, name := range r.snapshotMetadata() { + expected, ok := snapshot.Meta[name] + if !ok { + return fmt.Errorf("tuf: snapshot.json missing hash for %s", name) + } + actual, err := r.snapshotFileMeta(name) + if err != nil { + return err + } + if err := util.SnapshotFileMetaEqual(actual, expected); err != nil { + return fmt.Errorf("tuf: invalid %s in snapshot.json: %s", name, err) + } + } + + // verify hashes in timestamp.json are up to date + timestamp, err := r.timestamp() + if err != nil { + return err + } + snapshotMeta, err := r.timestampFileMeta("snapshot.json") + if err != nil { + return err + } + if err := util.TimestampFileMetaEqual(snapshotMeta, timestamp.Meta["snapshot.json"]); err != nil { + return fmt.Errorf("tuf: invalid snapshot.json in timestamp.json: %s", err) + } + + for _, name := range topLevelMetadata { + if err := r.verifySignatures(name); err != nil { + return err + } + } + + versions, err := r.fileVersions() + if err != nil { + return err + } + hashes, err := r.fileHashes() + if err != nil { + return err + } + + err = r.local.Commit(root.ConsistentSnapshot, versions, hashes) + if err == nil { + r.logger.Println("Committed successfully") + } + return err +} + +func (r *Repo) Clean() error { + err := r.local.Clean() + if err == nil { + r.logger.Println("Removed all staged metadata and target files") + } + return err +} + +func (r *Repo) verifySignatures(metaFilename string) error { + s, err := r.SignedMeta(metaFilename) + if err != nil { + return err + } + + role := strings.TrimSuffix(metaFilename, ".json") + + dbs, err := r.dbsForRole(role) + if err != nil { + return err + } + + for _, db := range dbs { + if err := db.Verify(s, role, 0); err != nil { + return ErrInsufficientSignatures{metaFilename, err} + } + } + + return nil +} + +func (r *Repo) snapshotFileMeta(roleFilename string) (data.SnapshotFileMeta, error) { + b, ok := r.meta[roleFilename] + if !ok { + return data.SnapshotFileMeta{}, ErrMissingMetadata{roleFilename} + } + return util.GenerateSnapshotFileMeta(bytes.NewReader(b), r.hashAlgorithms...) +} + +func (r *Repo) timestampFileMeta(roleFilename string) (data.TimestampFileMeta, error) { + b, ok := r.meta[roleFilename] + if !ok { + return data.TimestampFileMeta{}, ErrMissingMetadata{roleFilename} + } + return util.GenerateTimestampFileMeta(bytes.NewReader(b), r.hashAlgorithms...) +} + +func (r *Repo) Payload(roleFilename string) ([]byte, error) { + s, err := r.SignedMeta(roleFilename) + if err != nil { + return nil, err + } + + p, err := cjson.EncodeCanonical(s.Signed) + if err != nil { + return nil, err + } + + return p, nil +} + +func (r *Repo) CheckRoleUnexpired(role string, validAt time.Time) error { + var expires time.Time + switch role { + case "root": + root, err := r.root() + if err != nil { + return err + } + expires = root.Expires + case "snapshot": + snapshot, err := r.snapshot() + if err != nil { + return err + } + expires = snapshot.Expires + case "timestamp": + timestamp, err := r.timestamp() + if err != nil { + return err + } + expires = timestamp.Expires + case "targets": + targets, err := r.topLevelTargets() + if err != nil { + return err + } + expires = targets.Expires + default: + return fmt.Errorf("invalid role: %s", role) + } + if expires.Before(validAt) || expires.Equal(validAt) { + return fmt.Errorf("role expired on: %s", expires) + } + return nil +} + +// GetMeta returns the underlying meta file map from the store. +func (r *Repo) GetMeta() (map[string]json.RawMessage, error) { + return r.local.GetMeta() +} |