Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions pkg/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
package manifest

import (
"bytes"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
Expand Down Expand Up @@ -193,16 +194,61 @@ func (m *Manifest) signingPayload() ([]byte, error) {
return []byte(payload), nil
}

// TrustedPublishers is the compile-time-embedded list of publisher
// ed25519 public keys ("ed25519:<base64>" or raw base64) that are
// trusted to sign manifests. Empty list = fail-closed (no publisher
// passes the trust-anchor check). Production builds MUST populate
// this list with the known-good publisher keys.
var TrustedPublishers []string

// VerifyTrustAnchor checks that Store.Publisher is on the trusted
// publishers list. Without this check, VerifySignature only confirms
// the manifest was signed by whoever claims to be the publisher;
// VerifyTrustAnchor confirms the publisher itself is known and trusted.
//
// Returns nil if Store.Publisher is in TrustedPublishers.
// Returns an error if TrustedPublishers is empty (fail-closed) or if
// the publisher is not found.
func (m *Manifest) VerifyTrustAnchor() error {
if len(TrustedPublishers) == 0 {
return fmt.Errorf("trust anchor: TrustedPublishers is empty — no publisher is trusted")
}

pubkeyRaw, ok := strings.CutPrefix(m.Store.Publisher, "ed25519:")
if !ok {
return fmt.Errorf("store.publisher must be \"ed25519:<base64>\"")
}
pubkey, err := base64.StdEncoding.DecodeString(pubkeyRaw)
if err != nil {
return fmt.Errorf("store.publisher: invalid base64: %w", err)
}
if len(pubkey) != ed25519.PublicKeySize {
return fmt.Errorf("store.publisher: wrong key length %d, want %d", len(pubkey), ed25519.PublicKeySize)
}

for _, trusted := range TrustedPublishers {
trustedRaw := strings.TrimPrefix(trusted, "ed25519:")
trustedKey, err := base64.StdEncoding.DecodeString(trustedRaw)
if err != nil {
continue // skip malformed entries
}
if bytes.Equal(pubkey, trustedKey) {
return nil
}
}
return fmt.Errorf("trust anchor: publisher %s is not on the trusted-publishers list", m.Store.Publisher)
}

// VerifySignature checks that Store.Signature is a valid ed25519
// signature over the signing payload, verified against the Store.Publisher
// key embedded in the manifest. This provides cryptographic integrity —
// tampering with any manifest field that feeds the signing payload
// (Publisher, ID, ManifestVersion, Binary.SHA256, Grants) will cause
// verification to fail.
//
// NOTE: This does NOT check that Store.Publisher is a trusted key;
// a trust-anchor check (verifying Store.Publisher against a
// daemon-embedded trusted-publisher pubkey) is the next hardening step.
// IMPORTANT: This does NOT check that Store.Publisher is a trusted key.
// Callers MUST also call VerifyTrustAnchor() after VerifySignature()
// to confirm the publisher is on the TrustedPublishers list.
func (m *Manifest) VerifySignature() error {
pubkeyRaw, ok := strings.CutPrefix(m.Store.Publisher, "ed25519:")
if !ok {
Expand Down
106 changes: 106 additions & 0 deletions pkg/manifest/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,112 @@ func TestVerifySignatureRejectsEmptySignature(t *testing.T) {
}
}

func TestVerifyTrustAnchorEmptyListIsFailClosed(t *testing.T) {
// With TrustedPublishers empty (default), VerifyTrustAnchor must reject all publishers.
orig := TrustedPublishers
TrustedPublishers = nil
defer func() { TrustedPublishers = orig }()

m := mustValid(t)
if err := m.VerifyTrustAnchor(); err == nil {
t.Error("expected error with empty TrustedPublishers, got nil")
}
}

func TestVerifyTrustAnchorRejectsUntrustedPublisher(t *testing.T) {
trustedPub, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
untrustedPub, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}

orig := TrustedPublishers
TrustedPublishers = []string{"ed25519:" + base64Enc(trustedPub)}
defer func() { TrustedPublishers = orig }()

m := mustValid(t)
m.Store.Publisher = "ed25519:" + base64Enc(untrustedPub)
if err := m.VerifyTrustAnchor(); err == nil {
t.Error("expected error for untrusted publisher, got nil")
}
}

func TestVerifyTrustAnchorAcceptsTrustedPublisher(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}

orig := TrustedPublishers
TrustedPublishers = []string{"ed25519:" + base64Enc(pub)}
defer func() { TrustedPublishers = orig }()

m := mustValid(t)
m.Store.Publisher = "ed25519:" + base64Enc(pub)
sig, err := signTestManifest(m, priv)
if err != nil {
t.Fatal(err)
}
m.Store.Signature = sig

// VerifySignature must pass for a valid signature.
if err := m.VerifySignature(); err != nil {
t.Fatalf("valid signature rejected: %v", err)
}
// VerifyTrustAnchor must pass because the publisher IS trusted.
if err := m.VerifyTrustAnchor(); err != nil {
t.Errorf("trusted publisher rejected by VerifyTrustAnchor: %v", err)
}
}

func TestVerifyTrustAnchorMultipleTrustedKeys(t *testing.T) {
pub1, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
pub2, priv2, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}

orig := TrustedPublishers
TrustedPublishers = []string{
"ed25519:" + base64Enc(pub1),
"ed25519:" + base64Enc(pub2),
}
defer func() { TrustedPublishers = orig }()

m := mustValid(t)
m.Store.Publisher = "ed25519:" + base64Enc(pub2)
sig, err := signTestManifest(m, priv2)
if err != nil {
t.Fatal(err)
}
m.Store.Signature = sig

if err := m.VerifySignature(); err != nil {
t.Fatalf("valid signature rejected: %v", err)
}
if err := m.VerifyTrustAnchor(); err != nil {
t.Errorf("second trusted publisher rejected: %v", err)
}
}

func TestVerifyTrustAnchorRejectsBadPublisherFormat(t *testing.T) {
orig := TrustedPublishers
TrustedPublishers = []string{"ed25519:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}
defer func() { TrustedPublishers = orig }()

m := mustValid(t)
m.Store.Publisher = "not-valid-publisher"
if err := m.VerifyTrustAnchor(); err == nil {
t.Error("expected error with bad publisher format, got nil")
}
}

func hasErrorContaining(errs []error, substr string) bool {
for _, e := range errs {
if strings.Contains(e.Error(), substr) {
Expand Down
Loading