package signing import ( "crypto/ed25519" "encoding/base64" "errors" "strings" "testing" ) // The signing key, server name and key ID from the spec appendices, // section "Cryptographic Test Vectors". Every implementation that signs // JSON should produce the signatures below from them. const ( specSeed = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA1" specServerName = "domain" specKeyID = "1" ) func specKey(t *testing.T) *KeyPair { t.Helper() seed, err := base64.RawStdEncoding.DecodeString(specSeed) if err != nil { t.Fatalf("decode the spec seed: %v", err) } priv := ed25519.NewKeyFromSeed(seed) pub, ok := priv.Public().(ed25519.PublicKey) if !ok { t.Fatal("unexpected public key type") } return &KeyPair{ID: specKeyID, Private: priv, Public: pub} } // TestSpecJSONSigningVectors signs the two objects from the spec and // compares the result byte for byte. func TestSpecJSONSigningVectors(t *testing.T) { key := specKey(t) cases := []struct { name string in string want string }{ { name: "empty object", in: `{}`, want: `{"signatures":{"domain":{"ed25519:1":"K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"}}}`, }, { name: "object with data", in: `{"one":1,"two":"Two"}`, want: `{"one":1,"signatures":{"domain":{"ed25519:1":"KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"}},"two":"Two"}`, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, err := SignJSON(specServerName, key, []byte(tc.in)) if err != nil { t.Fatalf("SignJSON: %v", err) } if string(got) != tc.want { t.Errorf("SignJSON() =\n %s\nwant\n %s", got, tc.want) } if err := VerifyJSON(specServerName, "ed25519:"+specKeyID, key.Public, got); err != nil { t.Errorf("VerifyJSON on our own signature: %v", err) } }) } } // TestSignatureIgnoresUnsignedAndSignatures is the property that lets a // server add a signature, or a timestamp under unsigned, to an object // without invalidating what is already there. func TestSignatureIgnoresUnsignedAndSignatures(t *testing.T) { key := specKey(t) keyID := "ed25519:" + specKeyID signed, err := SignJSON(specServerName, key, []byte(`{"a":1,"unsigned":{"age":5}}`)) if err != nil { t.Fatalf("SignJSON: %v", err) } if !strings.Contains(string(signed), `"unsigned":{"age":5}`) { t.Errorf("unsigned was dropped from the signed object: %s", signed) } // Rewrite unsigned and add another server's signature; ours must // still verify. tampered := strings.Replace(string(signed), `"age":5`, `"age":9999`, 1) if err := VerifyJSON(specServerName, keyID, key.Public, []byte(tampered)); err != nil { t.Errorf("changing unsigned broke the signature: %v", err) } other, err := Generate("other") if err != nil { t.Fatal(err) } countersigned, err := SignJSON("second.example", other, signed) if err != nil { t.Fatalf("SignJSON for a second server: %v", err) } if err := VerifyJSON(specServerName, keyID, key.Public, countersigned); err != nil { t.Errorf("a second signature broke the first: %v", err) } if err := VerifyJSON("second.example", other.KeyID(), other.Public, countersigned); err != nil { t.Errorf("the second signature does not verify: %v", err) } } // TestSignJSONReplacesOurOwnSignature checks that signing twice with the // same key leaves one signature, not two entries fighting over the slot. func TestSignJSONReplacesOurOwnSignature(t *testing.T) { key := specKey(t) once, err := SignJSON(specServerName, key, []byte(`{"a":1}`)) if err != nil { t.Fatal(err) } twice, err := SignJSON(specServerName, key, once) if err != nil { t.Fatal(err) } if string(once) != string(twice) { t.Errorf("signing twice changed the object:\n %s\n %s", once, twice) } } func TestVerifyJSONRejects(t *testing.T) { key := specKey(t) keyID := "ed25519:" + specKeyID signed, err := SignJSON(specServerName, key, []byte(`{"a":1}`)) if err != nil { t.Fatal(err) } t.Run("content changed", func(t *testing.T) { tampered := strings.Replace(string(signed), `"a":1`, `"a":2`, 1) err := VerifyJSON(specServerName, keyID, key.Public, []byte(tampered)) if !errors.Is(err, ErrBadSignature) { t.Errorf("error = %v, want ErrBadSignature", err) } }) t.Run("wrong key", func(t *testing.T) { other, err := Generate("other") if err != nil { t.Fatal(err) } err = VerifyJSON(specServerName, keyID, other.Public, signed) if !errors.Is(err, ErrBadSignature) { t.Errorf("error = %v, want ErrBadSignature", err) } }) t.Run("no signature from that server", func(t *testing.T) { err := VerifyJSON("somebody.else", keyID, key.Public, signed) if !errors.Is(err, ErrNoSignature) { t.Errorf("error = %v, want ErrNoSignature", err) } }) t.Run("no signature with that key ID", func(t *testing.T) { err := VerifyJSON(specServerName, "ed25519:other", key.Public, signed) if !errors.Is(err, ErrNoSignature) { t.Errorf("error = %v, want ErrNoSignature", err) } }) t.Run("signature is not base64", func(t *testing.T) { broken := strings.Replace(string(signed), `"ed25519:1":"`, `"ed25519:1":"!!!`, 1) err := VerifyJSON(specServerName, keyID, key.Public, []byte(broken)) if !errors.Is(err, ErrBadSignature) { t.Errorf("error = %v, want ErrBadSignature", err) } }) t.Run("signature is the wrong length", func(t *testing.T) { short, err := SignJSON(specServerName, key, []byte(`{"a":1}`)) if err != nil { t.Fatal(err) } broken := strings.Replace(string(short), `"ed25519:1":"`, `"ed25519:1":"AAAA`, 1) err = VerifyJSON(specServerName, keyID, key.Public, []byte(broken)) if !errors.Is(err, ErrBadSignature) { t.Errorf("error = %v, want ErrBadSignature", err) } }) } func TestSigningBytes(t *testing.T) { got, err := SigningBytes([]byte(`{"b":2,"signatures":{"x":{}},"unsigned":{"age":1},"a":1}`)) if err != nil { t.Fatalf("SigningBytes: %v", err) } if want := `{"a":1,"b":2}`; string(got) != want { t.Errorf("SigningBytes() = %s, want %s", got, want) } } func TestExtractSignatures(t *testing.T) { sigs, err := ExtractSignatures([]byte(`{"signatures":{"a.example":{"ed25519:1":"sig"}}}`)) if err != nil { t.Fatalf("ExtractSignatures: %v", err) } if got, ok := sigs.Get("a.example", "ed25519:1"); !ok || got != "sig" { t.Errorf("Get() = %q, %v; want \"sig\", true", got, ok) } if _, ok := sigs.Get("b.example", "ed25519:1"); ok { t.Error("Get found a signature for a server that has none") } none, err := ExtractSignatures([]byte(`{}`)) if err != nil { t.Fatalf("ExtractSignatures on an unsigned object: %v", err) } if none != nil { t.Errorf("ExtractSignatures() = %v, want nil for an unsigned object", none) } } func TestSplitKeyID(t *testing.T) { cases := []struct { in string algorithm string id string ok bool }{ {"ed25519:abc", "ed25519", "abc", true}, {"ed25519:", "", "", false}, {":abc", "", "", false}, {"ed25519", "", "", false}, {"", "", "", false}, } for _, tc := range cases { algorithm, id, ok := SplitKeyID(tc.in) if algorithm != tc.algorithm || id != tc.id || ok != tc.ok { t.Errorf("SplitKeyID(%q) = %q, %q, %v; want %q, %q, %v", tc.in, algorithm, id, ok, tc.algorithm, tc.id, tc.ok) } } } func TestSignJSONRejectsBadInput(t *testing.T) { key := specKey(t) for _, tc := range []struct{ name, in string }{ {"not an object", `[1,2]`}, {"null", `null`}, {"not JSON", `{`}, {"a float somewhere", `{"a":1.5}`}, } { if _, err := SignJSON(specServerName, key, []byte(tc.in)); err == nil { t.Errorf("SignJSON(%s) succeeded, want an error", tc.name) } } if _, err := SignJSON("", key, []byte(`{}`)); err == nil { t.Error("SignJSON with no server name succeeded, want an error") } }