From 272c810810da775eee6562557c5562df2bbb561d Mon Sep 17 00:00:00 2001 From: Moreno Ambrosin Date: Tue, 19 Nov 2024 14:01:07 -0800 Subject: [PATCH] Add full primitives for ED25519 This change adds exported `ed25519.New{Signer,Verifier}` functions which are kept internal using `internalapi.Token`. PiperOrigin-RevId: 698137108 Change-Id: I16edf1fb6bd586c6d98e52a4b620032675ce8c15 --- signature/ed25519/key_test.go | 7 +- signature/ed25519/signer.go | 61 ++++ signature/ed25519/signer_verifier_test.go | 336 ++++++++++++++++++++++ signature/ed25519/verifier.go | 67 +++++ 4 files changed, 467 insertions(+), 4 deletions(-) create mode 100644 signature/ed25519/signer.go create mode 100644 signature/ed25519/signer_verifier_test.go create mode 100644 signature/ed25519/verifier.go diff --git a/signature/ed25519/key_test.go b/signature/ed25519/key_test.go index 0dcb7c5..4ffb874 100644 --- a/signature/ed25519/key_test.go +++ b/signature/ed25519/key_test.go @@ -408,10 +408,9 @@ func TestPublicKeyKeyBytes(t *testing.T) { } const ( - // Taken from - // https://github.com/google/boringssl/blob/f10c1dc37174843c504a80e94c252e35b7b1eb61/crypto/evp/evp_tests.txt#L178 - privKeyHex = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60" - pubKeyHex = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" + // Taken from https://datatracker.ietf.org/doc/html/rfc8032#appendix-A - TEST 3. + privKeyHex = "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7" + pubKeyHex = "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025" ) var testCases = []struct { diff --git a/signature/ed25519/signer.go b/signature/ed25519/signer.go new file mode 100644 index 0000000..9058341 --- /dev/null +++ b/signature/ed25519/signer.go @@ -0,0 +1,61 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ed25519 + +import ( + "crypto/ed25519" + "fmt" + "slices" + + "github.com/tink-crypto/tink-go/v2/insecuresecretdataaccess" + "github.com/tink-crypto/tink-go/v2/internal/internalapi" + "github.com/tink-crypto/tink-go/v2/tink" +) + +// signer is an implementation of [tink.Signer] for ED25519. +type signer struct { + privateKey ed25519.PrivateKey + prefix []byte + variant Variant +} + +var _ tink.Signer = (*signer)(nil) + +// NewSigner creates a new [tink.Signer] for ED25519. +// +// This is an internal API. +func NewSigner(privateKey *PrivateKey, _ internalapi.Token) (tink.Signer, error) { + return &signer{ + privateKey: ed25519.NewKeyFromSeed(privateKey.PrivateKeyBytes().Data(insecuresecretdataaccess.Token{})), + prefix: privateKey.OutputPrefix(), + variant: privateKey.publicKey.params.Variant(), + }, nil +} + +// Sign computes a signature for the given data. +// +// If the key has prefix, the signature will be prefixed with the output +// prefix. +func (e *signer) Sign(data []byte) ([]byte, error) { + messageToSign := data + if e.variant == VariantLegacy { + messageToSign = slices.Concat(data, []byte{0}) + } + r := ed25519.Sign(e.privateKey, messageToSign) + if len(r) != ed25519.SignatureSize { + return nil, fmt.Errorf("ed25519: invalid signature") + } + return slices.Concat(e.prefix, r), nil +} diff --git a/signature/ed25519/signer_verifier_test.go b/signature/ed25519/signer_verifier_test.go new file mode 100644 index 0000000..7441195 --- /dev/null +++ b/signature/ed25519/signer_verifier_test.go @@ -0,0 +1,336 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ed25519_test + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "encoding/hex" + "fmt" + "slices" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tink-crypto/tink-go/v2/core/cryptofmt" + "github.com/tink-crypto/tink-go/v2/insecuresecretdataaccess" + "github.com/tink-crypto/tink-go/v2/internal/internalapi" + "github.com/tink-crypto/tink-go/v2/secretdata" + tinked25519 "github.com/tink-crypto/tink-go/v2/signature/ed25519" + "github.com/tink-crypto/tink-go/v2/subtle/random" + "github.com/tink-crypto/tink-go/v2/testutil" +) + +func TestSignVerifyCorrectness(t *testing.T) { + // Taken from https://datatracker.ietf.org/doc/html/rfc8032#section-7.1 - TEST 3. + message := []byte{0xaf, 0x82} + signatureHex := "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a" + signatureLegacyHex := "afeae7a4fcd7d710a03353dfbe11a9906c6918633bb4dfef655d62d21f7535a1108ea3ef5bef2b0d0acefbf0e051f62ee2582652ae769df983ad1b11a95d3a08" + wantSignature, err := hex.DecodeString(signatureHex) + if err != nil { + t.Fatalf("hex.DecodeString(%q) err = %v, want nil", signatureHex, err) + } + wantLegacySignature, err := hex.DecodeString(signatureLegacyHex) + if err != nil { + t.Fatalf("hex.DecodeString(%q) err = %v, want nil", signatureHex, err) + } + tinkPrefix := []byte{cryptofmt.TinkStartByte, 0x01, 0x02, 0x03, 0x04} + crunchyAndLefacyPrefix := []byte{cryptofmt.LegacyStartByte, 0x01, 0x02, 0x03, 0x04} + for _, tc := range []struct { + name string + variant tinked25519.Variant + idRequirement uint32 + signature []byte + }{ + + { + name: "TINK", + variant: tinked25519.VariantTink, + idRequirement: uint32(0x01020304), + signature: slices.Concat(tinkPrefix, wantSignature), + }, + { + name: "CRUNCHY", + variant: tinked25519.VariantCrunchy, + idRequirement: uint32(0x01020304), + signature: slices.Concat(crunchyAndLefacyPrefix, wantSignature), + }, + { + name: "RAW", + variant: tinked25519.VariantNoPrefix, + idRequirement: uint32(0), + signature: wantSignature, + }, + { + name: "LEGACY", + variant: tinked25519.VariantLegacy, + idRequirement: uint32(0x01020304), + signature: slices.Concat(crunchyAndLefacyPrefix, wantLegacySignature), + }, + } { + t.Run(tc.name, func(t *testing.T) { + params, err := tinked25519.NewParameters(tc.variant) + if err != nil { + t.Fatalf("tinked25519.NewParameters(%v) err = %v, want nil", tc.variant, err) + } + publicKeyBytes, privateKeyBytes := getTestKeyPair(t) + publicKey, err := tinked25519.NewPublicKey(publicKeyBytes, tc.idRequirement, params) + if err != nil { + t.Fatalf("tinked25519.NewPublicKey(%v, %v, %v) err = %v, want nil", publicKeyBytes, tc.idRequirement, params, err) + } + privateKey, err := tinked25519.NewPrivateKey(secretdata.NewBytesFromData(privateKeyBytes, insecuresecretdataaccess.Token{}), tc.idRequirement, params) + if err != nil { + t.Fatalf("tinked25519.NewPrivateKey(%v, %v, %v) err = %v, want nil", privateKeyBytes, tc.idRequirement, params, err) + } + + // Sign. + signer, err := tinked25519.NewSigner(privateKey, internalapi.Token{}) + if err != nil { + t.Fatalf("tinked25519.NewSigner(%v, internalapi.Token{}) err = %v, want nil", privateKey, err) + } + gotSignature, err := signer.Sign(message) + if err != nil { + t.Fatalf("signer.Sign(%x) err = %v, want nil", message, err) + } + if diff := cmp.Diff(gotSignature, tc.signature); diff != "" { + t.Errorf("signer.Sign() returned unexpected diff (-want +got):\n%s", diff) + } + + // Verify. + verifier, err := tinked25519.NewVerifier(publicKey, internalapi.Token{}) + if err != nil { + t.Fatalf("tinked25519.NewVerifier(%v, internalapi.Token{}) err = %v, want nil", publicKey, err) + } + if err := verifier.Verify(tc.signature, message); err != nil { + t.Errorf("verifier.Verify(%x, %x) err = %v, want nil", tc.signature, message, err) + } + }) + } +} + +func TestVerifyFails(t *testing.T) { + for _, tc := range []struct { + name string + variant tinked25519.Variant + }{ + { + name: "TINK", + variant: tinked25519.VariantTink, + }, + { + name: "CRUNCHY", + variant: tinked25519.VariantCrunchy, + }, + { + name: "LEGACY", + variant: tinked25519.VariantLegacy, + }, + { + name: "RAW", + variant: tinked25519.VariantNoPrefix, + }, + } { + t.Run(tc.name, func(t *testing.T) { + public, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("key generation error: %s", err) + } + publicKey, privateKey := keyPair(t, public, priv, tc.variant) + signer, err := tinked25519.NewSigner(privateKey, internalapi.Token{}) + if err != nil { + t.Fatalf("tinked25519.NewSigner(%v, internalapi.Token{}) err = %v, want nil", privateKey, err) + } + verifier, err := tinked25519.NewVerifier(publicKey, internalapi.Token{}) + if err != nil { + t.Fatalf("tinked25519.NewVerifier(%v, internalapi.Token{}) err = %v, want nil", publicKey, err) + } + data := random.GetRandomBytes(20) + signature, err := signer.Sign(data) + if err != nil { + t.Fatalf("signer.Sign(%x) err = %v, want nil", data, err) + } + + prefix := signature[:len(publicKey.OutputPrefix())] + rawSignature := signature[len(publicKey.OutputPrefix()):] + + // Modify the prefix. + for i := 0; i < len(prefix); i++ { + modifiedPrefix := slices.Clone(prefix) + for j := 0; j < 8; j++ { + modifiedPrefix[i] = byte(modifiedPrefix[i] ^ (1 << uint32(j))) + s := slices.Concat(modifiedPrefix, rawSignature) + if err := verifier.Verify(s, data); err == nil { + t.Errorf("verifier.Verify(%x, data) err = nil, want error", s) + } + } + } + // Modify the signature. + for i := 0; i < len(rawSignature); i++ { + modifiedRawSignature := slices.Clone(rawSignature) + for j := 0; j < 8; j++ { + modifiedRawSignature[i] = byte(modifiedRawSignature[i] ^ (1 << uint32(j))) + s := slices.Concat(prefix, modifiedRawSignature) + if err := verifier.Verify(s, data); err == nil { + t.Errorf("verifier.Verify(%x, data) err = nil, want error", s) + } + } + } + // Modify the message. + for i := 0; i < len(data); i++ { + modifiedData := slices.Clone(data) + for j := 0; j < 8; j++ { + modifiedData[i] = byte(modifiedData[i] ^ (1 << uint32(j))) + if err := verifier.Verify(signature, modifiedData); err == nil { + t.Errorf("verifier.Verify(signature, %x) err = nil, want error", modifiedData) + } + } + } + }) + } +} + +func TestSignVerify(t *testing.T) { + public, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("key generation error: %s", err) + } + publicKey, privateKey := keyPair(t, public, priv, tinked25519.VariantNoPrefix) + signer, err := tinked25519.NewSigner(privateKey, internalapi.Token{}) + if err != nil { + t.Fatalf("tinked25519.NewSigner(%v, internalapi.Token{}) err = %v, want nil", privateKey, err) + } + verifier, err := tinked25519.NewVerifier(publicKey, internalapi.Token{}) + if err != nil { + t.Fatalf("tinked25519.NewVerifier(%v, internalapi.Token{}) err = %v, want nil", publicKey, err) + } + for i := 0; i < 100; i++ { + data := random.GetRandomBytes(20) + signatureBytes, err := signer.Sign(data) + if err != nil { + t.Fatalf("signer.Sign(%x) err = %v, want nil", data, err) + } + if err := verifier.Verify(signatureBytes, data); err != nil { + t.Errorf("verifier.Verify(%x, %x) err = %v, want nil", signatureBytes, data, err) + } + } +} + +type ed25519Suite struct { + testutil.WycheproofSuite + TestGroups []*ed25519Group `json:"testGroups"` +} + +type ed25519Group struct { + testutil.WycheproofGroup + KeyDER string `json:"keyDer"` + KeyPEM string `json:"keyPem"` + SHA string `json:"sha"` + Type string `json:"type"` + Key *ed25519TestKey `json:"key"` + Tests []*ed25519Case `json:"tests"` +} + +type ed25519Case struct { + testutil.WycheproofCase + Message testutil.HexBytes `json:"msg"` + Signature testutil.HexBytes `json:"sig"` +} + +type ed25519TestKey struct { + SK testutil.HexBytes `json:"sk"` + PK testutil.HexBytes `json:"pk"` +} + +func TestWycheproof(t *testing.T) { + suite := new(ed25519Suite) + if err := testutil.PopulateSuite(suite, "eddsa_test.json"); err != nil { + t.Fatalf("failed populating suite: %s", err) + } + for _, group := range suite.TestGroups { + private := ed25519.PrivateKey(group.Key.SK) + public := ed25519.PrivateKey(group.Key.PK) + + publicKey, privateKey := keyPair(t, public, private, tinked25519.VariantNoPrefix) + signer, err := tinked25519.NewSigner(privateKey, internalapi.Token{}) + if err != nil { + continue + } + verifier, err := tinked25519.NewVerifier(publicKey, internalapi.Token{}) + if err != nil { + continue + } + for _, test := range group.Tests { + caseName := fmt.Sprintf("Sign-%s-%s:Case-%d", suite.Algorithm, group.Type, test.CaseID) + t.Run(caseName, func(t *testing.T) { + got, err := signer.Sign(test.Message) + switch test.Result { + case "valid": + if err != nil { + t.Fatalf("ED25519Signer.Sign() failed in a valid test case: %s", err) + } + if !bytes.Equal(got, test.Signature) { + // Ed25519 is deterministic. + // Getting an alternative signature may leak the private key. + // This is especially the case if an attacker can also learn the valid signature. + t.Fatalf("ED25519Signer.Sign() = 0x%x, want = 0x%x", got, test.Signature) + } + case "invalid": + if err == nil && bytes.Equal(got, test.Signature) { + t.Fatalf("ED25519Signer.Sign() produced a matching signature in an invalid test case.") + } + default: + t.Fatalf("unrecognized result: %q", test.Result) + } + }) + + caseName = fmt.Sprintf("Verify-%s-%s:Case-%d", suite.Algorithm, group.Type, test.CaseID) + t.Run(caseName, func(t *testing.T) { + err := verifier.Verify(test.Signature, test.Message) + switch test.Result { + case "valid": + if err != nil { + t.Fatalf("ED25519Verifier.Verify() failed in a valid test case: %v", err) + } + case "invalid": + if err == nil { + t.Fatal("ED25519Verifier.Verify() succeeded in an invalid test case.") + } + default: + t.Fatalf("unsupported test result: %q", test.Result) + } + }) + } + } +} + +func keyPair(t *testing.T, public, priv []byte, variant tinked25519.Variant) (*tinked25519.PublicKey, *tinked25519.PrivateKey) { + params, err := tinked25519.NewParameters(variant) + if err != nil { + t.Fatalf("tinked25519.NewParameters(%v) err = %v, want nil", variant, err) + } + idRequirement := uint32(0x01020304) + if variant == tinked25519.VariantNoPrefix { + idRequirement = 0 + } + pubKey, err := tinked25519.NewPublicKey(public, idRequirement, params) + if err != nil { + t.Fatalf("tinked25519.NewPublicKey(%v, %v , %v) err = %v, want nil", public, idRequirement, params, err) + } + privKey, err := tinked25519.NewPrivateKey(secretdata.NewBytesFromData(priv[:32], insecuresecretdataaccess.Token{}), idRequirement, params) + if err != nil { + t.Fatalf("tinked25519.NewPrivateKey(%v, %v, %v) err = %v, want nil", priv[:32], idRequirement, params, err) + } + return pubKey, privKey +} diff --git a/signature/ed25519/verifier.go b/signature/ed25519/verifier.go new file mode 100644 index 0000000..7a60d5e --- /dev/null +++ b/signature/ed25519/verifier.go @@ -0,0 +1,67 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ed25519 + +import ( + "bytes" + "crypto/ed25519" + "fmt" + "slices" + + "github.com/tink-crypto/tink-go/v2/internal/internalapi" + "github.com/tink-crypto/tink-go/v2/tink" +) + +// verifier is an implementation of [tink.Verifier] for ED25519. +type verifier struct { + publicKey ed25519.PublicKey + prefix []byte + variant Variant +} + +var _ tink.Verifier = (*verifier)(nil) + +// NewVerifier creates a new [tink.Verifier] for ED25519. +// +// This is an internal API. +func NewVerifier(publicKey *PublicKey, _ internalapi.Token) (tink.Verifier, error) { + return &verifier{ + publicKey: publicKey.KeyBytes(), + variant: publicKey.params.Variant(), + prefix: publicKey.OutputPrefix(), + }, nil +} + +// Verify verifies whether the given signature is valid for the given data. +// +// It returns an error if the prefix is not valid or the signature is not +// valid. +func (e *verifier) Verify(signature, data []byte) error { + if !bytes.HasPrefix(signature, e.prefix) { + return fmt.Errorf("ed25519: the signature doesn't have the expected prefix") + } + signatureNoPrefix := signature[len(e.prefix):] + if len(signatureNoPrefix) != ed25519.SignatureSize { + return fmt.Errorf("ed25519: the length of the signature is not %d", ed25519.SignatureSize) + } + signedMessage := data + if e.variant == VariantLegacy { + signedMessage = slices.Concat(data, []byte{0}) + } + if !ed25519.Verify(e.publicKey, signedMessage, signatureNoPrefix) { + return fmt.Errorf("ed25519: invalid signature") + } + return nil +}