165 lines
4.6 KiB
Go
165 lines
4.6 KiB
Go
|
|
package signing
|
||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/ed25519"
|
||
|
|
"errors"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
type doc struct {
|
||
|
|
Revision struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
State string `json:"state"`
|
||
|
|
Upstream string `json:"upstream"`
|
||
|
|
Signature *Signature `json:"signature,omitempty"`
|
||
|
|
} `json:"revision"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func newDoc(id, state string) doc {
|
||
|
|
var d doc
|
||
|
|
d.Revision.ID = id
|
||
|
|
d.Revision.State = state
|
||
|
|
d.Revision.Upstream = "http://adapter:8080"
|
||
|
|
return d
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSignAndVerify(t *testing.T) {
|
||
|
|
signer, pub, err := GenerateKey("key-1")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
v := NewVerifier(map[string]ed25519.PublicKey{"key-1": pub})
|
||
|
|
|
||
|
|
d := newDoc("R-1", "stable")
|
||
|
|
sig, err := signer.Sign(d)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := v.Verify(d, &sig); err != nil {
|
||
|
|
t.Fatalf("freshly signed document does not verify: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSignatureCoversContent is the property the whole barrier depends on: a
|
||
|
|
// tampered descriptor must not verify.
|
||
|
|
func TestSignatureCoversContent(t *testing.T) {
|
||
|
|
signer, pub, _ := GenerateKey("key-1")
|
||
|
|
v := NewVerifier(map[string]ed25519.PublicKey{"key-1": pub})
|
||
|
|
|
||
|
|
d := newDoc("R-1", "experiment")
|
||
|
|
sig, _ := signer.Sign(d)
|
||
|
|
|
||
|
|
// Promoting a signed experiment to stable by editing the descriptor is
|
||
|
|
// exactly the attack the signature exists to stop.
|
||
|
|
tampered := d
|
||
|
|
tampered.Revision.State = "stable"
|
||
|
|
if err := v.Verify(tampered, &sig); !errors.Is(err, ErrBadSignature) {
|
||
|
|
t.Errorf("a tampered descriptor verified: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
redirected := d
|
||
|
|
redirected.Revision.Upstream = "http://attacker:8080"
|
||
|
|
if err := v.Verify(redirected, &sig); !errors.Is(err, ErrBadSignature) {
|
||
|
|
t.Errorf("a redirected upstream verified: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSignatureMemberIsExcluded checks that a document signs its content and
|
||
|
|
// not its own signature, so a signed document round-trips.
|
||
|
|
func TestSignatureMemberIsExcluded(t *testing.T) {
|
||
|
|
signer, pub, _ := GenerateKey("key-1")
|
||
|
|
v := NewVerifier(map[string]ed25519.PublicKey{"key-1": pub})
|
||
|
|
|
||
|
|
d := newDoc("R-1", "stable")
|
||
|
|
sig, _ := signer.Sign(d)
|
||
|
|
|
||
|
|
attached := d
|
||
|
|
attached.Revision.Signature = &sig
|
||
|
|
if err := v.Verify(attached, &sig); err != nil {
|
||
|
|
t.Fatalf("a document carrying its own signature does not verify: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUnsignedIsRefused(t *testing.T) {
|
||
|
|
_, pub, _ := GenerateKey("key-1")
|
||
|
|
v := NewVerifier(map[string]ed25519.PublicKey{"key-1": pub})
|
||
|
|
|
||
|
|
// A verifier that passes unsigned input is worse than no verifier, because
|
||
|
|
// it reports success.
|
||
|
|
if err := v.Verify(newDoc("R-1", "stable"), nil); !errors.Is(err, ErrUnsigned) {
|
||
|
|
t.Errorf("unsigned document returned %v, want ErrUnsigned", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUntrustedKeyIsRefused(t *testing.T) {
|
||
|
|
rogue, _, _ := GenerateKey("rogue-key")
|
||
|
|
_, trustedPub, _ := GenerateKey("key-1")
|
||
|
|
v := NewVerifier(map[string]ed25519.PublicKey{"key-1": trustedPub})
|
||
|
|
|
||
|
|
d := newDoc("R-1", "stable")
|
||
|
|
sig, _ := rogue.Sign(d)
|
||
|
|
|
||
|
|
if err := v.Verify(d, &sig); !errors.Is(err, ErrUnknownKey) {
|
||
|
|
t.Errorf("a signature from an untrusted key returned %v, want ErrUnknownKey", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAlgorithmCannotBeDowngraded(t *testing.T) {
|
||
|
|
signer, pub, _ := GenerateKey("key-1")
|
||
|
|
v := NewVerifier(map[string]ed25519.PublicKey{"key-1": pub})
|
||
|
|
|
||
|
|
d := newDoc("R-1", "stable")
|
||
|
|
sig, _ := signer.Sign(d)
|
||
|
|
sig.Algorithm = "none"
|
||
|
|
|
||
|
|
if err := v.Verify(d, &sig); !errors.Is(err, ErrUnsupportedAlgorithm) {
|
||
|
|
t.Errorf("an algorithm downgrade returned %v, want ErrUnsupportedAlgorithm", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestCanonicalizationIsStable matters because an adapter or Daimon in another
|
||
|
|
// language must be able to produce a signature this verifier accepts.
|
||
|
|
func TestCanonicalizationIsStable(t *testing.T) {
|
||
|
|
d := newDoc("R-1", "stable")
|
||
|
|
|
||
|
|
first, err := Canonicalize(d)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
for i := 0; i < 100; i++ {
|
||
|
|
again, err := Canonicalize(d)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if string(again) != string(first) {
|
||
|
|
t.Fatalf("canonical form varied between runs:\n%s\n%s", first, again)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Key order in the source must not change the canonical bytes.
|
||
|
|
fromMap := map[string]any{
|
||
|
|
"revision": map[string]any{
|
||
|
|
"upstream": "http://adapter:8080",
|
||
|
|
"state": "stable",
|
||
|
|
"id": "R-1",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
mapForm, err := Canonicalize(fromMap)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if string(mapForm) != string(first) {
|
||
|
|
t.Errorf("canonical form depends on key order:\n%s\n%s", first, mapForm)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNewSignerRejectsBadInput(t *testing.T) {
|
||
|
|
_, priv, _ := ed25519.GenerateKey(nil)
|
||
|
|
if _, err := NewSigner("", priv); err == nil {
|
||
|
|
t.Error("a signer with no key id was accepted")
|
||
|
|
}
|
||
|
|
if _, err := NewSigner("key-1", ed25519.PrivateKey("short")); err == nil {
|
||
|
|
t.Error("a malformed private key was accepted")
|
||
|
|
}
|
||
|
|
}
|