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
- Variables
- func IsProtected(annotations map[string]string) bool
- func SubjectDigest(data []byte) map[string]string
- type Envelope
- type Key
- func (k *Key) CanDecrypt() bool
- func (k *Key) CanEncrypt() bool
- func (k *Key) CanSign() bool
- func (k *Key) CanVerify() bool
- func (k *Key) Decrypt(blob []byte) ([]byte, error)
- func (k *Key) Describe() string
- func (k *Key) Encrypt(data []byte) ([]byte, error)
- func (k *Key) EncryptAlgorithm() string
- func (k *Key) Fingerprint() string
- func (k *Key) Identity() string
- func (k *Key) Private() bool
- func (k *Key) Protect(annotations map[string]string, data []byte, stmt Statement, mode Mode) error
- func (k *Key) Recover(annotations map[string]string) ([]byte, error)
- func (k *Key) Sign(data []byte) ([]byte, error)
- func (k *Key) SignAlgorithm() string
- func (k *Key) SignStatement(stmt Statement) (Envelope, error)
- func (k *Key) Supports(mode Mode) error
- func (k *Key) Symmetric() bool
- func (k *Key) Verify(data, sig []byte) error
- func (k *Key) VerifyAnnotations(annotations map[string]string, data []byte) (Verification, error)
- func (k *Key) VerifyEnvelope(env Envelope) ([]byte, error)
- type Mode
- type Predicate
- type Signature
- type Statement
- type Subject
- type Verification
Constants ¶
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" )
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" )
const ( AlgAESGCM = "aes-256-gcm" AlgRSAOAEP = "rsa-oaep-sha256-aes-256-gcm" )
const ( AlgHMACSHA256 = "hmac-sha256" AlgEd25519 = "ed25519" AlgECDSASHA256 = "ecdsa-sha256" AlgRSAPSSSHA256 = "rsa-pss-sha256" )
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.
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.
const MinRSABits = 2048
MinRSABits is the smallest accepted RSA modulus; smaller keys are considered broken.
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 ¶
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") )
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") )
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") )
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") )
var ErrSecretTooShort = fmt.Errorf("symmetric secret must be at least %d bytes (generate one with `openssl rand -hex 32`)", MinSecretLen)
Functions ¶
func IsProtected ¶
IsProtected reports whether annotations carry an attestation or encrypted copy.
func SubjectDigest ¶ added in v1.138.1
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
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 ParseKey ¶
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
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 ¶
CanDecrypt reports whether the key can decrypt (private material required).
func (*Key) CanEncrypt ¶
CanEncrypt reports whether the key can encrypt (any key of an encryption-capable type; a public key is enough).
func (*Key) Encrypt ¶
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 ¶
EncryptAlgorithm returns the encryption algorithm this key supports, or "" if the key type cannot encrypt (Ed25519).
func (*Key) Fingerprint ¶
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 ¶
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 ¶
Private reports whether the key holds private material (a secret or a private key), as opposed to a public-only key.
func (*Key) Protect ¶
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 ¶
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 ¶
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 ¶
SignAlgorithm returns the signature algorithm this key supports.
func (*Key) SignStatement ¶ added in v1.138.1
SignStatement returns a DSSE envelope over stmt, signed with this key.
func (*Key) Supports ¶
Supports reports whether the key can publish in mode, with a descriptive error when it cannot.
func (*Key) Verify ¶
Verify checks that sig is a valid signature over the DSSE pre-authentication encoding of data for this key.
func (*Key) VerifyAnnotations ¶
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
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
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
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
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
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
String renders the statement for humans, in a stable field order.
func (Statement) SubjectName ¶ added in v1.138.1
SubjectName returns the first subject's name, for display.
type Subject ¶ added in v1.138.1
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.