// 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)}, }, } }