Documentation
¶
Overview ¶
Package sig re-implements the core of a Sigstore/Cosign-style signing stack on top of the Go standard library — no cosign, no sigstore, no external crypto. It provides:
- DSSE envelopes (Dead Simple Signing Envelope) with the standard PAE pre-authentication encoding, so signatures cover a payload *and* its type;
- keyed signers/verifiers over ed25519 and ECDSA P-256, with PEM (PKCS#8 / PKIX) serialization and content-addressed key IDs;
- a pluggable trust root plus an identity/issuer policy, so verification can answer "who signed this?" and not merely "is the math valid?";
- a local, Rekor-style append-only transparency log (RFC 6962 Merkle tree) with signed checkpoints and inclusion proofs, so an offline deployment still gets tamper-evident signing records;
- a self-contained Bundle that carries envelopes + inclusion proofs, the unit that gets attached to an image via OCI referrers and consumed by the verify module.
Threat model (read before touching the verify path) ¶
The attacker controls the registry, the network, and any unsigned bytes. They may swap manifests, strip or replace signatures, replay old signatures onto a new digest, or present a valid signature made by an untrusted key. The verify path therefore fails closed: an empty/unknown trust root verifies nothing, a signature by a key not in the trust root is not "unsigned-but-ok" but a hard failure, and the subject digest a signature commits to is compared against the digest actually being verified (so a valid signature for image A never authenticates image B). Determinism: ed25519 is deterministic (RFC 8032) and tested byte-for-byte; ECDSA is randomized, so it is only round-trip tested.
Index ¶
- Constants
- Variables
- func KeyID(pub crypto.PublicKey) (string, error)
- func LoadTrustConfig(data []byte) (*TrustRoot, Policy, error)
- func MarshalPrivateKeyPEM(s Signer) ([]byte, error)
- func MarshalPublicKeyPEM(v Verifier) ([]byte, error)
- func NewImagePayload(dockerRef, manifestDigest string, optional map[string]string) ([]byte, error)
- func PAE(payloadType string, payload []byte) []byte
- func ValidateDigest(d string) error
- func VerifyInclusion(rec *InclusionRecord, loggedBytes []byte, logVerifier Verifier) error
- func VerifyRekorInclusion(rec *InclusionRecord, loggedBytes []byte, logVerifier Verifier) error
- type Algorithm
- type Bundle
- type BundleEntry
- type Checkpoint
- type CosignIdentity
- type CosignPolicy
- type CosignVerifier
- type Critical
- type CriticalIdentity
- type CriticalImage
- type Envelope
- type InclusionRecord
- type LogEntry
- type Policy
- type PolicyConfig
- type Signature
- type Signer
- type SimpleSigning
- type TransLog
- type TrustConfig
- type TrustConfigKey
- type TrustRoot
- type TrustedKey
- type Verifier
- type VerifyResult
Constants ¶
const ( KindSignature = "signature" KindAttestation = "attestation" )
KindSignature and KindAttestation label bundle entries.
const BundleMediaType = "application/vnd.docker-security.bundle.v1+json"
BundleMediaType is the artifact type used for the bundle and its OCI referrer.
const SimpleSigningMediaType = "application/vnd.dev.cosign.simplesigning.v1+json"
SimpleSigningMediaType is the DSSE payload type for image signatures.
Variables ¶
var ( // ErrVerify is the umbrella error for any failed verification. The verify // path deliberately collapses distinct failures into one class so callers // cannot accidentally treat "wrong key" more leniently than "bad math". ErrVerify = errors.New("signature verification failed") // ErrUntrusted means a signature is cryptographically valid but was made by // a key that is not in the trust root. ErrUntrusted = errors.New("no trusted key verified the signature") // ErrPolicy means verification succeeded but the signer identity or issuer // did not satisfy the configured policy. ErrPolicy = errors.New("signer did not satisfy policy") )
Errors returned across the package. Callers gate on these with errors.Is.
Functions ¶
func KeyID ¶
KeyID derives a stable identifier for a public key: the hex-encoded SHA-256 of its DER PKIX (SubjectPublicKeyInfo) encoding. Two processes that hold the same key compute the same ID with no coordination, which is exactly what a keyid field in a DSSE signature needs.
func LoadTrustConfig ¶
LoadTrustConfig parses a JSON TrustConfig into a TrustRoot and Policy.
func MarshalPrivateKeyPEM ¶
MarshalPrivateKeyPEM encodes a signer's private key as a PKCS#8 PEM block.
func MarshalPublicKeyPEM ¶
MarshalPublicKeyPEM encodes a verifier's public key as a PKIX PEM block.
func NewImagePayload ¶
NewImagePayload builds a simple-signing payload for a manifest digest and an optional human reference. The digest must be a full "sha256:<hex>" string; an empty or malformed digest is rejected rather than silently signed, because a signature over an empty digest authenticates nothing.
func PAE ¶
PAE exposes the pre-authentication encoding for callers that need to sign or verify the exact bytes a DSSE signature covers (e.g. a transparency log entry).
func ValidateDigest ¶
ValidateDigest is the exported form of the digest check, reused by callers (registry, attest) that accept digests from untrusted input.
func VerifyInclusion ¶
func VerifyInclusion(rec *InclusionRecord, loggedBytes []byte, logVerifier Verifier) error
VerifyInclusion checks an inclusion record end to end against a trusted log verifier: (1) the checkpoint signature is valid under logVerifier, and (2) the entry hash + audit path recompute to the checkpoint's root. Both must hold — a valid proof against an unsigned root proves nothing, and a signed root with a bad proof does not cover this entry. The caller supplies loggedBytes (the original data) so the record cannot lie about what was logged.
func VerifyRekorInclusion ¶
func VerifyRekorInclusion(rec *InclusionRecord, loggedBytes []byte, logVerifier Verifier) error
VerifyRekorInclusion verifies that the signature envelope was recorded in a Rekor-style transparency log, reusing the Merkle inclusion primitive. It binds the cosign path to the same tamper-evident-log guarantee the keyed path has. logVerifier is the log's public key verifier (its checkpoint signer).
Types ¶
type Algorithm ¶
type Algorithm string
Algorithm names a supported signature scheme.
const ( // AlgEd25519 is EdDSA over Curve25519 (RFC 8032): deterministic signatures. AlgEd25519 Algorithm = "ed25519" // AlgECDSAP256 is ECDSA over NIST P-256 with SHA-256 digests. Signatures are // randomized, so they are verified by round-trip, never by byte-equality. AlgECDSAP256 Algorithm = "ecdsa-p256" )
type Bundle ¶
type Bundle struct {
// MediaType identifies this as a docker-security verification bundle.
MediaType string `json:"mediaType"`
// SubjectDigest is the image manifest digest ("sha256:<hex>") the entries
// pertain to. Verifiers cross-check this against the digest under test.
SubjectDigest string `json:"subjectDigest"`
// Entries holds the signatures/attestations.
Entries []BundleEntry `json:"entries"`
}
Bundle carries verifiable material bound to a single subject digest.
func ParseBundle ¶
ParseBundle decodes and shape-checks a bundle.
func (*Bundle) AddAttestation ¶
func (b *Bundle) AddAttestation(env *Envelope, inc *InclusionRecord)
AddAttestation appends an attestation envelope (with optional inclusion proof).
func (*Bundle) AddSignature ¶
func (b *Bundle) AddSignature(env *Envelope, inc *InclusionRecord)
AddSignature appends a signature envelope (with optional inclusion proof).
type BundleEntry ¶
type BundleEntry struct {
// Kind is "signature" or "attestation" (informational; verification keys off
// the envelope's payload type, not this tag).
Kind string `json:"kind"`
// Envelope is the DSSE envelope.
Envelope *Envelope `json:"envelope"`
// Inclusion is the transparency-log proof for Envelope, if logged.
Inclusion *InclusionRecord `json:"inclusion,omitempty"`
}
BundleEntry is one envelope with optional log proof and a kind tag.
type Checkpoint ¶
type Checkpoint struct {
// LogID identifies the log instance (hex SHA-256 of its public key).
LogID string `json:"log_id"`
Size int `json:"size"`
// RootHash is the hex Merkle root over the first Size entries.
RootHash string `json:"root_hash"`
// Signature is the log key's signature over the canonical checkpoint body.
Signature string `json:"signature"` // base64
}
Checkpoint is a signed statement of the log's state at a size: the Merkle root over all entries [0, Size). It is the anchor a proof is checked against.
type CosignIdentity ¶
type CosignIdentity struct {
// SubjectID is the SAN identity: an email, a SPIFFE ID, or a URI (e.g. a
// GitHub Actions workflow ref). It is what a policy's certificate-identity is
// matched against.
SubjectID string
// Issuer is the OIDC issuer URL from the Fulcio extension (e.g.
// "https://token.actions.githubusercontent.com").
Issuer string
// NotBefore/NotAfter are the certificate's validity window.
NotBefore, NotAfter time.Time
}
CosignIdentity is the signer identity extracted from a Fulcio certificate.
type CosignPolicy ¶
type CosignPolicy struct {
// CertificateIdentity, if set, must equal the certificate SAN identity, or —
// when it ends with "*" — be a prefix match (for workflow-ref families).
CertificateIdentity string
// CertificateOIDCIssuer, if set, must equal the certificate's OIDC issuer.
CertificateOIDCIssuer string
}
CosignPolicy constrains which keyless identities are acceptable. An empty policy accepts any identity that chains to the trusted roots — callers should almost always set at least CertificateIdentity + CertificateOIDCIssuer, since a valid-but-unexpected signer is exactly the attack keyless signing invites.
type CosignVerifier ¶
type CosignVerifier struct {
// contains filtered or unexported fields
}
CosignVerifier verifies keyless cosign signatures against a set of trusted Fulcio CA certificates.
func NewCosignVerifier ¶
func NewCosignVerifier(rootPEM []byte) (*CosignVerifier, error)
NewCosignVerifier builds a verifier trusting the given Fulcio root CA certificate(s), supplied as PEM. Intermediates, if any, may be included in the same PEM bundle.
func (*CosignVerifier) VerifyImageSignature ¶
func (v *CosignVerifier) VerifyImageSignature(certPEM, payload, signature []byte, pol CosignPolicy, signingTime time.Time) (*CosignIdentity, error)
VerifyImageSignature verifies a keyless signature over a simple-signing payload: it validates the certificate chain (as of signingTime), enforces the identity policy, and checks the signature under the certificate's public key. It returns the extracted identity on success.
certPEM is the signing certificate (Fulcio leaf); payload is the raw simple-signing JSON; signature is the raw signature bytes cosign produced over that payload. signingTime is when the signature was made (from the Rekor entry or the certificate's NotBefore when unknown).
type Critical ¶
type Critical struct {
Identity CriticalIdentity `json:"identity"`
Image CriticalImage `json:"image"`
Type string `json:"type"`
}
Critical holds the fields that are, by definition, security-relevant: change any of them and the signature no longer applies.
type CriticalIdentity ¶
type CriticalIdentity struct {
DockerReference string `json:"docker-reference"`
}
CriticalIdentity records the (mutable) reference the signer intended.
type CriticalImage ¶
type CriticalImage struct {
DockerManifestDigest string `json:"docker-manifest-digest"`
}
CriticalImage records the (immutable) manifest digest being signed.
type Envelope ¶
type Envelope struct {
// Payload is the base64 (standard, padded) encoding of the raw payload.
Payload string `json:"payload"`
// PayloadType is the payload's type URI (e.g. an in-toto or simple-signing
// media type). It is authenticated: it is part of the signed PAE.
PayloadType string `json:"payloadType"`
// Signatures holds one entry per signer.
Signatures []Signature `json:"signatures"`
}
Envelope is a DSSE v1 envelope. The payload is transported base64-encoded; the signatures cover the PAE of (payloadType, raw payload), never the base64 text.
func ParseEnvelope ¶
ParseEnvelope decodes a DSSE envelope from JSON, rejecting obviously malformed input (missing payload type or signatures) so downstream code can assume a well-formed shape.
func SignEnvelope ¶
SignEnvelope builds a DSSE envelope over payload with the given type, signed by each signer. Multiple signers produce multiple signatures on one envelope (threshold/co-signing), which is how "N of M maintainers" policies are met.
func (*Envelope) DecodePayload ¶
DecodePayload returns the raw (base64-decoded) payload bytes.
func (*Envelope) VerifyWith ¶
VerifyWith checks the envelope against a single verifier and returns nil only if at least one signature on the envelope validates under that key. It does not consult a trust root or policy — callers that need "who is allowed to sign" use TrustRoot.Verify instead.
type InclusionRecord ¶
type InclusionRecord struct {
Entry LogEntry `json:"entry"`
Checkpoint Checkpoint `json:"checkpoint"`
// Proof is the audit path (bottom-up sibling hashes), hex-encoded.
Proof []string `json:"proof"`
}
InclusionRecord is the portable proof handed to a verifier: the entry, the checkpoint it was included in, and the audit path linking them.
func (*InclusionRecord) Marshal ¶
func (r *InclusionRecord) Marshal() ([]byte, error)
Marshal renders an inclusion record as JSON.
type LogEntry ¶
type LogEntry struct {
Index int `json:"index"`
Hash string `json:"hash"` // hex SHA-256 of the logged bytes
}
LogEntry is one recorded item: the SHA-256 of the thing logged (typically a DSSE envelope's canonical bytes). Storing a hash, not the payload, keeps the log compact and avoids retaining potentially sensitive payloads.
type Policy ¶
type Policy struct {
// Identities, if non-empty, is the allow-list of acceptable signer
// identities. A signer whose identity is not listed fails ErrPolicy.
Identities []string
// Issuers, if non-empty, is the allow-list of acceptable OIDC issuers/CAs.
Issuers []string
}
Policy constrains which trusted signers are acceptable for a given artifact. A zero Policy (no identities, no issuers) accepts any key in the trust root — "trusted key" is itself the floor. Naming identities or issuers tightens it.
type PolicyConfig ¶
type PolicyConfig struct {
Identities []string `json:"identities,omitempty"`
Issuers []string `json:"issuers,omitempty"`
}
PolicyConfig is the on-disk form of a Policy.
type Signature ¶
type Signature struct {
// KeyID identifies the signing key (content-addressed; see KeyID).
KeyID string `json:"keyid,omitempty"`
// Sig is the base64 (standard, padded) signature over the PAE.
Sig string `json:"sig"`
}
Signature is one signer's contribution to an Envelope.
type Signer ¶
type Signer interface {
// Sign returns a signature over msg. For ed25519 msg is signed directly; for
// ECDSA msg is hashed with SHA-256 first. Callers pass the raw message (e.g.
// a DSSE PAE), not a pre-hash.
Sign(msg []byte) ([]byte, error)
// Verifier returns the public verifier matching this signer.
Verifier() Verifier
// KeyID is the content-addressed identifier of the public key.
KeyID() string
// Algorithm reports the signature scheme.
Algorithm() Algorithm
}
Signer produces signatures. Implementations wrap a private key and know their own public half so callers can publish a matching Verifier.
func GenerateKey ¶
GenerateKey creates a new signer for the given algorithm, drawing entropy from randr. Passing a fixed reader yields deterministic keys, which is how the test suite gets reproducible fixtures without committing secrets it must then rotate. In production, pass crypto/rand.Reader (or nil, which defaults to it).
func LoadSignerPEM ¶
LoadSignerPEM parses a PKCS#8 private-key PEM block into a Signer.
type SimpleSigning ¶
type SimpleSigning struct {
Critical Critical `json:"critical"`
Optional map[string]string `json:"optional,omitempty"`
}
SimpleSigning is the payload that binds a signature to an image manifest digest. It mirrors Cosign's structure so the intent is unambiguous.
func ParseImagePayload ¶
func ParseImagePayload(data []byte) (*SimpleSigning, error)
ParseImagePayload decodes a simple-signing payload and validates its shape.
func (*SimpleSigning) SignedDigest ¶
func (p *SimpleSigning) SignedDigest() string
SignedDigest returns the manifest digest this payload commits to.
type TransLog ¶
type TransLog struct {
// contains filtered or unexported fields
}
TransLog is an in-memory append-only transparency log signed by a log key. It is safe for concurrent use.
func NewTransLog ¶
NewTransLog creates a log signed by the given key. The log ID is the key's content-addressed ID, so a checkpoint names exactly which log key vouches for it.
func (*TransLog) Append ¶
func (l *TransLog) Append(data []byte) (*InclusionRecord, error)
Append records bytes (typically a marshaled DSSE envelope) and returns an inclusion record proving membership in the resulting checkpoint. Determinism: the same sequence of appends yields the same roots and, since the log key is ed25519, the same checkpoint signatures.
func (*TransLog) Checkpoint ¶
func (l *TransLog) Checkpoint() (Checkpoint, error)
Checkpoint returns a freshly signed checkpoint over the current log state.
type TrustConfig ¶
type TrustConfig struct {
Keys []TrustConfigKey `json:"keys"`
Policy PolicyConfig `json:"policy,omitempty"`
}
TrustConfig is the on-disk form of a trust root: PEM public keys with their identities. It is what an operator commits to a repo or hands the verify module, so trust is reviewable configuration rather than code.
type TrustConfigKey ¶
type TrustConfigKey struct {
// PublicKeyPEM is a PKIX PEM public key.
PublicKeyPEM string `json:"public_key_pem"`
Identity string `json:"identity,omitempty"`
Issuer string `json:"issuer,omitempty"`
}
TrustConfigKey is one entry in a TrustConfig.
type TrustRoot ¶
type TrustRoot struct {
// contains filtered or unexported fields
}
TrustRoot is the set of keys a verifier will accept, indexed by key ID.
func NewTrustRoot ¶
func NewTrustRoot() *TrustRoot
NewTrustRoot returns an empty trust root. An empty root verifies nothing — that is the safe default, not a bug.
func (*TrustRoot) Add ¶
func (t *TrustRoot) Add(k TrustedKey) error
Add registers a trusted key. A later Add for the same key ID replaces the earlier binding.
func (*TrustRoot) AddVerifier ¶
AddVerifier is a convenience for keys with no identity metadata.
func (*TrustRoot) KeyIDs ¶
KeyIDs returns the trusted key IDs in sorted order (for stable reporting).
func (*TrustRoot) Verify ¶
func (t *TrustRoot) Verify(env *Envelope, policy Policy) (VerifyResult, error)
Verify checks a DSSE envelope against the trust root and policy. It returns the identity of the first trusted key that both verifies a signature and satisfies the policy. Failure modes are distinct and wrapped:
- ErrUntrusted: a signature verified, but under no trusted key (or there were no signatures a trusted key could check).
- ErrPolicy: a trusted key verified, but its identity/issuer is not allowed by the policy.
The scan of trusted keys is order-independent (map iteration) but the outcome is not: a policy-satisfying match wins over a mere trusted match, so a multi-signer envelope is accepted as soon as one signer clears policy.
type TrustedKey ¶
type TrustedKey struct {
Verifier Verifier
// Identity is the signer identity (email, SPIFFE ID, or builder URI).
Identity string
// Issuer is the OIDC issuer or CA that vouches for the identity, if any.
Issuer string
}
TrustedKey binds a verifier to an identity and issuer, mirroring the subject/issuer a keyless (Fulcio) certificate would carry. In a keyed deployment these are administrator-assigned labels ("ci@corp.example", "https://accounts.google.com"); in a keyless one they come from the cert SAN.
type Verifier ¶
type Verifier interface {
// Verify returns nil if sig is a valid signature over msg for this key, and
// an error wrapping ErrVerify otherwise.
Verify(msg, sig []byte) error
// KeyID is the content-addressed identifier of the public key.
KeyID() string
// Algorithm reports the signature scheme.
Algorithm() Algorithm
// PublicKey returns the underlying crypto.PublicKey, for serialization.
PublicKey() crypto.PublicKey
}
Verifier checks signatures against a single public key.
func LoadVerifierPEM ¶
LoadVerifierPEM parses a PKIX public-key PEM block into a Verifier.
type VerifyResult ¶
type VerifyResult struct {
// KeyID is the trusted key that verified the envelope.
KeyID string
// Identity and Issuer are the labels bound to that key.
Identity string
Issuer string
}
VerifyResult reports the outcome of a successful trust-root verification.