| package tuf |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| "io/fs" |
| "log" |
| "os" |
| "path/filepath" |
| "strings" |
| |
| "github.com/secure-systems-lab/go-securesystemslib/encrypted" |
| "github.com/theupdateframework/go-tuf/data" |
| "github.com/theupdateframework/go-tuf/internal/fsutil" |
| "github.com/theupdateframework/go-tuf/internal/sets" |
| "github.com/theupdateframework/go-tuf/pkg/keys" |
| "github.com/theupdateframework/go-tuf/util" |
| ) |
| |
| type LocalStore interface { |
| // GetMeta returns a map from metadata file names (e.g. root.json) to their raw JSON payload or an error. |
| GetMeta() (map[string]json.RawMessage, error) |
| |
| // SetMeta is used to update a metadata file name with a JSON payload. |
| SetMeta(name string, meta json.RawMessage) error |
| |
| // WalkStagedTargets calls targetsFn for each staged target file in paths. |
| // If paths is empty, all staged target files will be walked. |
| WalkStagedTargets(paths []string, targetsFn TargetsWalkFunc) error |
| |
| // FileIsStaged determines if a metadata file is currently staged, to avoid incrementing |
| // version numbers repeatedly while staged. |
| FileIsStaged(filename string) bool |
| |
| // Commit is used to publish staged files to the repository |
| // |
| // This will also reset the staged meta to signal incrementing version numbers. |
| // TUF 1.0 requires that the root metadata version numbers in the repository does not |
| // gaps. To avoid this, we will only increment the number once until we commit. |
| Commit(bool, map[string]int64, map[string]data.Hashes) error |
| |
| // GetSigners return a list of signers for a role. |
| // This may include revoked keys, so the signers should not |
| // be used without filtering. |
| GetSigners(role string) ([]keys.Signer, error) |
| |
| // SaveSigner adds a signer to a role. |
| SaveSigner(role string, signer keys.Signer) error |
| |
| // SignersForRole return a list of signing keys for a role. |
| SignersForKeyIDs(keyIDs []string) []keys.Signer |
| |
| // Clean is used to remove all staged manifests. |
| Clean() error |
| } |
| |
| type PassphraseChanger interface { |
| // ChangePassphrase changes the passphrase for a role keys file. |
| ChangePassphrase(string) error |
| } |
| |
| func MemoryStore(meta map[string]json.RawMessage, files map[string][]byte) LocalStore { |
| if meta == nil { |
| meta = make(map[string]json.RawMessage) |
| } |
| return &memoryStore{ |
| meta: meta, |
| stagedMeta: make(map[string]json.RawMessage), |
| files: files, |
| signerForKeyID: make(map[string]keys.Signer), |
| keyIDsForRole: make(map[string][]string), |
| } |
| } |
| |
| type memoryStore struct { |
| meta map[string]json.RawMessage |
| stagedMeta map[string]json.RawMessage |
| files map[string][]byte |
| |
| signerForKeyID map[string]keys.Signer |
| keyIDsForRole map[string][]string |
| } |
| |
| func (m *memoryStore) GetMeta() (map[string]json.RawMessage, error) { |
| meta := make(map[string]json.RawMessage, len(m.meta)+len(m.stagedMeta)) |
| for key, value := range m.meta { |
| meta[key] = value |
| } |
| for key, value := range m.stagedMeta { |
| meta[key] = value |
| } |
| return meta, nil |
| } |
| |
| func (m *memoryStore) SetMeta(name string, meta json.RawMessage) error { |
| m.stagedMeta[name] = meta |
| return nil |
| } |
| |
| func (m *memoryStore) FileIsStaged(name string) bool { |
| _, ok := m.stagedMeta[name] |
| return ok |
| } |
| |
| func (m *memoryStore) WalkStagedTargets(paths []string, targetsFn TargetsWalkFunc) error { |
| if len(paths) == 0 { |
| for path, data := range m.files { |
| if err := targetsFn(path, bytes.NewReader(data)); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| for _, path := range paths { |
| data, ok := m.files[path] |
| if !ok { |
| return ErrFileNotFound{path} |
| } |
| if err := targetsFn(path, bytes.NewReader(data)); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func (m *memoryStore) Commit(consistentSnapshot bool, versions map[string]int64, hashes map[string]data.Hashes) error { |
| for name, meta := range m.stagedMeta { |
| paths := computeMetadataPaths(consistentSnapshot, name, versions) |
| for _, path := range paths { |
| m.meta[path] = meta |
| } |
| // Remove from staged metadata. |
| // This will prompt incrementing version numbers again now that we've |
| // successfully committed the metadata to the local store. |
| delete(m.stagedMeta, name) |
| } |
| return nil |
| } |
| |
| func (m *memoryStore) GetSigners(role string) ([]keys.Signer, error) { |
| keyIDs, ok := m.keyIDsForRole[role] |
| if ok { |
| return m.SignersForKeyIDs(keyIDs), nil |
| } |
| |
| return nil, nil |
| } |
| |
| func (m *memoryStore) SaveSigner(role string, signer keys.Signer) error { |
| keyIDs := signer.PublicData().IDs() |
| |
| for _, keyID := range keyIDs { |
| m.signerForKeyID[keyID] = signer |
| } |
| |
| mergedKeyIDs := sets.DeduplicateStrings(append(m.keyIDsForRole[role], keyIDs...)) |
| m.keyIDsForRole[role] = mergedKeyIDs |
| return nil |
| } |
| |
| func (m *memoryStore) SignersForKeyIDs(keyIDs []string) []keys.Signer { |
| signers := []keys.Signer{} |
| keyIDsSeen := map[string]struct{}{} |
| |
| for _, keyID := range keyIDs { |
| signer, ok := m.signerForKeyID[keyID] |
| if !ok { |
| continue |
| } |
| addSigner := false |
| |
| for _, skid := range signer.PublicData().IDs() { |
| if _, seen := keyIDsSeen[skid]; !seen { |
| addSigner = true |
| } |
| |
| keyIDsSeen[skid] = struct{}{} |
| } |
| |
| if addSigner { |
| signers = append(signers, signer) |
| } |
| } |
| |
| return signers |
| } |
| |
| func (m *memoryStore) Clean() error { |
| return nil |
| } |
| |
| type persistedKeys struct { |
| Encrypted bool `json:"encrypted"` |
| Data json.RawMessage `json:"data"` |
| } |
| |
| type StoreOpts struct { |
| Logger *log.Logger |
| PassFunc util.PassphraseFunc |
| } |
| |
| func FileSystemStore(dir string, p util.PassphraseFunc) LocalStore { |
| return &fileSystemStore{ |
| dir: dir, |
| passphraseFunc: p, |
| logger: log.New(io.Discard, "", 0), |
| signerForKeyID: make(map[string]keys.Signer), |
| keyIDsForRole: make(map[string][]string), |
| } |
| } |
| |
| func FileSystemStoreWithOpts(dir string, opts ...StoreOpts) LocalStore { |
| store := &fileSystemStore{ |
| dir: dir, |
| passphraseFunc: nil, |
| logger: log.New(io.Discard, "", 0), |
| signerForKeyID: make(map[string]keys.Signer), |
| keyIDsForRole: make(map[string][]string), |
| } |
| for _, opt := range opts { |
| if opt.Logger != nil { |
| store.logger = opt.Logger |
| } |
| if opt.PassFunc != nil { |
| store.passphraseFunc = opt.PassFunc |
| } |
| } |
| return store |
| } |
| |
| type fileSystemStore struct { |
| dir string |
| passphraseFunc util.PassphraseFunc |
| logger *log.Logger |
| |
| signerForKeyID map[string]keys.Signer |
| keyIDsForRole map[string][]string |
| } |
| |
| func (f *fileSystemStore) repoDir() string { |
| return filepath.Join(f.dir, "repository") |
| } |
| |
| func (f *fileSystemStore) stagedDir() string { |
| return filepath.Join(f.dir, "staged") |
| } |
| |
| func (f *fileSystemStore) GetMeta() (map[string]json.RawMessage, error) { |
| // Build a map of metadata names (e.g. root.json) to their full paths |
| // (whether in the committed repo dir, or in the staged repo dir). |
| metaPaths := map[string]string{} |
| |
| rd := f.repoDir() |
| committed, err := os.ReadDir(f.repoDir()) |
| if err != nil && !errors.Is(err, fs.ErrNotExist) { |
| return nil, fmt.Errorf("could not list repo dir: %w", err) |
| } |
| |
| for _, e := range committed { |
| imf, err := fsutil.IsMetaFile(e) |
| if err != nil { |
| return nil, err |
| } |
| if imf { |
| name := e.Name() |
| metaPaths[name] = filepath.Join(rd, name) |
| } |
| } |
| |
| sd := f.stagedDir() |
| staged, err := os.ReadDir(sd) |
| if err != nil && !errors.Is(err, fs.ErrNotExist) { |
| return nil, fmt.Errorf("could not list staged dir: %w", err) |
| } |
| |
| for _, e := range staged { |
| imf, err := fsutil.IsMetaFile(e) |
| if err != nil { |
| return nil, err |
| } |
| if imf { |
| name := e.Name() |
| metaPaths[name] = filepath.Join(sd, name) |
| } |
| } |
| |
| meta := make(map[string]json.RawMessage) |
| for name, path := range metaPaths { |
| f, err := os.ReadFile(path) |
| if err != nil { |
| return nil, err |
| } |
| meta[name] = f |
| } |
| return meta, nil |
| } |
| |
| func (f *fileSystemStore) SetMeta(name string, meta json.RawMessage) error { |
| if err := f.createDirs(); err != nil { |
| return err |
| } |
| if err := util.AtomicallyWriteFile(filepath.Join(f.stagedDir(), name), meta, 0644); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| func (f *fileSystemStore) FileIsStaged(name string) bool { |
| _, err := os.Stat(filepath.Join(f.stagedDir(), name)) |
| return err == nil |
| } |
| |
| func (f *fileSystemStore) createDirs() error { |
| for _, dir := range []string{"keys", "repository", "staged/targets"} { |
| if err := os.MkdirAll(filepath.Join(f.dir, dir), 0755); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func (f *fileSystemStore) WalkStagedTargets(paths []string, targetsFn TargetsWalkFunc) error { |
| if len(paths) == 0 { |
| walkFunc := func(fpath string, info os.FileInfo, err error) error { |
| if err != nil { |
| return err |
| } |
| if info.IsDir() || !info.Mode().IsRegular() { |
| return nil |
| } |
| rel, err := filepath.Rel(filepath.Join(f.stagedDir(), "targets"), fpath) |
| if err != nil { |
| return err |
| } |
| file, err := os.Open(fpath) |
| if err != nil { |
| return err |
| } |
| defer file.Close() |
| return targetsFn(filepath.ToSlash(rel), file) |
| } |
| return filepath.Walk(filepath.Join(f.stagedDir(), "targets"), walkFunc) |
| } |
| |
| // check all the files exist before processing any files |
| for _, path := range paths { |
| realFilepath := filepath.Join(f.stagedDir(), "targets", path) |
| if _, err := os.Stat(realFilepath); err != nil { |
| if os.IsNotExist(err) { |
| return ErrFileNotFound{realFilepath} |
| } |
| return err |
| } |
| } |
| |
| for _, path := range paths { |
| realFilepath := filepath.Join(f.stagedDir(), "targets", path) |
| file, err := os.Open(realFilepath) |
| if err != nil { |
| if os.IsNotExist(err) { |
| return ErrFileNotFound{realFilepath} |
| } |
| return err |
| } |
| err = targetsFn(path, file) |
| file.Close() |
| if err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func (f *fileSystemStore) createRepoFile(path string) (*os.File, error) { |
| dst := filepath.Join(f.repoDir(), path) |
| if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { |
| return nil, err |
| } |
| return os.Create(dst) |
| } |
| |
| func (f *fileSystemStore) Commit(consistentSnapshot bool, versions map[string]int64, hashes map[string]data.Hashes) error { |
| isTarget := func(path string) bool { |
| return strings.HasPrefix(path, "targets/") |
| } |
| copyToRepo := func(fpath string, info os.FileInfo, err error) error { |
| if err != nil { |
| return err |
| } |
| if info.IsDir() || !info.Mode().IsRegular() { |
| return nil |
| } |
| rel, err := filepath.Rel(f.stagedDir(), fpath) |
| if err != nil { |
| return err |
| } |
| relpath := filepath.ToSlash(rel) |
| |
| var paths []string |
| if isTarget(relpath) { |
| paths = computeTargetPaths(consistentSnapshot, relpath, hashes) |
| } else { |
| paths = computeMetadataPaths(consistentSnapshot, relpath, versions) |
| } |
| var files []io.Writer |
| for _, path := range paths { |
| file, err := f.createRepoFile(path) |
| if err != nil { |
| return err |
| } |
| defer file.Close() |
| files = append(files, file) |
| } |
| staged, err := os.Open(fpath) |
| if err != nil { |
| return err |
| } |
| defer staged.Close() |
| if _, err = io.Copy(io.MultiWriter(files...), staged); err != nil { |
| return err |
| } |
| return nil |
| } |
| // Checks if target file should be deleted |
| needsRemoval := func(fpath string) bool { |
| if consistentSnapshot { |
| // strip out the hash |
| name := strings.SplitN(filepath.Base(fpath), ".", 2) |
| if len(name) != 2 || name[1] == "" { |
| return false |
| } |
| fpath = filepath.Join(filepath.Dir(fpath), name[1]) |
| } |
| _, ok := hashes[filepath.ToSlash(fpath)] |
| return !ok |
| } |
| // Checks if folder is empty |
| folderNeedsRemoval := func(fpath string) bool { |
| f, err := os.Open(fpath) |
| if err != nil { |
| return false |
| } |
| defer f.Close() |
| _, err = f.Readdirnames(1) |
| return err == io.EOF |
| } |
| removeFile := func(fpath string, info os.FileInfo, err error) error { |
| if err != nil { |
| return err |
| } |
| rel, err := filepath.Rel(f.repoDir(), fpath) |
| if err != nil { |
| return err |
| } |
| relpath := filepath.ToSlash(rel) |
| if !info.IsDir() && isTarget(relpath) && needsRemoval(rel) { |
| // Delete the target file |
| if err := os.Remove(fpath); err != nil { |
| return err |
| } |
| // Delete the target folder too if it's empty |
| targetFolder := filepath.Dir(fpath) |
| if folderNeedsRemoval(targetFolder) { |
| if err := os.Remove(targetFolder); err != nil { |
| return err |
| } |
| } |
| } |
| return nil |
| } |
| if err := filepath.Walk(f.stagedDir(), copyToRepo); err != nil { |
| return err |
| } |
| if err := filepath.Walk(f.repoDir(), removeFile); err != nil { |
| return err |
| } |
| return f.Clean() |
| } |
| |
| func (f *fileSystemStore) GetSigners(role string) ([]keys.Signer, error) { |
| keyIDs, ok := f.keyIDsForRole[role] |
| if ok { |
| return f.SignersForKeyIDs(keyIDs), nil |
| } |
| |
| privKeys, _, err := f.loadPrivateKeys(role) |
| if err != nil { |
| if os.IsNotExist(err) { |
| return nil, nil |
| } |
| return nil, err |
| } |
| |
| signers := []keys.Signer{} |
| for _, key := range privKeys { |
| signer, err := keys.GetSigner(key) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Cache the signers. |
| for _, keyID := range signer.PublicData().IDs() { |
| f.keyIDsForRole[role] = append(f.keyIDsForRole[role], keyID) |
| f.signerForKeyID[keyID] = signer |
| } |
| signers = append(signers, signer) |
| } |
| |
| return signers, nil |
| } |
| |
| func (f *fileSystemStore) SignersForKeyIDs(keyIDs []string) []keys.Signer { |
| signers := []keys.Signer{} |
| keyIDsSeen := map[string]struct{}{} |
| |
| for _, keyID := range keyIDs { |
| signer, ok := f.signerForKeyID[keyID] |
| if !ok { |
| continue |
| } |
| |
| addSigner := false |
| |
| for _, skid := range signer.PublicData().IDs() { |
| if _, seen := keyIDsSeen[skid]; !seen { |
| addSigner = true |
| } |
| |
| keyIDsSeen[skid] = struct{}{} |
| } |
| |
| if addSigner { |
| signers = append(signers, signer) |
| } |
| } |
| |
| return signers |
| } |
| |
| // ChangePassphrase changes the passphrase for a role keys file. Implements |
| // PassphraseChanger interface. |
| func (f *fileSystemStore) ChangePassphrase(role string) error { |
| // No need to proceed if passphrase func is not set |
| if f.passphraseFunc == nil { |
| return ErrPassphraseRequired{role} |
| } |
| // Read the existing keys (if any) |
| // If encrypted, will prompt for existing passphrase |
| keys, _, err := f.loadPrivateKeys(role) |
| if err != nil { |
| if os.IsNotExist(err) { |
| f.logger.Printf("Failed to change passphrase. Missing keys file for %s role. \n", role) |
| } |
| return err |
| } |
| // Prompt for new passphrase |
| pass, err := f.passphraseFunc(role, true, true) |
| if err != nil { |
| return err |
| } |
| // Proceed saving the keys |
| pk := &persistedKeys{Encrypted: true} |
| pk.Data, err = encrypted.Marshal(keys, pass) |
| if err != nil { |
| return err |
| } |
| data, err := json.MarshalIndent(pk, "", "\t") |
| if err != nil { |
| return err |
| } |
| if err := util.AtomicallyWriteFile(f.keysPath(role), append(data, '\n'), 0600); err != nil { |
| return err |
| } |
| f.logger.Printf("Successfully changed passphrase for %s keys file\n", role) |
| return nil |
| } |
| |
| func (f *fileSystemStore) SaveSigner(role string, signer keys.Signer) error { |
| if err := f.createDirs(); err != nil { |
| return err |
| } |
| |
| // add the key to the existing keys (if any) |
| privKeys, pass, err := f.loadPrivateKeys(role) |
| if err != nil && !os.IsNotExist(err) { |
| return err |
| } |
| key, err := signer.MarshalPrivateKey() |
| if err != nil { |
| return err |
| } |
| privKeys = append(privKeys, key) |
| |
| // if loadPrivateKeys didn't return a passphrase (because no keys yet exist) |
| // and passphraseFunc is set, get the passphrase so the keys file can |
| // be encrypted later (passphraseFunc being nil indicates the keys file |
| // should not be encrypted) |
| if pass == nil && f.passphraseFunc != nil { |
| pass, err = f.passphraseFunc(role, true, false) |
| if err != nil { |
| return err |
| } |
| } |
| |
| pk := &persistedKeys{} |
| if pass != nil { |
| pk.Data, err = encrypted.Marshal(privKeys, pass) |
| if err != nil { |
| return err |
| } |
| pk.Encrypted = true |
| } else { |
| pk.Data, err = json.MarshalIndent(privKeys, "", "\t") |
| if err != nil { |
| return err |
| } |
| } |
| data, err := json.MarshalIndent(pk, "", "\t") |
| if err != nil { |
| return err |
| } |
| if err := util.AtomicallyWriteFile(f.keysPath(role), append(data, '\n'), 0600); err != nil { |
| return err |
| } |
| |
| // Merge privKeys into f.keyIDsForRole and register signers with |
| // f.signerForKeyID. |
| keyIDsForRole := f.keyIDsForRole[role] |
| for _, key := range privKeys { |
| signer, err := keys.GetSigner(key) |
| if err != nil { |
| return err |
| } |
| |
| keyIDs := signer.PublicData().IDs() |
| |
| for _, keyID := range keyIDs { |
| f.signerForKeyID[keyID] = signer |
| } |
| |
| keyIDsForRole = append(keyIDsForRole, keyIDs...) |
| } |
| |
| f.keyIDsForRole[role] = sets.DeduplicateStrings(keyIDsForRole) |
| |
| return nil |
| } |
| |
| // loadPrivateKeys loads keys for the given role and returns them along with the |
| // passphrase (if read) so that callers don't need to re-read it. |
| func (f *fileSystemStore) loadPrivateKeys(role string) ([]*data.PrivateKey, []byte, error) { |
| file, err := os.Open(f.keysPath(role)) |
| if err != nil { |
| return nil, nil, err |
| } |
| defer file.Close() |
| |
| pk := &persistedKeys{} |
| if err := json.NewDecoder(file).Decode(pk); err != nil { |
| return nil, nil, err |
| } |
| |
| var keys []*data.PrivateKey |
| if !pk.Encrypted { |
| if err := json.Unmarshal(pk.Data, &keys); err != nil { |
| return nil, nil, err |
| } |
| return keys, nil, nil |
| } |
| |
| // the keys are encrypted so cannot be loaded if passphraseFunc is not set |
| if f.passphraseFunc == nil { |
| return nil, nil, ErrPassphraseRequired{role} |
| } |
| |
| // try the empty string as the password first |
| pass := []byte("") |
| if err := encrypted.Unmarshal(pk.Data, &keys, pass); err != nil { |
| pass, err = f.passphraseFunc(role, false, false) |
| if err != nil { |
| return nil, nil, err |
| } |
| if err = encrypted.Unmarshal(pk.Data, &keys, pass); err != nil { |
| return nil, nil, err |
| } |
| } |
| return keys, pass, nil |
| } |
| |
| func (f *fileSystemStore) keysPath(role string) string { |
| return filepath.Join(f.dir, "keys", role+".json") |
| } |
| |
| func (f *fileSystemStore) Clean() error { |
| _, err := os.Stat(filepath.Join(f.repoDir(), "root.json")) |
| if os.IsNotExist(err) { |
| return ErrNewRepository |
| } else if err != nil { |
| return err |
| } |
| if err := os.RemoveAll(f.stagedDir()); err != nil { |
| return err |
| } |
| return os.MkdirAll(filepath.Join(f.stagedDir(), "targets"), 0755) |
| } |
| |
| func computeTargetPaths(consistentSnapshot bool, name string, hashes map[string]data.Hashes) []string { |
| if consistentSnapshot { |
| return util.HashedPaths(name, hashes[name]) |
| } else { |
| return []string{name} |
| } |
| } |
| |
| func computeMetadataPaths(consistentSnapshot bool, name string, versions map[string]int64) []string { |
| copyVersion := false |
| |
| switch name { |
| case "root.json": |
| copyVersion = true |
| case "timestamp.json": |
| copyVersion = false |
| default: |
| if consistentSnapshot { |
| copyVersion = true |
| } else { |
| copyVersion = false |
| } |
| } |
| |
| paths := []string{name} |
| if copyVersion { |
| paths = append(paths, util.VersionedPath(name, versions[name])) |
| } |
| |
| return paths |
| } |