112 lines
3.8 KiB
Go
112 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)},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|