protect

package
v1.138.1 Latest Latest
Warning

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

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

Documentation

Overview

Package protect signs or encrypts agent YAML shipped in OCI artifacts and verifies it on the way back.

A key is either a symmetric secret or an asymmetric key (private, or public-only). The kind is detected from the file contents: PEM-encoded (RFC 7468) or OpenSSH keys are asymmetric; anything else is a raw secret.

The YAML always stays in clear in the artifact layer. Depending on the Mode chosen by the publisher, the manifest annotations carry a signature (MAC or digital signature) and optionally an authenticated encrypted copy of the whole YAML that key holders can decrypt.

Attestation format

Signatures are carried in a DSSE envelope (Envelope) whose payload is an in-toto Statement v1 (Statement): the artifact's subject (the reference it was published as, plus the sha256 digest of the agent YAML) and a predicate holding the publication metadata (Predicate). The envelope is stored in clear in AnnotationAttestation, so any DSSE/in-toto consumer can read and check it, and only a key holder can have produced it.

The statement half is parsed strictly: `_type`, the subjects and their digests, and `predicateType` are what bind an artifact to its bytes and its location, so unknown fields there are refused. The predicate half is deliberately lenient — unknown fields are surfaced as opaque values (Predicate.Unknown) and an unknown predicateType yields "signature valid, predicate not understood" rather than a failure. Adding metadata must never break a deployed verifier; predicateType URIs are the version.

Security model

Verification answers "was this produced by a holder of the key?". For a symmetric secret, both the MAC and the AEAD ciphertext require the secret, so either annotation alone is proof. For an asymmetric key, only a signature proves possession of the private key: anyone holding the public key can encrypt. Encrypt mode with an asymmetric key therefore requires the private key and records both a signature and an encrypted copy, and verification with an asymmetric key always requires a signature. This also rules out downgrading a signed artifact to an encrypted-only one.

Signatures cover the DSSE pre-authentication encoding of the statement, and the statement carries the digest of the YAML, so the two are bound together: neither a swapped layer nor a swapped statement verifies. Because the subject records the publication reference, a signed artifact copied to another repository or tag is detected via Verification.CheckSubject. Serving an older signed version under the same tag is still not detected; pin digests when that matters.

Only the agent YAML and the statement's own contents are authenticated. The subject digest covers the YAML layer, not the manifest, so the other manifest annotations (author, licenses, revision, tags, the advertised creation date) are unauthenticated: a party who can push to the repository can rewrite them while the signature still verifies. Callers must not base decisions on them; use Verification.Statement instead. Authenticating the whole manifest would require publishing the envelope as a referring artifact (OCI Referrers API).

Index

Constants

View Source
const (
	// AnnotationAttestation holds the base64 DSSE [Envelope] whose payload is
	// the in-toto [Statement] describing this artifact: the reference it was
	// published as, the digest of the agent YAML, and the publication
	// metadata. Stored in clear so anyone (and any DSSE/in-toto tool) can read
	// it, and authenticated by the signature it carries so only a key holder
	// can have produced it.
	AnnotationAttestation = "io.docker.agent.attestation"
	// AnnotationPredicateType advertises the `predicateType` of the statement
	// inside AnnotationAttestation, so a consumer can tell what an attestation
	// is about without base64-decoding it. Same key BuildKit puts on its
	// in-toto attestation layers. Purely informational: it is not signed, so
	// verification always uses the value inside the envelope (see
	// [Statement.PredicateType]) and never this annotation.
	AnnotationPredicateType = "in-toto.io/predicate-type"
	// AnnotationSignatureAlgorithm names the algorithm of the signature inside
	// AnnotationAttestation.
	AnnotationSignatureAlgorithm = "io.docker.agent.signature.algorithm"
	// AnnotationEncrypted holds a base64 authenticated-encrypted copy of the
	// whole YAML layer.
	AnnotationEncrypted = "io.docker.agent.encrypted"
	// AnnotationEncryptedAlgorithm names the algorithm behind AnnotationEncrypted.
	AnnotationEncryptedAlgorithm = "io.docker.agent.encrypted.algorithm"
)
View Source
const (
	// PayloadType is the DSSE payloadType of the signed body: an in-toto
	// Statement. It is part of the pre-authentication encoding (see
	// paeEncode), so it cannot be swapped without invalidating the signature.
	PayloadType = "application/vnd.in-toto+json"
	// StatementType is the in-toto Statement v1 `_type`.
	StatementType = "https://in-toto.io/Statement/v1"
	// PredicateTypePublication identifies the predicate holding the
	// publication metadata of a shared agent. The URI is the version: a
	// breaking change to the predicate shape gets a new URI, and verifiers
	// that do not know a URI report the predicate as not understood instead of
	// rejecting the artifact.
	PredicateTypePublication = "https://docker.com/docker-agent/share/publication/v1"
)
View Source
const (
	AlgAESGCM  = "aes-256-gcm"
	AlgRSAOAEP = "rsa-oaep-sha256-aes-256-gcm"
)
View Source
const (
	AlgHMACSHA256   = "hmac-sha256"
	AlgEd25519      = "ed25519"
	AlgECDSASHA256  = "ecdsa-sha256"
	AlgRSAPSSSHA256 = "rsa-pss-sha256"
)
View Source
const EnvelopeMediaType = "application/vnd.dsse.envelope.v1+json"

EnvelopeMediaType is the media type of the value held in AnnotationAttestation, for consumers that want to know what they are looking at. The annotation stores the base64 of exactly this object.

View Source
const FilePrefix = "file://"

FilePrefix marks a ResolveKey value as a path to a key file. It is a literal prefix, not a URL scheme: no percent-decoding or authority parsing.

View Source
const MinRSABits = 2048

MinRSABits is the smallest accepted RSA modulus; smaller keys are considered broken.

View Source
const MinSecretLen = 16

MinSecretLen is the minimum accepted length of a symmetric secret. The clear YAML plus its MAC/ciphertext is an offline oracle for guessing the secret, and HKDF adds no entropy, so short secrets are refused outright.

Variables

View Source
var (
	ErrNotProtected     = errors.New("artifact is neither signed nor encrypted")
	ErrNotEncrypted     = errors.New("artifact has no encrypted copy")
	ErrNotSigned        = errors.New("artifact is not signed: an encrypted copy alone does not prove who published it when the key is asymmetric")
	ErrTampered         = errors.New("encrypted copy does not match the artifact content")
	ErrAlgorithmMism    = errors.New("algorithm mismatch")
	ErrEncryptNeedsPriv = errors.New("encrypt mode with an asymmetric key requires the private key, so the artifact can also be signed")
)
View Source
var (
	ErrMalformedAttestation = errors.New("malformed attestation annotation")
	ErrUnknownStatementType = errors.New("unsupported in-toto statement type")
	ErrStatementMismatch    = errors.New("attestation does not describe this artifact")
	ErrSubjectMismatch      = errors.New("attested subject does not match the reference being read")
)
View Source
var (
	ErrCannotEncrypt = errors.New("key cannot encrypt")
	ErrCannotDecrypt = errors.New("key cannot decrypt: a private key or secret is required")
	ErrDecryption    = errors.New("decryption failed")
)
View Source
var (
	ErrInvalidSignature = errors.New("signature verification failed")
	ErrCannotSign       = errors.New("key cannot sign: a private key or secret is required")
	ErrCannotVerify     = errors.New("key cannot verify signatures")
)
View Source
var ErrSecretTooShort = fmt.Errorf("symmetric secret must be at least %d bytes (generate one with `openssl rand -hex 32`)", MinSecretLen)

Functions

func IsProtected

func IsProtected(annotations map[string]string) bool

IsProtected reports whether annotations carry an attestation or encrypted copy.

func SubjectDigest added in v1.138.1

func SubjectDigest(data []byte) map[string]string

SubjectDigest returns the in-toto digest set of data: bare hex, no "sha256:" prefix.

Types

type Envelope added in v1.138.1

type Envelope struct {
	// Payload is the base64 SERIALIZED_BODY. Verification covers the exact
	// bytes it decodes to, never a re-serialized copy.
	Payload string `json:"payload"`
	// PayloadType must be [PayloadType]; it is authenticated through the PAE.
	PayloadType string `json:"payloadType"`
	// Signatures holds one entry per signer. At least one must verify.
	Signatures []Signature `json:"signatures"`
}

Envelope is a DSSE envelope (Payload holds the base64 in-toto Statement). It is stored verbatim in AnnotationAttestation.

func ParseEnvelope added in v1.138.1

func ParseEnvelope(raw []byte) (Envelope, error)

ParseEnvelope decodes a DSSE envelope. Nothing in it is authenticated yet.

type Key

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

Key is a symmetric secret or an asymmetric key (private, or public-only).

func LoadKey

func LoadKey(path string) (*Key, error)

LoadKey reads and parses a key file. See ParseKey.

func ParseKey

func ParseKey(data []byte) (*Key, error)

ParseKey detects the key kind from its encoding. PEM blocks and OpenSSH authorized_keys lines (with or without options) are parsed as asymmetric keys. Anything else is a raw symmetric secret (surrounding whitespace is trimmed so a trailing newline does not change the key) — unless it contains a PEM boundary or an OpenSSH key-type token, in which case it is treated as a broken key file and rejected. Failing closed here matters: a truncated or BOM-prefixed public key must never silently become an HMAC key made of public material. Random secrets never contain those markers.

func ResolveKey added in v1.133.0

func ResolveKey(value string) (*Key, error)

ResolveKey parses a key given on the command line: a value prefixed with FilePrefix names a key file (a leading ~ is expanded, since shells do not expand it mid-word), anything else is the key material itself.

func (*Key) CanDecrypt

func (k *Key) CanDecrypt() bool

CanDecrypt reports whether the key can decrypt (private material required).

func (*Key) CanEncrypt

func (k *Key) CanEncrypt() bool

CanEncrypt reports whether the key can encrypt (any key of an encryption-capable type; a public key is enough).

func (*Key) CanSign

func (k *Key) CanSign() bool

CanSign reports whether the key can produce signatures.

func (*Key) CanVerify

func (k *Key) CanVerify() bool

CanVerify reports whether the key can verify signatures.

func (*Key) Decrypt

func (k *Key) Decrypt(blob []byte) ([]byte, error)

Decrypt opens a blob produced by Encrypt with the matching key.

func (*Key) Describe

func (k *Key) Describe() string

Describe returns a short human-readable description of the key.

func (*Key) Encrypt

func (k *Key) Encrypt(data []byte) ([]byte, error)

Encrypt returns an authenticated ciphertext of data that only holders of the secret (symmetric) or of the private key (asymmetric) can open. The algorithm label is bound as AEAD additional data.

Blob layouts:

  • aes-256-gcm: nonce || ciphertext
  • ecies-*: ephemeralPub || nonce || ciphertext
  • rsa-oaep-*: wrappedKey || nonce || ciphertext

func (*Key) EncryptAlgorithm

func (k *Key) EncryptAlgorithm() string

EncryptAlgorithm returns the encryption algorithm this key supports, or "" if the key type cannot encrypt (Ed25519).

func (*Key) Fingerprint

func (k *Key) Fingerprint() string

Fingerprint returns a stable hex identifier for the key material: SHA-256 of the secret, or of the PKIX-encoded public key. Private and public halves of the same pair share a fingerprint.

func (*Key) Identity

func (k *Key) Identity() string

Identity extends Fingerprint with the key's role, so that the private and public halves of a pair — which have different verification capabilities — are told apart. Suitable as a cache key for verification results.

func (*Key) Private

func (k *Key) Private() bool

Private reports whether the key holds private material (a secret or a private key), as opposed to a public-only key.

func (*Key) Protect

func (k *Key) Protect(annotations map[string]string, data []byte, stmt Statement, mode Mode) error

Protect records the protection for data in annotations according to mode. The statement is the in-toto attestation the signature covers; it is stored in clear inside a DSSE envelope.

func (*Key) Recover

func (k *Key) Recover(annotations map[string]string) ([]byte, error)

Recover decrypts the encrypted copy carried in annotations, returning the clear YAML. It works from the annotations alone, without the layer. Note that it does not check the signature; use VerifyAnnotations for that.

func (*Key) Sign

func (k *Key) Sign(data []byte) ([]byte, error)

Sign returns the raw signature (or MAC) of data. data is a DSSE SERIALIZED_BODY: what is actually signed is its pre-authentication encoding (see paeEncode), which binds the payload type and so keeps a signature from being reused over a body of another type.

func (*Key) SignAlgorithm

func (k *Key) SignAlgorithm() string

SignAlgorithm returns the signature algorithm this key supports.

func (*Key) SignStatement added in v1.138.1

func (k *Key) SignStatement(stmt Statement) (Envelope, error)

SignStatement returns a DSSE envelope over stmt, signed with this key.

func (*Key) Supports

func (k *Key) Supports(mode Mode) error

Supports reports whether the key can publish in mode, with a descriptive error when it cannot.

func (*Key) Symmetric

func (k *Key) Symmetric() bool

Symmetric reports whether the key is a raw secret.

func (*Key) Verify

func (k *Key) Verify(data, sig []byte) error

Verify checks that sig is a valid signature over the DSSE pre-authentication encoding of data for this key.

func (*Key) VerifyAnnotations

func (k *Key) VerifyAnnotations(annotations map[string]string, data []byte) (Verification, error)

VerifyAnnotations checks that data is what a holder of this key published, using the protection annotations carry, and reports what was checked.

A DSSE attestation, when present, is always verified: the signature covers the in-toto statement, whose subject digest must in turn match data — so a valid statement paired with a different layer is rejected. The authenticated statement is returned in the report, and callers that know which reference they requested should also call Verification.CheckSubject.

An encrypted copy is decrypted and compared to data when the key can decrypt; a public key only checks its algorithm label and relies on the signature. With an asymmetric key a signature is mandatory, since anyone holding the public key could have produced the encrypted copy. ErrNotProtected is returned when the artifact carries no protection at all.

func (*Key) VerifyEnvelope added in v1.138.1

func (k *Key) VerifyEnvelope(env Envelope) ([]byte, error)

VerifyEnvelope checks env against this key and returns the payload bytes it authenticated — the exact SERIALIZED_BODY, which is what callers must parse. Re-encoding the returned bytes, or reading the payload out of the envelope again afterwards, would break that guarantee.

Every signature is tried: keyid is an unauthenticated hint and must not select which one counts.

type Mode

type Mode string

Mode selects what the publisher records in the annotations.

const (
	// ModeSign records a signature (asymmetric key) or MAC (secret).
	// Holders of the matching public key or secret can verify integrity.
	ModeSign Mode = "sign"
	// ModeEncrypt records an encrypted copy of the whole YAML. Holders of the
	// secret or private key can both verify integrity and recover the YAML
	// from the annotation alone. With an asymmetric key a signature is
	// recorded as well (see the package security model).
	ModeEncrypt Mode = "encrypt"
)

type Predicate added in v1.138.1

type Predicate struct {
	// Registry is the registry host, e.g. "index.docker.io".
	Registry string `json:"registry"`
	// Repository is the namespace and repository, e.g. "gtardif/myagent".
	Repository string `json:"repository"`
	// Tag is the tag, e.g. "v1". Empty for a digest reference.
	Tag string `json:"tag,omitempty"`
	// Created is the publication time, RFC 3339, UTC.
	Created string `json:"created"`
	// Unknown holds the fields this version does not understand, as the
	// compact JSON text they were received as. Never written.
	Unknown map[string]string `json:"-"`
}

Predicate is the publication metadata of a shared agent: where the artifact was published and when.

It is deliberately lenient. Fields a future publisher adds must not break a deployed verifier, so anything this version does not know is surfaced in Unknown rather than rejected, and a known field carrying an unexpected JSON type is treated the same way instead of failing the artifact. Everything security-critical lives in the statement instead (see Statement).

type Signature added in v1.138.1

type Signature struct {
	// KeyID is an unauthenticated hint identifying the signing key. It must
	// never drive a security decision: verification tries every signature.
	KeyID string `json:"keyid,omitempty"`
	// Sig is the base64 raw signature (or MAC).
	Sig string `json:"sig"`
}

Signature is one DSSE signature over PAE(payloadType, body).

type Statement added in v1.138.1

type Statement struct {
	// Type is always [StatementType].
	Type string
	// Subject lists the artifacts the predicate is about. A statement attests
	// all of its subjects equally: a match against any entry is a match.
	Subject []Subject
	// PredicateType names the predicate shape, and is its version.
	PredicateType string
	// Predicate is the publication metadata, populated only when
	// PredicateUnderstood is set.
	Predicate Predicate
	// PredicateRaw is the predicate exactly as received (nil when absent). It
	// is what Marshal re-emits, so unknown fields survive a round-trip.
	PredicateRaw json.RawMessage
	// PredicateUnderstood reports whether PredicateType is a shape this
	// version knows how to read. A false value is a valid outcome: the
	// signature is still verified, the metadata is simply opaque.
	PredicateUnderstood bool
}

Statement is an in-toto Statement v1: the signed body of the envelope.

The security-critical part is parsed strictly: `_type`, `subject` (each with its digest set) and `predicateType` bind the artifact to a location and to its bytes, so unknown fields there are refused rather than ignored. The predicate is parsed leniently — see Predicate.

func NewStatement added in v1.138.1

func NewStatement(ref string, data []byte, created time.Time) (Statement, error)

NewStatement describes data published at ref at time created.

The subject digest covers the agent YAML, not the manifest: annotations are part of the manifest, so a manifest digest could never be recorded inside one without a circular dependency. Attesting the manifest instead would mean storing the envelope as a referring artifact (OCI Referrers API).

func ParseStatement added in v1.138.1

func ParseStatement(raw []byte) (Statement, error)

ParseStatement decodes a statement from the exact bytes the signature covered. Callers must verify those bytes first: nothing here is trustworthy before that, and re-serializing to verify would make verification depend on this process's JSON encoder.

The statement envelope is strict — unknown fields, a foreign `_type` or a subject without a usable digest are refused. The predicate is not: an unknown predicateType or unknown predicate fields yield a statement whose metadata is (partly) opaque, not an error.

func (Statement) Digest added in v1.138.1

func (s Statement) Digest() string

Digest returns the first subject's digest in the usual "sha256:<hex>" form, for display. In the statement itself digests are bare hex.

func (Statement) Marshal added in v1.138.1

func (s Statement) Marshal() ([]byte, error)

Marshal returns the statement JSON to sign and store. The predicate is re-emitted byte-for-byte when it came from the wire, so unknown fields are never dropped.

func (Statement) String added in v1.138.1

func (s Statement) String() string

String renders the statement for humans, in a stable field order.

func (Statement) SubjectName added in v1.138.1

func (s Statement) SubjectName() string

SubjectName returns the first subject's name, for display.

type Subject added in v1.138.1

type Subject struct {
	Name   string            `json:"name"`
	Digest map[string]string `json:"digest"`
}

Subject is one in-toto subject: a name and a digest set of bare-hex digests.

type Verification

type Verification struct {
	// SignatureAlgorithm is set when a signature was verified.
	SignatureAlgorithm string
	// EncryptedAlgorithm is set when the encrypted copy was decrypted and
	// matched the content. It stays empty for a public key, which can only
	// check the copy's algorithm label.
	EncryptedAlgorithm string
	// Statement is the authenticated in-toto statement the publisher signed.
	// Set whenever SignatureAlgorithm is.
	Statement Statement
}

Verification reports which protections VerifyAnnotations actually checked.

func (Verification) CheckSubject added in v1.138.1

func (v Verification) CheckSubject(ref string) error

CheckSubject reports whether the attestation names ref as a subject. Callers that know which reference they requested should use this to detect a signed artifact copied to another location, which the signature alone cannot catch. It is a no-op when the artifact carried no attestation (nothing was signed) or when ref cannot be parsed.

func (Verification) SignatureAlgorithmSummary added in v1.138.1

func (v Verification) SignatureAlgorithmSummary() string

SignatureAlgorithmSummary names the protections that were checked, without the attested metadata.

func (Verification) String

func (v Verification) String() string

String reports what was checked, including the attested metadata.

Jump to

Keyboard shortcuts

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