Some checks failed
validate / Rego (opa fmt + opa test) (push) Successful in 16s
validate / JSON Schema (ajv) (push) Successful in 20s
build-images / signing-service image (push) Successful in 24s
validate / DB (migrate up/down dry-run) (push) Failing after 26s
validate / Go (vet + test) (push) Successful in 1m16s
Adds services/verification-service exposing POST /api/v1/verify/chain for sink:dummy-sink. The handler runs the six hard-coded chain checks described in DEV-612 and reserves decision_id in a new `verification_decisions` table so a replayed sink delivery is refused without recomputing signatures. - DSSE signature verification against configurable approval + transform signer key_ids, loaded from VERIFICATION_SIGNER_KEYS (pem/hex/b64/b64u) - RFC 6962 inclusion-proof verifier (shared leaf/node hashing with services/transparency-log/pkg/backend) - Content binding: subject.digest.sha256 must match predicate.content_hash (approval) and request.payload_hash (transform) - Hard-coded coverage of allowed_transformations / allowed_sinks / expires_at predicates (OPA deferred to Phase 2 per the subtask) - Replay guard via new db/trust/migrations/0005_verification_decisions (in-memory store ships; Postgres skeleton mirrors sibling pattern) - Dockerfile mirrors signing-service pattern (distroless static + nonroot) - README documents endpoint, reason-code taxonomy, required predicate fields, and the Phase-2 non-scope Tests: happy path + 7 negatives (bad signature, missing inclusion proof, hash drift, sink not allowed, expired, replay, operation not allowed). Co-Authored-By: Paperclip <noreply@paperclip.ing>
111 lines
3.8 KiB
Go
111 lines
3.8 KiB
Go
// Package dsse models the DSSE envelope shape the signing-service produces
|
|
// and verifies an envelope's Ed25519 signature using the DSSEv1 Pre-
|
|
// Authentication Encoding (PAE) from the DSSE spec.
|
|
//
|
|
// The envelope shape mirrors services/testagent-transform/internal/dsse so
|
|
// an entry fetched from the transparency-log round-trips through either
|
|
// package byte-for-byte.
|
|
package dsse
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// EnvelopeSignature mirrors the DSSE spec.
|
|
type EnvelopeSignature struct {
|
|
KeyID string `json:"keyid"`
|
|
Sig string `json:"sig"`
|
|
}
|
|
|
|
// Envelope is the DSSE envelope the signing-service returns. Payload is
|
|
// base64url (no padding) of the canonical in-toto Statement bytes.
|
|
type Envelope struct {
|
|
PayloadType string `json:"payloadType"`
|
|
Payload string `json:"payload"`
|
|
Signatures []EnvelopeSignature `json:"signatures"`
|
|
}
|
|
|
|
// Keyring maps keyid (as embedded in the envelope signature) to the
|
|
// Ed25519 public key that signed it.
|
|
type Keyring map[string]ed25519.PublicKey
|
|
|
|
// ErrNoSignatures is returned when the envelope has no signatures to try.
|
|
var ErrNoSignatures = errors.New("dsse: envelope has no signatures")
|
|
|
|
// ErrUnknownKey is returned when none of the envelope's signatures has a
|
|
// keyid that is present in the verifier's keyring.
|
|
var ErrUnknownKey = errors.New("dsse: no signature keyid is in the keyring")
|
|
|
|
// ErrSignatureInvalid is returned when at least one keyring-matching
|
|
// signature was tried and none verified.
|
|
var ErrSignatureInvalid = errors.New("dsse: no signature verified against its keyring entry")
|
|
|
|
// Verify checks that at least one signature on env matches a public key in
|
|
// kr. The payload is base64url-decoded, the DSSEv1 PAE is reconstructed,
|
|
// and ed25519.Verify is applied. On success the decoded payload bytes and
|
|
// the matched keyid are returned so the caller can parse the Statement
|
|
// and record the signing actor.
|
|
func Verify(env Envelope, kr Keyring) ([]byte, string, error) {
|
|
if len(env.Signatures) == 0 {
|
|
return nil, "", ErrNoSignatures
|
|
}
|
|
payload, err := base64.RawURLEncoding.DecodeString(env.Payload)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("dsse: decode payload: %w", err)
|
|
}
|
|
pae := pae(env.PayloadType, payload)
|
|
|
|
tried := false
|
|
for _, sig := range env.Signatures {
|
|
pub, ok := kr[sig.KeyID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
tried = true
|
|
raw, err := base64.RawURLEncoding.DecodeString(sig.Sig)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if ed25519.Verify(pub, pae, raw) {
|
|
return payload, sig.KeyID, nil
|
|
}
|
|
}
|
|
if !tried {
|
|
return nil, "", ErrUnknownKey
|
|
}
|
|
return nil, "", ErrSignatureInvalid
|
|
}
|
|
|
|
// pae reconstructs the DSSEv1 Pre-Authentication Encoding:
|
|
//
|
|
// "DSSEv1 " || len(payloadType) || " " || payloadType || " " || len(payload) || " " || payload
|
|
//
|
|
// (ASCII decimal lengths, bytes.) See
|
|
// https://github.com/secure-systems-lab/dsse/blob/master/protocol.md.
|
|
func pae(payloadType string, payload []byte) []byte {
|
|
prefix := fmt.Sprintf("DSSEv1 %d %s %d ", len(payloadType), payloadType, len(payload))
|
|
out := make([]byte, 0, len(prefix)+len(payload))
|
|
out = append(out, prefix...)
|
|
out = append(out, payload...)
|
|
return out
|
|
}
|
|
|
|
// Sign is a test helper that builds an Envelope by signing stmtBytes with
|
|
// priv. Production signing lives in signing-service; the verifier only
|
|
// exposes Sign so the chain tests can craft envelopes that are supposed
|
|
// to verify (and tamper variants that are supposed to fail) without a
|
|
// dependency on the signing-service module.
|
|
func Sign(payloadType string, payloadBytes []byte, keyID string, priv ed25519.PrivateKey) Envelope {
|
|
paeBytes := pae(payloadType, payloadBytes)
|
|
sig := ed25519.Sign(priv, paeBytes)
|
|
return Envelope{
|
|
PayloadType: payloadType,
|
|
Payload: base64.RawURLEncoding.EncodeToString(payloadBytes),
|
|
Signatures: []EnvelopeSignature{
|
|
{KeyID: keyID, Sig: base64.RawURLEncoding.EncodeToString(sig)},
|
|
},
|
|
}
|
|
}
|