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
- func DecodeConfig(config map[string]any, target any) error
- func NormalizeMountPath(mount string) (string, error)
- func NormalizePatterns(patterns []string) []string
- func Supported(f Format) bool
- func Template(sourcePath string) ([]byte, error)
- func ValidateName(name, field string) error
- type Bound
- type Config
- type ConfigFile
- type Digest
- type Effective
- type FileRecord
- type Format
- type Input
- type Limits
- type Lock
- type LockedFile
- type LockedSource
- type Origin
- type Path
- type Pattern
- type PatternSet
- type Resolved
- type SourceSpec
- type Spec
- type SpecBody
- type SpecMetadata
Constants ¶
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).
const ( KindBundle = "Bundle" KindBundleLock = "BundleLock" KindVerificationPolicy = "VerificationPolicy" )
Document kinds within APIVersionV1Alpha1.
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).
const ( SourceTypePath = "path" SourceTypeGit = "git" )
Built-in source types.
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.
const ConfigSchemaVersion = 1
ConfigSchemaVersion is the schema version of the v1 config blob.
const DefaultSpecName = "devproof.yaml"
DefaultSpecName is the conventional file name for a bundle manifest.
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.
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.
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).
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 ¶
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 ¶
NormalizeMountPath canonicalizes a mount path. Empty and "." both mean the bundle root.
func NormalizePatterns ¶
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 Template ¶ added in v0.2.0
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 ¶
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.
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 ¶
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 ¶
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 ParseDigest ¶
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.
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 ¶
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 ¶
ArtifactType returns the OCI artifactType for f.
func (Format) ConfigMediaType ¶
ConfigMediaType returns the config media type for f.
func (Format) LayerMediaType ¶
LayerMediaType returns the single filesystem layer media type for f.
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 ¶
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 ¶
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 (*Lock) SourceByName ¶
func (l *Lock) SourceByName(name string) (*LockedSource, bool)
SourceByName returns a locked source.
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.
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 ¶
ParsePattern compiles a selection pattern.
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 ¶
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
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.
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 ¶
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 ¶
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.
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.