bundle

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 13, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package bundle defines the DevProof bundle format: its version identifiers, media types, and the manifest, lock, and inventory documents that describe what a bundle contains.

The constants here are a compatibility surface. Every one of them participates in the OCI subject digest, so changing a value is a new format version, never an edit (DP-015).

Index

Constants

View Source
const (
	// MediaTypeArtifactV1 is the OCI manifest's artifactType.
	MediaTypeArtifactV1 = "application/vnd.thingz.devproof.bundle.v1"
	// MediaTypeConfigV1 is the DevProof config blob's media type.
	MediaTypeConfigV1 = "application/vnd.thingz.devproof.config.v1+json"
	// MediaTypeLayerV1 is the single filesystem layer's media type.
	MediaTypeLayerV1 = "application/vnd.oci.image.layer.v1.tar+gzip"
)

Media types for format v1.

MediaTypeLayerV1 is the standard OCI gzip layer type, not a DevProof type, so that a generic OCI consumer can materialize the payload without understanding provenance (DP-006).

View Source
const (
	KindBundle             = "Bundle"
	KindBundleLock         = "BundleLock"
	KindVerificationPolicy = "VerificationPolicy"
)

Document kinds within APIVersionV1Alpha1.

View Source
const (
	// ModeFile is the canonical mode for a regular file with no execute bit.
	ModeFile = 0o644
	// ModeExecutable is the canonical mode for a regular file with any
	// execute bit set.
	ModeExecutable = 0o755
	// ModeDirectory is the canonical mode for every derived parent directory.
	ModeDirectory = 0o755
)

Canonical file modes. The portable profile normalizes every regular file to one of two values and every directory to one, so that a bundle built on a permissive umask and one built on a restrictive umask are byte-identical (DP-005).

View Source
const (
	SourceTypePath = "path"
	SourceTypeGit  = "git"
)

Built-in source types.

View Source
const APIVersionV1Alpha1 = "devproof.thingz.io/v1alpha1"

APIVersionV1Alpha1 is the manifest and policy API group and version.

It is independent of the bundle format version: a later manifest API may still build format v1 if its canonical semantics are identical.

View Source
const ConfigSchemaVersion = 1

ConfigSchemaVersion is the schema version of the v1 config blob.

View Source
const DefaultSpecName = "devproof.yaml"

DefaultSpecName is the conventional file name for a bundle manifest.

View Source
const DigestSize = sha256.Size

DigestSize is the length of a SHA-256 digest in bytes. Format v1 accepts no other algorithm anywhere the format requires one, so there is no algorithm negotiation to get wrong.

View Source
const MaxRepresentableFileBytes = int64(1)<<33 - 1

MaxRepresentableFileBytes is the largest file format v1 can encode: eleven octal digits in the USTAR size field, one byte short of 8 GiB.

This is a property of the format, not a policy choice, so it is the ceiling for maxFileBytes rather than a separate check. Format v1 describes an oversized file with no PAX size record; see DP-019.

View Source
const PredicateTypeProvenanceV1 = "https://devproof.thingz.io/provenance/v1"

PredicateTypeProvenanceV1 is the in-toto predicate type for DevProof provenance. It embeds a SLSA Provenance v1 document and adds the DevProof facts SLSA has no field for: lock digest and per-source tree digests (DP-024).

View Source
const TreeDigestDomainV1 = "devproof-tree-v1\x00"

TreeDigestDomainV1 is the domain-separation prefix hashed before any file record in the v1 tree digest.

Domain separation is what keeps a tree digest from ever colliding with a digest computed over the same bytes for a different purpose, and what makes a future format's records unmistakable for v1's.

Variables

This section is empty.

Functions

func DecodeConfig

func DecodeConfig(config map[string]any, target any) error

DecodeConfig strictly decodes a resolver's configuration into target.

The round trip through JSON is deliberate. It gives resolvers ordinary struct tags and, more importantly, DisallowUnknownFields: a config key a resolver does not recognize is a mistake the author should hear about, not something to drop on the floor.

func NormalizeMountPath

func NormalizeMountPath(mount string) (string, error)

NormalizeMountPath canonicalizes a mount path. Empty and "." both mean the bundle root.

func NormalizePatterns

func NormalizePatterns(patterns []string) []string

NormalizePatterns returns a sorted, de-duplicated copy.

Patterns are a set, so the lock records them in a canonical order. Without this, reordering a manifest's include list would change the lock digest while selecting exactly the same files.

func Supported

func Supported(f Format) bool

Supported reports whether this build understands f.

func Template added in v0.2.0

func Template(sourcePath string) ([]byte, error)

Template renders a starter manifest whose local source reads sourcePath.

The result is a commented document rather than a marshaled struct, because its purpose is to answer "what can go here?" without sending anyone to the schema. Comments are the only part of a manifest that can do that, and they do not survive a round trip through the typed model.

Only the source path varies. Generating the rest from an inspection of the directory was considered and rejected: a scan produces a valid manifest that teaches nothing, because it cannot show a git source, a filter, or a mount path that is not already there.

func ValidateName

func ValidateName(name, field string) error

ValidateName checks a logical name: lowercase ASCII letters, digits, and hyphens, starting and ending alphanumeric.

The grammar is restrictive because these names appear in diagnostics, evidence, and error messages, where a name containing a newline or a control character is a way to forge output.

Types

type Bound

type Bound int

Bound identifies one configurable resource limit.

Limits are enumerated rather than addressed by field name so that a verification report can say which input supplied each effective value without stringly-typed keys (DP-021).

const (
	BoundMaxFiles Bound = iota
	BoundMaxFileBytes
	BoundMaxExpandedBytes
	BoundMaxCompressedBytes
	BoundMaxCompressionRatio
	BoundMaxPathBytes
	BoundMaxPathSegmentBytes
	BoundMaxPathDepth
	BoundMaxConfigBytes
	BoundMaxManifestBytes
	BoundMaxSpecBytes
	BoundMaxLockBytes
	BoundMaxReferrers
	BoundMaxEvidenceBytes
	BoundMaxParallelSources
)

The configurable bounds. Values are not serialized; Bound.String is.

func (Bound) String

func (b Bound) String() string

type Config

type Config struct {
	SchemaVersion int          `json:"schemaVersion"`
	Format        Format       `json:"format"`
	TreeDigest    string       `json:"treeDigest"`
	FileCount     int64        `json:"fileCount"`
	TotalSize     int64        `json:"totalSize"`
	Files         []ConfigFile `json:"files"`
}

Config is the DevProof config blob: the exact inventory of what a bundle contains.

It exists so that verification never has to trust tar extraction behavior. A consumer can check every entry against this list before writing anything, which is what makes "the archive contains exactly this and nothing else" a checkable claim rather than an assumption about the extractor.

The struct deliberately has nowhere to put a bundle name, a source, a timestamp, a builder, a registry reference, a tag, an annotation, or a signature. Those belong to evidence, and admitting any of them here would make two builds of identical content produce different subject digests (DP-002).

func ParseConfig

func ParseConfig(data []byte) (*Config, error)

ParseConfig decodes and validates a config blob.

Decoding is strict: an unknown field is an error, not something to ignore. A field this build does not understand may be load-bearing for the producer, and silently dropping it would mean verifying something other than what was published.

func (*Config) Validate

func (c *Config) Validate() error

Validate checks every internal consistency rule the format defines.

The checks are exhaustive rather than representative because this is the document a verifier compares the layer against: a config that is internally inconsistent can be made to agree with more than one archive.

type ConfigFile

type ConfigFile struct {
	Path   string `json:"path"`
	Mode   uint32 `json:"mode"`
	Size   int64  `json:"size"`
	Digest string `json:"digest"`
}

ConfigFile is one inventory entry.

type Digest

type Digest [DigestSize]byte

Digest is a raw SHA-256 digest.

It is a fixed-size array rather than a string for two reasons: the tree record encoding embeds the raw 32 bytes, and a fixed size makes an unset digest impossible to confuse with a valid one of the wrong length.

func DigestOf

func DigestOf(b []byte) Digest

DigestOf returns the SHA-256 of b.

func ParseDigest

func ParseDigest(s string) (Digest, error)

ParseDigest accepts the OCI "sha256:<64 lowercase hex>" form.

The encoding is validated, not merely decoded. Uppercase hex, a missing algorithm, and a wrong length all decode to something under a lax parser, and two spellings of one digest that compare unequal would defeat content addressing entirely.

func (Digest) Hex

func (d Digest) Hex() string

Hex renders the digest as lowercase hex with no algorithm prefix.

func (Digest) IsZero

func (d Digest) IsZero() bool

IsZero reports whether d is the zero value, which is never a digest any content produced.

func (Digest) String

func (d Digest) String() string

String renders the digest in OCI form: "sha256:" followed by lowercase hex.

type Effective added in v0.2.0

type Effective struct {
	Bound  Bound
	Name   string
	Value  int64
	Origin Origin
}

Effective is one bound's resolved value and where it came from.

type FileRecord added in v0.4.0

type FileRecord struct {
	// Path is the canonical bundle-relative path.
	Path Path
	// Mode is the normalized mode: ModeFile or ModeExecutable.
	Mode uint32
	// Size is the file's length in bytes.
	Size int64
	// Digest is the SHA-256 of the file's exact content bytes.
	Digest Digest
}

FileRecord is one entry of the canonical inventory: everything about a file that contributes to identity, and nothing that does not.

Ownership, timestamps, and link targets are absent by construction rather than normalized away later, so there is no code path where one could reach the tree digest.

type Format

type Format string

Format identifies a DevProof canonical bundle format version.

It is carried in the config blob and recognized through the artifact media type. Readers dispatch on it and reject what they do not know, rather than guessing at a newer layout (DP-015).

const FormatV1 Format = "devproof-bundle-v1"

FormatV1 is the first canonical bundle format: a portable filesystem profile of regular files and implicit directories, encoded as one gzip-compressed tar layer.

func FormatForArtifactType

func FormatForArtifactType(artifactType string) (Format, bool)

FormatForArtifactType maps an OCI artifactType onto a bundle format.

The artifact type is what a consumer sees before fetching any blob, so it is the first place an unsupported version can be rejected — before spending a request on a config it cannot parse.

func SupportedFormats

func SupportedFormats() []Format

SupportedFormats returns the formats this build understands, in a stable order suitable for `devproof version` output.

func (Format) ArtifactType

func (f Format) ArtifactType() (string, bool)

ArtifactType returns the OCI artifactType for f.

func (Format) ConfigMediaType

func (f Format) ConfigMediaType() (string, bool)

ConfigMediaType returns the config media type for f.

func (Format) LayerMediaType

func (f Format) LayerMediaType() (string, bool)

LayerMediaType returns the single filesystem layer media type for f.

func (Format) String

func (f Format) String() string

type Input

type Input struct {
	Origin Origin
	Limits Limits
}

Input is one contributor to the effective limits.

type Limits

type Limits struct {
	// MaxFiles bounds the number of files in a bundle.
	MaxFiles int64 `json:"maxFiles,omitempty" yaml:"maxFiles,omitempty"`
	// MaxFileBytes bounds any single file's size.
	MaxFileBytes int64 `json:"maxFileBytes,omitempty" yaml:"maxFileBytes,omitempty"`
	// MaxExpandedBytes bounds the total uncompressed payload.
	MaxExpandedBytes int64 `json:"maxExpandedBytes,omitempty" yaml:"maxExpandedBytes,omitempty"`
	// MaxCompressedBytes bounds the compressed layer.
	MaxCompressedBytes int64 `json:"maxCompressedBytes,omitempty" yaml:"maxCompressedBytes,omitempty"`
	// MaxCompressionRatio bounds expanded bytes divided by compressed bytes.
	MaxCompressionRatio int64 `json:"maxCompressionRatio,omitempty" yaml:"maxCompressionRatio,omitempty"`
	// MaxPathBytes bounds a canonical path's UTF-8 length.
	MaxPathBytes int64 `json:"maxPathBytes,omitempty" yaml:"maxPathBytes,omitempty"`
	// MaxPathSegmentBytes bounds one path segment's UTF-8 length.
	MaxPathSegmentBytes int64 `json:"maxPathSegmentBytes,omitempty" yaml:"maxPathSegmentBytes,omitempty"`
	// MaxPathDepth bounds a canonical path's segment count.
	MaxPathDepth int64 `json:"maxPathDepth,omitempty" yaml:"maxPathDepth,omitempty"`
	// MaxConfigBytes bounds the DevProof config blob.
	MaxConfigBytes int64 `json:"maxConfigBytes,omitempty" yaml:"maxConfigBytes,omitempty"`
	// MaxManifestBytes bounds the OCI manifest.
	MaxManifestBytes int64 `json:"maxManifestBytes,omitempty" yaml:"maxManifestBytes,omitempty"`
	// MaxSpecBytes bounds a manifest document.
	MaxSpecBytes int64 `json:"maxSpecBytes,omitempty" yaml:"maxSpecBytes,omitempty"`
	// MaxLockBytes bounds a lock document.
	MaxLockBytes int64 `json:"maxLockBytes,omitempty" yaml:"maxLockBytes,omitempty"`
	// MaxReferrers bounds how many referrer descriptors are enumerated.
	MaxReferrers int64 `json:"maxReferrers,omitempty" yaml:"maxReferrers,omitempty"`
	// MaxEvidenceBytes bounds one evidence object.
	MaxEvidenceBytes int64 `json:"maxEvidenceBytes,omitempty" yaml:"maxEvidenceBytes,omitempty"`
	// MaxParallelSources bounds concurrent source resolution.
	MaxParallelSources int64 `json:"maxParallelSources,omitempty" yaml:"maxParallelSources,omitempty"`
}

Limits bounds the resources one operation may consume.

A zero field means "unset" and inherits the default. Zero never means unlimited: an unbounded extraction is the decompression-bomb case, and spelling it as the zero value would make it the accidental default. Every field carries explicit json and yaml tags. Without them yaml.v3 derives a key by lowercasing the whole field name, so a policy would have had to spell MaxFiles as "maxfiles" -- a spelling no document used and none should. The tags are the serialized names, and a policy is canonicalized by them, so they are a compatibility surface rather than a formatting choice.

func Ceilings

func Ceilings() Limits

Ceilings returns the documented hard maximums. A caller may not configure past these, because beyond them the format's own portability claims stop holding: a path segment over 255 bytes, for instance, cannot be written on most filesystems, so such a bundle could be built but never expanded.

func DefaultLimits

func DefaultLimits() Limits

DefaultLimits returns the documented defaults (DP-020).

func (Limits) Validate

func (l Limits) Validate() error

Validate rejects negative and above-ceiling values.

It does not fill defaults: a caller that supplied an impossible bound should hear about it, not have it quietly replaced.

func (Limits) WithDefaults

func (l Limits) WithDefaults() Limits

WithDefaults returns a copy with every unset bound filled in.

type Lock

type Lock struct {
	APIVersion string `json:"apiVersion"`
	Kind       string `json:"kind"`

	// ManifestDigest binds this lock to one manifest. A manifest edit that
	// changes meaning invalidates the lock.
	ManifestDigest string `json:"manifestDigest"`
	// Format is the bundle format the lock was produced for.
	Format Format `json:"format"`

	Sources []LockedSource `json:"sources"`
	Files   []LockedFile   `json:"files"`

	// TreeDigest is the canonical payload identity the lock commits to.
	TreeDigest string `json:"treeDigest"`
}

Lock records the immutable resolution of a manifest.

It is generated, never hand-written, and it is what makes a build reproducible: a locked build fails rather than quietly accepting material that has changed since the lock was made (DP-004).

The struct has nowhere to record a timestamp, a hostname, a user, a registry destination, or a credential. That is deliberate. A lock is reviewed in a pull request and committed to a repository, so anything it can hold is something that leaks.

func NewLock

func NewLock(manifestDigest string, format Format, sources []LockedSource, files []LockedFile, treeDigest string) *Lock

NewLock assembles a lock, canonicalizing its set-like fields.

func ParseLock

func ParseLock(data []byte) (*Lock, error)

ParseLock decodes and validates a lock.

func (*Lock) SourceByName

func (l *Lock) SourceByName(name string) (*LockedSource, bool)

SourceByName returns a locked source.

func (*Lock) Validate

func (l *Lock) Validate() error

Validate checks the lock's structure and internal consistency.

type LockedFile

type LockedFile struct {
	Path string `json:"path"`
	// Source names the single owner of this path. Every final path has
	// exactly one (DP-011).
	Source     string `json:"source"`
	SourcePath string `json:"sourcePath"`
	Mode       uint32 `json:"mode"`
	Size       int64  `json:"size"`
	Digest     string `json:"digest"`
}

LockedFile records one file's final placement and identity.

type LockedSource

type LockedSource struct {
	Name string `json:"name"`
	Type string `json:"type"`
	// Resolver identifies the implementation that produced this resolution.
	Resolver string `json:"resolver"`
	// ResolverVersion is that implementation's version.
	//
	// Recorded separately because the point of naming a resolver is that one
	// whose behavior changed cannot silently satisfy a lock an earlier one
	// made -- and the name alone does not change when the behavior does. It
	// was being verified during resolution and then dropped on the way to the
	// lock, which left the comment above describing something nothing stored.
	ResolverVersion string `json:"resolverVersion,omitempty"`

	// Requested is what the manifest asked for, which may be mutable.
	Requested map[string]any `json:"requested,omitempty"`
	// Resolved is what it resolved to, which must not be.
	Resolved map[string]any `json:"resolved,omitempty"`

	// TreeDigest is the canonical digest of this source's filtered
	// contribution, before its mount path is applied. It is the common
	// identity across source types: a Git commit and a local directory that
	// hold the same files have the same value here.
	TreeDigest string `json:"treeDigest"`

	MountPath string   `json:"mountPath,omitempty"`
	Include   []string `json:"include,omitempty"`
	Exclude   []string `json:"exclude,omitempty"`
}

LockedSource records how one source resolved.

type Origin

type Origin string

Origin records which input supplied a limit's effective value.

const (
	OriginDefault Origin = "default"
	OriginClient  Origin = "client"
	OriginRequest Origin = "request"
	OriginPolicy  Origin = "policy"
)

Limit origins, from weakest to strongest precedence. Precedence is not "last wins": every input may only tighten, so the effective value is the minimum across all of them (DP-021).

type Path added in v0.4.0

type Path string

Path is a validated, normalized, relative bundle path.

Its zero value is not a valid path. Canonicalization is the format's business and lives in an internal package; this type is here so that a source resolver written outside this module can name what it returns.

That is not a hypothetical. The resolver contract is public and DP-009 promises it, but the interface was written in terms of types under internal/, which no external module may name -- so the extension point could be described, and called, and never implemented.

func (Path) String added in v0.4.0

func (p Path) String() string

type Pattern

type Pattern struct {
	// contains filtered or unexported fields
}

Pattern selects source-relative paths.

The syntax is deliberately small: `*` for non-separator runs, `?` for one non-separator character, `[a-z]` classes, and `**` for whole segments. There is no negation, no ordering significance, and no .gitignore inheritance. Every one of those would make selection depend on the order rules were written, and DP-011 requires that a manifest's meaning not depend on how it was arranged.

func ParsePattern

func ParsePattern(raw string) (*Pattern, error)

ParsePattern compiles a selection pattern.

func (*Pattern) Match

func (p *Pattern) Match(candidate string) bool

Match reports whether p selects a source-relative path.

func (*Pattern) String

func (p *Pattern) String() string

String returns the pattern as written.

type PatternSet

type PatternSet struct {
	// contains filtered or unexported fields
}

PatternSet is a source's include and exclude rules.

Include and exclude are sets, not ordered lists: a path is selected when it matches any include and no exclude. Because neither list has precedence, reordering a manifest cannot change what it selects.

func NewPatternSet

func NewPatternSet(include, exclude []string) (*PatternSet, error)

NewPatternSet compiles include and exclude rules.

An absent include list means `**`, which selects everything. Spelling the default explicitly means there is no separate "no filter" code path whose behavior could drift from the filtered one.

func (*PatternSet) Selects

func (s *PatternSet) Selects(candidate string) bool

Selects reports whether a source-relative path survives filtering.

type Resolved

type Resolved struct {
	Limits Limits
	// contains filtered or unexported fields
}

Resolved is an effective limit set plus the origin of each value.

func Resolve

func Resolve(inputs ...Input) Resolved

Resolve intersects every supplied input over the defaults: for each bound, the effective value is the smallest one anybody asked for, and the origin records who asked (DP-021).

Intersection rather than override is what stops a verification policy from being usable as privilege escalation: a policy shipped with an artifact can tighten what the embedding application allowed, never widen it.

func (Resolved) Each added in v0.2.0

func (r Resolved) Each() []Effective

Each returns every bound with its effective value and origin, in declaration order.

DP-021 says a verification result records which input supplied each effective value. It could not, because nothing could enumerate them: Origin answered for one bound at a time and the bound list was unexported, so a caller had to know every name in advance to ask. Adding a bound now reaches the report without anyone remembering to extend it.

func (Resolved) Origin

func (r Resolved) Origin(b Bound) Origin

Origin reports which input supplied b's effective value.

type SourceSpec

type SourceSpec struct {
	Name      string `json:"name" yaml:"name"`
	Type      string `json:"type" yaml:"type"`
	MountPath string `json:"mountPath,omitempty" yaml:"mountPath,omitempty"`

	Include []string `json:"include,omitempty" yaml:"include,omitempty"`
	Exclude []string `json:"exclude,omitempty" yaml:"exclude,omitempty"`

	// Config is resolver-specific.
	//
	// It is held untyped until a registered resolver decodes it, so an
	// unknown field in a resolver's config is that resolver's error to
	// report rather than something the loader must know about in advance.
	// A map rather than raw bytes because the manifest digest is computed
	// over canonical JSON of the typed model, and raw YAML has no canonical
	// form.
	Config map[string]any `json:"config,omitempty" yaml:"config,omitempty"`
}

SourceSpec declares one source.

func (*SourceSpec) Validate

func (s *SourceSpec) Validate() error

Validate checks one source declaration.

type Spec

type Spec struct {
	APIVersion string       `json:"apiVersion" yaml:"apiVersion"`
	Kind       string       `json:"kind" yaml:"kind"`
	Metadata   SpecMetadata `json:"metadata" yaml:"metadata"`
	Spec       SpecBody     `json:"spec" yaml:"spec"`
}

Spec is a bundle manifest: what a user intends to package.

It records intent and may name mutable things — a branch, a moving tag, a local directory. Resolving those into something immutable is the lock's job, and keeping the two documents separate is what makes "what did you ask for" and "what did you get" independently reviewable (DP-004).

func ParseSpec

func ParseSpec(data []byte) (*Spec, error)

ParseSpec decodes and validates a manifest from YAML or JSON.

Decoding is strict in both directions that matter: an unknown field is an error, and so is a duplicate mapping key. A manifest is a security-relevant document, and a reader that silently ignores a field it does not recognize will happily build something other than what was written.

func (*Spec) Normalized

func (s *Spec) Normalized() *Spec

Normalized returns a copy with set-like fields canonicalized.

The manifest digest is computed over this form, so that YAML spelling, key order, comments, and the order of an include list cannot change it. Two manifests that mean the same thing hash the same (DP-004).

func (*Spec) SourceByName

func (s *Spec) SourceByName(name string) (*SourceSpec, bool)

SourceByName returns a source declaration.

func (*Spec) Validate

func (s *Spec) Validate() error

Validate checks the manifest's structure.

type SpecBody

type SpecBody struct {
	Sources []SourceSpec `json:"sources" yaml:"sources"`
}

SpecBody holds the sources to compose.

type SpecMetadata

type SpecMetadata struct {
	// Name is a label for diagnostics and evidence. It does not affect
	// payload or subject identity: two bundles with the same content and
	// different names have the same digest (DP-002).
	Name string `json:"name" yaml:"name"`
}

SpecMetadata carries the bundle's logical name.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL