blob: 126d510bc63673f0651451e585730c9006696fdc [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package repo
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"syscall"
"time"
"go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/build"
"go.fuchsia.dev/fuchsia/src/sys/pkg/lib/merkle"
tuf "github.com/theupdateframework/go-tuf"
tufData "github.com/theupdateframework/go-tuf/data"
)
var roles = []string{"timestamp", "targets", "snapshot"}
type ErrFileAddFailed string
func (e ErrFileAddFailed) Error() string {
return fmt.Sprintf("file couldn't be added: %s", string(e))
}
func NewAddErr(m string, e error) ErrFileAddFailed {
return ErrFileAddFailed(fmt.Sprintf("%s: %s", m, e))
}
type customTargetMetadata struct {
Merkle string `json:"merkle"`
Size int64 `json:"size"`
}
// TimeProvider provides the service to get Unix timestamp.
type TimeProvider interface {
// UnixTimestamp returns the Unix timestamp.
UnixTimestamp() int
}
type Repo struct {
*tuf.Repo
path string
blobsDir string
encryptionKey []byte
timeProvider TimeProvider
}
var NotCreatingNonExistentRepoError = errors.New("repo does not exist and createIfNotExist is false, so not creating one")
// SystemProvider uses the time pkg to get Unix timestamp.
type SystemTimeProvider struct{}
func (*SystemTimeProvider) UnixTimestamp() int {
return int(time.Now().Unix())
}
func passphrase(role string, confirm bool) ([]byte, error) { return []byte{}, nil }
// New initializes a new Repo structure that may read/write repository data at
// the given path.
func New(path, blobsDir string) (*Repo, error) {
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(path, 0755)
}
if err != nil {
return nil, fmt.Errorf("repository directory %q: %w", path, err)
}
}
if info != nil && !info.IsDir() {
return nil, fmt.Errorf("repository path %q: %w", path, syscall.ENOTDIR)
}
repo, err := tuf.NewRepo(tuf.FileSystemStore(path, passphrase), "sha512")
if err != nil {
return nil, err
}
r := &Repo{repo, path, blobsDir, nil, &SystemTimeProvider{}}
if err := os.MkdirAll(blobsDir, os.ModePerm); err != nil {
return nil, err
}
if err := os.MkdirAll(r.stagedFilesPath(), os.ModePerm); err != nil {
return nil, err
}
return r, nil
}
func (r *Repo) EncryptWith(path string) error {
keyBytes, err := ioutil.ReadFile(path)
if err != nil {
return err
}
r.encryptionKey = keyBytes
return nil
}
// Init initializes a repository, preparing it for publishing. If a
// repository already exists, either os.ErrExist, or a TUF error are returned.
// If a repository does not exist at the given location, a repo will be created there.
func (r *Repo) Init() error {
return r.OptionallyInitAtLocation(true)
}
// OptionallyInitAtLocation initializes a new repository, preparing it
// for publishing, if a repository does not already exist at its
// location and createIfNotExists is true.
// If a repository already exists, either os.ErrExist, or a TUF error are returned.
func (r *Repo) OptionallyInitAtLocation(createIfNotExists bool) error {
if _, err := os.Stat(filepath.Join(r.path, "repository", "root.json")); err == nil {
return os.ErrExist
}
// The repo doesn't exist at this location, but the caller may
// want to treat this as an error. The common case here is
// that a command line user makes a typo and doesn't
// understand why a new repo was silently created.
if !createIfNotExists {
return NotCreatingNonExistentRepoError
}
// Fuchsia repositories always use consistent snapshots.
if err := r.Repo.Init(true); err != nil {
return err
}
rk, err := r.Repo.RootKeys()
if err != nil || len(rk) == 0 {
err = r.GenKeys()
}
return err
}
// GenKeys will generate a full suite of the necessary keys for signing a
// repository.
func (r *Repo) GenKeys() error {
if _, err := r.GenKey("root"); err != nil {
return err
}
for _, role := range roles {
if _, err := r.GenKey(role); err != nil {
return err
}
}
return nil
}
// AddPackage adds a package with the given name with the content from the given
// reader. The package blob is also added. If merkle is non-empty, it is used,
// otherwise the package merkleroot is computed on the fly.
func (r *Repo) AddPackage(name string, rd io.Reader, merkle string) error {
root, size, err := r.AddBlob(merkle, rd)
if err != nil {
return NewAddErr("adding package blob", err)
}
stagingPath := filepath.Join(r.stagedFilesPath(), name)
os.MkdirAll(filepath.Dir(stagingPath), os.ModePerm)
// add merkle root as custom JSON
metadata := customTargetMetadata{Merkle: root, Size: size}
jsonStr, err := json.Marshal(metadata)
if err != nil {
return NewAddErr(fmt.Sprintf("serializing %v", metadata), err)
}
blobPath := filepath.Join(r.blobsDir, root)
if err := linkOrCopy(blobPath, stagingPath); err != nil {
return NewAddErr("creating file in staging directory", err)
}
// add file with custom JSON to repository
if err := r.AddTarget(name, json.RawMessage(jsonStr)); err != nil {
return fmt.Errorf("failed adding target %s to TUF repo: %s", name, err)
}
return nil
}
func cryptingWriter(dst io.Writer, key []byte) (io.WriteCloser, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
iv := make([]byte, aes.BlockSize)
// Write the iv to the front of the output stream
if _, err := io.ReadFull(io.TeeReader(rand.Reader, dst), iv); err != nil {
return nil, err
}
stream := cipher.NewCTR(block, iv)
return cipher.StreamWriter{stream, dst, nil}, nil
}
// HasBlob returns true if the given merkleroot is already in the repository
// blob store.
func (r *Repo) HasBlob(root string) bool {
blobPath := filepath.Join(r.blobsDir, root)
fi, err := os.Stat(blobPath)
return err == nil && fi.Mode().IsRegular()
}
// AddBlob writes the content of the given reader to the blob identified by the
// given merkleroot. If merkleroot is empty string, a merkleroot is computed.
// Addblob always returns the plaintext size of the blob that is added, even if
// blob encryption is used.
func (r *Repo) AddBlob(root string, rd io.Reader) (string, int64, error) {
if root != "" {
dstPath := filepath.Join(r.blobsDir, root)
if fi, err := os.Stat(dstPath); err == nil {
fileSize := fi.Size()
if r.encryptionKey != nil {
fileSize -= aes.BlockSize
}
return root, fileSize, nil
}
var dst io.WriteCloser
var err error
dst, err = os.OpenFile(dstPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0666)
if err != nil {
return root, 0, err
}
defer dst.Close()
if r.encryptionKey != nil {
dst, err = cryptingWriter(dst, r.encryptionKey)
if err != nil {
return root, 0, err
}
}
n, err := io.Copy(dst, rd)
return root, n, err
}
var tree merkle.Tree
f, err := ioutil.TempFile(r.blobsDir, "blob")
if err != nil {
return "", 0, err
}
var dst io.WriteCloser = f
if r.encryptionKey != nil {
dst, err = cryptingWriter(dst, r.encryptionKey)
if err != nil {
return "", 0, err
}
}
n, err := tree.ReadFrom(io.TeeReader(rd, dst))
if err != nil {
f.Close()
return "", n, err
}
f.Close()
root = hex.EncodeToString(tree.Root())
return root, n, os.Rename(f.Name(), filepath.Join(r.blobsDir, root))
}
// CommitUpdates finalizes the changes to the update repository that have been
// staged by calling AddPackageFile. Setting dateVersioning to true will set
// the version of the targets, snapshot, and timestamp metadata files based on
// an offset in seconds from epoch (1970-01-01 00:00:00 UTC).
func (r *Repo) CommitUpdates(dateVersioning bool) error {
if dateVersioning {
dTime := r.timeProvider.UnixTimestamp()
tVer, err := r.TargetsVersion()
if err != nil {
return err
}
if dTime > tVer {
r.SetTargetsVersion(dTime)
}
sVer, err := r.SnapshotVersion()
if err != nil {
return err
}
if dTime > sVer {
r.SetSnapshotVersion(dTime)
}
tsVer, err := r.TimestampVersion()
if err != nil {
return err
}
if dTime > tsVer {
r.SetTimestampVersion(dTime)
}
}
return r.commitUpdates()
}
// hasTarget returns true if the given targetFiles contains a target matching
// exactly all of name, version and merkle, and false otherwise.
func (r *Repo) hasTarget(name, version, merkle string, targets tufData.TargetFiles) (bool, error) {
path := filepath.Join(name, version)
if targets[path].Custom == nil {
return false, nil
}
var custom customTargetMetadata
if err := json.Unmarshal(*targets[path].Custom, &custom); err != nil {
return false, err
}
return custom.Merkle == merkle, nil
}
// PublishManifests publishes the packages and blobs identified in the package
// output manifests at the given paths, returning all input files involved, or an
// error.
func (r *Repo) PublishManifests(paths []string) ([]string, error) {
targets, err := r.Targets()
if err != nil {
return nil, err
}
var deps []string
for _, path := range paths {
pkgDeps, err := r.publishManifest(path, targets)
if err != nil {
return nil, err
}
deps = append(deps, pkgDeps...)
}
return deps, nil
}
// PublishManifest publishes the package and blobs identified in the package
// output manifest at the given path, returning all input files involved, or an
// error.
func (r *Repo) PublishManifest(path string) ([]string, error) {
targets, err := r.Targets()
if err != nil {
return nil, err
}
return r.publishManifest(path, targets)
}
// publishManifest publishes the package and blobs identified in the package
// output manifest at the given path, using a pre-loaded targets, and returning
// all input files involved, or an error.
func (r *Repo) publishManifest(path string, targets tufData.TargetFiles) ([]string, error) {
deps := []string{path}
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var packageManifest build.PackageManifest
if err := json.NewDecoder(f).Decode(&packageManifest); err != nil {
return nil, err
}
if packageManifest.Version != "1" {
return nil, fmt.Errorf("unknown version %q, can't publish", packageManifest.Version)
}
// first collect all the deps, and extract the package merkle root
var pkgMerkle string
for _, blob := range packageManifest.Blobs {
deps = append(deps, blob.SourcePath)
if blob.Path != "meta/" {
continue
}
pkgMerkle = blob.Merkle.String()
}
targetExists, err := r.hasTarget(packageManifest.Package.Name, packageManifest.Package.Version, pkgMerkle, targets)
if err != nil {
return nil, err
}
if targetExists {
return deps, nil
}
// publish the package if it's not already in targets.json
for _, blob := range packageManifest.Blobs {
if blob.Path == "meta/" {
p := packageManifest.Package
if err := p.Validate(); err != nil {
return nil, fmt.Errorf("Validate() failed: %v", err)
}
name := p.Name + "/" + p.Version
f, err := os.Open(blob.SourcePath)
if err != nil {
return nil, err
}
err = r.AddPackage(name, f, blob.Merkle.String())
f.Close()
} else {
if !r.HasBlob(blob.Merkle.String()) {
f, err := os.Open(blob.SourcePath)
if err != nil {
return nil, err
}
_, _, err = r.AddBlob(blob.Merkle.String(), f)
f.Close()
}
}
if err != nil {
return nil, err
}
}
return deps, nil
}
func (r *Repo) commitUpdates() error {
// TUF-1.0 section 4.4.2 states that the expiration must be in the
// ISO-8601 format in the UTC timezone with no nanoseconds.
expires := time.Now().AddDate(0, 0, 30).UTC().Round(time.Second)
if err := r.SnapshotWithExpires(expires); err != nil {
return fmt.Errorf("snapshot: %s", err)
}
if err := r.TimestampWithExpires(expires); err != nil {
return fmt.Errorf("timestamp: %s", err)
}
if err := r.Commit(); err != nil {
return fmt.Errorf("commit: %s", err)
}
return r.fixupRootConsistentSnapshot()
}
func (r *Repo) stagedFilesPath() string {
return filepath.Join(r.path, "staged", "targets")
}
// when the repository is "pre-initialized" by a root.json from the build, but
// no root keys are available to the publishing step, the commit process does
// not produce a consistent snapshot file for the root json manifest. This
// method implements that production.
func (r *Repo) fixupRootConsistentSnapshot() error {
b, err := ioutil.ReadFile(filepath.Join(r.path, "repository", "root.json"))
if err != nil {
return err
}
sum512 := sha512.Sum512(b)
rootSnap := filepath.Join(r.path, "repository", fmt.Sprintf("%x.root.json", sum512))
if _, err := os.Stat(rootSnap); os.IsNotExist(err) {
if err := ioutil.WriteFile(rootSnap, b, 0666); err != nil {
return err
}
}
return nil
}
// link is available for stubbing in tests
var link = os.Link
func linkOrCopy(srcPath, dstPath string) error {
if err := link(srcPath, dstPath); err != nil {
s, err := os.Open(srcPath)
if err != nil {
return err
}
defer s.Close()
d, err := ioutil.TempFile(filepath.Dir(dstPath), filepath.Base(dstPath))
if err != nil {
return err
}
if _, err := io.Copy(d, s); err != nil {
d.Close()
os.Remove(d.Name())
return err
}
if err := d.Close(); err != nil {
return err
}
return os.Rename(d.Name(), dstPath)
}
return nil
}