diff --git a/conformance_test.go b/conformance_test.go index ae4e719..e95f46d 100644 --- a/conformance_test.go +++ b/conformance_test.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto" "crypto/ecdsa" + "crypto/ed25519" "crypto/elliptic" "crypto/rsa" _ "crypto/sha256" @@ -12,6 +13,7 @@ import ( "encoding/hex" "encoding/json" "errors" + "io" "math/big" "os" "path/filepath" @@ -71,6 +73,7 @@ var testCases = []struct { {name: "sign1-sign-0004", deterministic: true}, {name: "sign1-sign-0005", deterministic: true}, {name: "sign1-sign-0006", deterministic: true}, + {name: "sign1-sign-0007", deterministic: true}, // EdDSA Ed25519 {name: "sign1-verify-0000"}, {name: "sign1-verify-0001"}, {name: "sign1-verify-0002"}, @@ -78,10 +81,12 @@ var testCases = []struct { {name: "sign1-verify-0004"}, {name: "sign1-verify-0005"}, {name: "sign1-verify-0006"}, + {name: "sign1-verify-0007"}, // EdDSA Ed25519 {name: "sign1-verify-negative-0000", err: "cbor: invalid protected header: cbor: require bstr type"}, {name: "sign1-verify-negative-0001", err: "cbor: invalid protected header: cbor: protected header: require map type"}, {name: "sign1-verify-negative-0002", err: "cbor: invalid protected header: cbor: found duplicate map key \"1\" at map element index 1"}, {name: "sign1-verify-negative-0003", err: "cbor: invalid unprotected header: cbor: found duplicate map key \"4\" at map element index 1"}, + {name: "sign1-verify-negative-0004"}, // EdDSA Ed25519 invalid signature } func TestConformance(t *testing.T) { @@ -164,7 +169,13 @@ func testSign1(t *testing.T, tc *TestCase, deterministic bool) { if sig.External != "" { external = mustHexToBytes(sig.External) } - err = sigMsg.Sign(new(zeroSource), external, signer) + // Ed25519 signatures are deterministic and should use nil for rand + // Other algorithms use zeroSource for reproducible test results + var rand io.Reader = new(zeroSource) + if tc.Alg == "EdDSA" { + rand = nil + } + err = sigMsg.Sign(rand, external, signer) if err != nil { t.Fatal(err) } @@ -187,11 +198,44 @@ func testSign1(t *testing.T, tc *TestCase, deterministic bool) { } func getSigner(tc *TestCase, private bool) (cose.Signer, cose.Verifier, error) { + alg := mustNameToAlg(tc.Alg) + + // Special handling for OKP keys + if tc.Key["kty"] == "OKP" { + switch tc.Key["crv"] { + case "Ed25519": + // Note: Ed448 would require different key size validation (57 bytes) + verifierKey, privateKeySigner, err := createEd25519Key(tc.Key, private) + if err != nil { + return nil, nil, err + } + + var signer cose.Signer + if private { + signer, err = cose.NewSigner(alg, privateKeySigner) + if err != nil { + return nil, nil, err + } + } + + verifier, err := cose.NewVerifier(alg, verifierKey) + if err != nil { + return nil, nil, err + } + return signer, verifier, nil + case "Ed448": + // Ed448 support would go here - requires golang.org/x/crypto/ed448 + return nil, nil, errors.New("Ed448 not yet implemented") + default: + return nil, nil, errors.New("unsupported OKP curve: " + tc.Key["crv"]) + } + } + + // Original implementation for RSA and EC keys pkey, err := getKey(tc.Key, private) if err != nil { return nil, nil, err } - alg := mustNameToAlg(tc.Alg) signer, err := cose.NewSigner(alg, pkey) if err != nil { return nil, nil, err @@ -248,6 +292,26 @@ func getKey(key Key, private bool) (crypto.Signer, error) { pkey.D = mustBase64ToBigInt(key["d"]) } return pkey, nil + case "OKP": + // Only Ed25519 is supported for now + switch key["crv"] { + case "Ed25519": + _, privateKeySigner, err := createEd25519Key(key, private) + if err != nil { + return nil, err + } + if !private { + // For public key only operations, we need to return a type that satisfies crypto.Signer + // but this won't work for verify-only operations. We'll handle this in getSigner. + return nil, errors.New("OKP public-only key not supported in this context") + } + return privateKeySigner, nil + case "Ed448": + // Ed448 support would require golang.org/x/crypto/ed448 + return nil, errors.New("Ed448 not yet implemented") + default: + return nil, errors.New("unsupported OKP curve: " + key["crv"]) + } } return nil, errors.New("unsupported key type: " + key["kty"]) } @@ -297,6 +361,35 @@ func mustBase64ToBigInt(s string) *big.Int { return new(big.Int).SetBytes(val) } +func mustBase64ToBytes(s string) []byte { + val, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + panic(err) + } + return val +} + +// createEd25519Key creates an Ed25519 key from test case data +func createEd25519Key(key Key, private bool) (ed25519.PublicKey, crypto.Signer, error) { + publicKey := mustBase64ToBytes(key["x"]) + if len(publicKey) != ed25519.PublicKeySize { + return nil, nil, errors.New("invalid Ed25519 public key size") + } + + if !private { + return ed25519.PublicKey(publicKey), nil, nil + } + + privateKey := mustBase64ToBytes(key["d"]) + if len(privateKey) != ed25519.SeedSize { + return nil, nil, errors.New("invalid Ed25519 private key size") + } + + fullPrivateKey := ed25519.NewKeyFromSeed(privateKey) + publicKeyFromPrivate := fullPrivateKey.Public().(ed25519.PublicKey) + return publicKeyFromPrivate, fullPrivateKey, nil +} + // mustNameToAlg returns the algorithm associated to name. // The content of name is not defined in any RFC, // but it's what the test cases use to identify algorithms. @@ -320,6 +413,8 @@ func mustNameToAlg(name string) cose.Algorithm { return cose.AlgorithmES384 case "ES512": return cose.AlgorithmES512 + case "EdDSA": + return cose.AlgorithmEdDSA } panic("algorithm name not found: " + name) } diff --git a/testdata/sign1-sign-0007.json b/testdata/sign1-sign-0007.json new file mode 100644 index 0000000..d57a97e --- /dev/null +++ b/testdata/sign1-sign-0007.json @@ -0,0 +1,34 @@ +{ + "uuid": "ED25519-SIGN-0001", + "title": "Sign1 - EdDSA with Ed25519 (sign)", + "description": "Sign with one signer using EdDSA with Ed25519", + "key": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + "d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A" + }, + "alg": "EdDSA", + "sign1::sign": { + "payload": "546869732069732074686520636f6e74656e742e", + "protectedHeaders": { + "cborHex": "a201270300", + "cborDiag": "{1: -8, 3: 0}" + }, + "unprotectedHeaders": { + "cborHex": "a104423131", + "cborDiag": "{4: '11'}" + }, + "tbsHex": { + "cborHex": "846a5369676e61747572653145a20127030040545468697320697320746865206f6e74656e742e", + "cborDiag": "[\"Signature1\", h'A201270300', h'', h'546869732069732074686520636F6E74656E742E']" + }, + "external": "", + "detached": false, + "expectedOutput": { + "cborHex": "d28445a201270300a10442313154546869732069732074686520636f6e74656e742e58407142fd2ff96d56db85bee905a76ba1d0b7321a95c8c4d3607c5781932b7afb8711497dfa751bf40b58b3bcc32300b1487f3db34085eef013bf08f4a44d6fef0d", + "cborDiag": "18([h'A201270300', {4: h'3131'}, h'546869732069732074686520636F6E74656E742E', h'7142FD2FF96D56DB85BEE905A76BA1D0B7321A95C8C4D3607C5781932B7AFB8711497DFA751BF40B58B3BCC32300B1487F3DB34085EEF013BF08F4A44D6FEF0D'])" + }, + "fixedOutputLength": 32 + } +} diff --git a/testdata/sign1-verify-0007.json b/testdata/sign1-verify-0007.json new file mode 100644 index 0000000..51243fc --- /dev/null +++ b/testdata/sign1-verify-0007.json @@ -0,0 +1,19 @@ +{ + "uuid": "ED25519-VERIFY-0001", + "title": "Sign1 - EdDSA with Ed25519 (verify)", + "description": "Verify signature with one signer using EdDSA with Ed25519", + "key": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + }, + "alg": "EdDSA", + "sign1::verify": { + "taggedCOSESign1": { + "cborHex": "d28445a201270300a10442313154546869732069732074686520636f6e74656e742e58407142fd2ff96d56db85bee905a76ba1d0b7321a95c8c4d3607c5781932b7afb8711497dfa751bf40b58b3bcc32300b1487f3db34085eef013bf08f4a44d6fef0d", + "cborDiag": "18([h'A201270300', {4: h'3131'}, h'546869732069732074686520636F6E74656E742E', h'7142FD2FF96D56DB85BEE905A76BA1D0B7321A95C8C4D3607C5781932B7AFB8711497DFA751BF40B58B3BCC32300B1487F3DB34085EEF013BF08F4A44D6FEF0D'])" + }, + "external": "", + "shouldVerify": true + } +} diff --git a/testdata/sign1-verify-negative-0004.json b/testdata/sign1-verify-negative-0004.json new file mode 100644 index 0000000..3d4e37c --- /dev/null +++ b/testdata/sign1-verify-negative-0004.json @@ -0,0 +1,19 @@ +{ + "uuid": "ED25519-VERIFY-NEGATIVE-0001", + "title": "Sign1 - EdDSA with Ed25519 (verify - invalid signature)", + "description": "Verification should fail with invalid Ed25519 signature", + "key": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + }, + "alg": "EdDSA", + "sign1::verify": { + "taggedCOSESign1": { + "cborHex": "d28445a201270300a104423131545468697320697320746865206f6e74656e742e5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "cborDiag": "18([h'A201270300', {4: h'3131'}, h'546869732069732074686520636F6E74656E742E', h'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'])" + }, + "external": "", + "shouldVerify": false + } +}