blob: 700fa1568162b1f9d06c866291c59e213624e32c [file] [log] [blame]
package data
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"strings"
"sync"
"time"
cjson "github.com/tent/canonical-json-go"
)
const (
KeyIDLength = sha256.Size * 2
KeyTypeEd25519 = "ed25519"
KeyTypeECDSA_SHA2_P256 = "ecdsa-sha2-nistp256"
KeySchemeEd25519 = "ed25519"
KeySchemeECDSA_SHA2_P256 = "ecdsa-sha2-nistp256"
KeyTypeRSASSA_PSS_SHA256 = "rsa"
KeySchemeRSASSA_PSS_SHA256 = "rsassa-pss-sha256"
)
var (
HashAlgorithms = []string{"sha256", "sha512"}
ErrPathsAndPathHashesSet = errors.New("tuf: failed validation of delegated target: paths and path_hash_prefixes are both set")
)
type Signed struct {
Signed json.RawMessage `json:"signed"`
Signatures []Signature `json:"signatures"`
}
type Signature struct {
KeyID string `json:"keyid"`
Signature HexBytes `json:"sig"`
}
type PublicKey struct {
Type string `json:"keytype"`
Scheme string `json:"scheme"`
Algorithms []string `json:"keyid_hash_algorithms,omitempty"`
Value json.RawMessage `json:"keyval"`
ids []string
idOnce sync.Once
}
type PrivateKey struct {
Type string `json:"keytype"`
Scheme string `json:"scheme,omitempty"`
Algorithms []string `json:"keyid_hash_algorithms,omitempty"`
Value json.RawMessage `json:"keyval"`
}
func (k *PublicKey) IDs() []string {
k.idOnce.Do(func() {
data, err := cjson.Marshal(k)
if err != nil {
panic(fmt.Errorf("tuf: error creating key ID: %w", err))
}
digest := sha256.Sum256(data)
k.ids = []string{hex.EncodeToString(digest[:])}
})
return k.ids
}
func (k *PublicKey) ContainsID(id string) bool {
for _, keyid := range k.IDs() {
if id == keyid {
return true
}
}
return false
}
func DefaultExpires(role string) time.Time {
var t time.Time
switch role {
case "root":
t = time.Now().AddDate(1, 0, 0)
case "targets":
t = time.Now().AddDate(0, 3, 0)
case "snapshot":
t = time.Now().AddDate(0, 0, 7)
case "timestamp":
t = time.Now().AddDate(0, 0, 1)
}
return t.UTC().Round(time.Second)
}
type Root struct {
Type string `json:"_type"`
SpecVersion string `json:"spec_version"`
Version int `json:"version"`
Expires time.Time `json:"expires"`
Keys map[string]*PublicKey `json:"keys"`
Roles map[string]*Role `json:"roles"`
Custom *json.RawMessage `json:"custom,omitempty"`
ConsistentSnapshot bool `json:"consistent_snapshot"`
}
func NewRoot() *Root {
return &Root{
Type: "root",
SpecVersion: "1.0",
Expires: DefaultExpires("root"),
Keys: make(map[string]*PublicKey),
Roles: make(map[string]*Role),
ConsistentSnapshot: true,
}
}
func (r *Root) AddKey(key *PublicKey) bool {
changed := false
for _, id := range key.IDs() {
if _, ok := r.Keys[id]; !ok {
changed = true
r.Keys[id] = key
}
}
return changed
}
type Role struct {
KeyIDs []string `json:"keyids"`
Threshold int `json:"threshold"`
}
func (r *Role) AddKeyIDs(ids []string) bool {
roleIDs := make(map[string]struct{})
for _, id := range r.KeyIDs {
roleIDs[id] = struct{}{}
}
changed := false
for _, id := range ids {
if _, ok := roleIDs[id]; !ok {
changed = true
r.KeyIDs = append(r.KeyIDs, id)
}
}
return changed
}
type Files map[string]FileMeta
type FileMeta struct {
Length int64 `json:"length",omitempty`
Hashes Hashes `json:"hashes",omitempty`
Custom *json.RawMessage `json:"custom,omitempty"`
}
type Hashes map[string]HexBytes
func (f FileMeta) HashAlgorithms() []string {
funcs := make([]string, 0, len(f.Hashes))
for name := range f.Hashes {
funcs = append(funcs, name)
}
return funcs
}
type SnapshotFileMeta struct {
FileMeta
Version int `json:"version"`
}
type SnapshotFiles map[string]SnapshotFileMeta
type Snapshot struct {
Type string `json:"_type"`
SpecVersion string `json:"spec_version"`
Version int `json:"version"`
Expires time.Time `json:"expires"`
Meta SnapshotFiles `json:"meta"`
Custom *json.RawMessage `json:"custom,omitempty"`
}
func NewSnapshot() *Snapshot {
return &Snapshot{
Type: "snapshot",
SpecVersion: "1.0",
Expires: DefaultExpires("snapshot"),
Meta: make(SnapshotFiles),
}
}
type TargetFiles map[string]TargetFileMeta
type TargetFileMeta struct {
FileMeta
}
func (f TargetFileMeta) HashAlgorithms() []string {
return f.FileMeta.HashAlgorithms()
}
type Targets struct {
Type string `json:"_type"`
SpecVersion string `json:"spec_version"`
Version int `json:"version"`
Expires time.Time `json:"expires"`
Targets TargetFiles `json:"targets"`
Delegations *Delegations `json:"delegations,omitempty"`
Custom *json.RawMessage `json:"custom,omitempty"`
}
// Delegations represents the edges from a parent Targets role to one or more
// delegated target roles. See spec v1.0.19 section 4.5.
type Delegations struct {
Keys map[string]*PublicKey `json:"keys"`
Roles []DelegatedRole `json:"roles"`
}
// DelegatedRole describes a delegated role, including what paths it is
// reponsible for. See spec v1.0.19 section 4.5.
type DelegatedRole struct {
Name string `json:"name"`
KeyIDs []string `json:"keyids"`
Threshold int `json:"threshold"`
Terminating bool `json:"terminating"`
PathHashPrefixes []string `json:"path_hash_prefixes,omitempty"`
Paths []string `json:"paths"`
}
// MatchesPath evaluates whether the path patterns or path hash prefixes match
// a given file. This determines whether a delegated role is responsible for
// signing and verifying the file.
func (d *DelegatedRole) MatchesPath(file string) (bool, error) {
if err := d.validatePaths(); err != nil {
return false, err
}
for _, pattern := range d.Paths {
if matched, _ := filepath.Match(pattern, file); matched {
return true, nil
}
}
pathHash := PathHexDigest(file)
for _, hashPrefix := range d.PathHashPrefixes {
if strings.HasPrefix(pathHash, hashPrefix) {
return true, nil
}
}
return false, nil
}
// validatePaths enforces the spec
// https://theupdateframework.github.io/specification/v1.0.19/index.html#file-formats-targets
// 'role MUST specify only one of the "path_hash_prefixes" or "paths"'
// Marshalling and unmarshalling JSON will fail and return
// ErrPathsAndPathHashesSet if both fields are set and not empty.
func (d *DelegatedRole) validatePaths() error {
if len(d.PathHashPrefixes) > 0 && len(d.Paths) > 0 {
return ErrPathsAndPathHashesSet
}
return nil
}
// MarshalJSON is called when writing the struct to JSON. We validate prior to
// marshalling to ensure that an invalid delegated role can not be serialized
// to JSON.
func (d *DelegatedRole) MarshalJSON() ([]byte, error) {
type delegatedRoleAlias DelegatedRole
if err := d.validatePaths(); err != nil {
return nil, err
}
return json.Marshal((*delegatedRoleAlias)(d))
}
// UnmarshalJSON is called when reading the struct from JSON. We validate once
// unmarshalled to ensure that an error is thrown if an invalid delegated role
// is read.
func (d *DelegatedRole) UnmarshalJSON(b []byte) error {
type delegatedRoleAlias DelegatedRole
if err := json.Unmarshal(b, (*delegatedRoleAlias)(d)); err != nil {
return err
}
return d.validatePaths()
}
func NewTargets() *Targets {
return &Targets{
Type: "targets",
SpecVersion: "1.0",
Expires: DefaultExpires("targets"),
Targets: make(TargetFiles),
}
}
type TimestampFileMeta struct {
FileMeta
Version int `json:"version"`
}
type TimestampFiles map[string]TimestampFileMeta
type Timestamp struct {
Type string `json:"_type"`
SpecVersion string `json:"spec_version"`
Version int `json:"version"`
Expires time.Time `json:"expires"`
Meta TimestampFiles `json:"meta"`
Custom *json.RawMessage `json:"custom,omitempty"`
}
func NewTimestamp() *Timestamp {
return &Timestamp{
Type: "timestamp",
SpecVersion: "1.0",
Expires: DefaultExpires("timestamp"),
Meta: make(TimestampFiles),
}
}