Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(WIP) Add pre-compile: verification for ed25519 sig made via GPG #2

Open
wants to merge 11 commits into
base: optimism
Choose a base branch
from
94 changes: 94 additions & 0 deletions core/vm/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ package vm
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"maps"
"math/big"

pgpcrypto "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/consensys/gnark-crypto/ecc"
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
"github.com/consensys/gnark-crypto/ecc/bls12-381/fp"
Expand Down Expand Up @@ -114,6 +116,7 @@ var PrecompiledContractsCancun = PrecompiledContracts{
common.BytesToAddress([]byte{0x8}): &bn256PairingIstanbul{},
common.BytesToAddress([]byte{0x9}): &blake2F{},
common.BytesToAddress([]byte{0xa}): &kzgPointEvaluation{},
common.BytesToAddress([]byte{0xed}): &gpgEd25519Verify{},
}

// PrecompiledContractsPrague contains the set of pre-compiled Ethereum
Expand All @@ -138,6 +141,7 @@ var PrecompiledContractsPrague = PrecompiledContracts{
common.BytesToAddress([]byte{0x11}): &bls12381Pairing{},
common.BytesToAddress([]byte{0x12}): &bls12381MapG1{},
common.BytesToAddress([]byte{0x13}): &bls12381MapG2{},
common.BytesToAddress([]byte{0xed}): &gpgEd25519Verify{},
}

var PrecompiledContractsBLS = PrecompiledContractsPrague
Expand Down Expand Up @@ -173,6 +177,7 @@ var PrecompiledContractsGranite = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{8}): &bn256PairingGranite{},
common.BytesToAddress([]byte{9}): &blake2F{},
common.BytesToAddress([]byte{0x0a}): &kzgPointEvaluation{},
common.BytesToAddress([]byte{0xed}): &gpgEd25519Verify{},
common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{},
}

Expand Down Expand Up @@ -1353,3 +1358,92 @@ func (c *p256Verify) Run(input []byte) ([]byte, error) {
return nil, nil
}
}

// gpgEd25519Verify implements native verification for ed25519 signatures produced via gpg
type gpgEd25519Verify struct{}

var (
errMessageTooShort = errors.New("message too short")
errPubKeyTooShort = errors.New("public key too short")
errSignatureTooShort = errors.New("signature too short")
errInvalidPublicKey = errors.New("invalid public key format")
errInvalidSignature = errors.New("invalid signature format")
errVerificationFailed = errors.New("signature verification failed")
)

// RequiredGas returns the gas required to execute the pre-compiled contract
func (c *gpgEd25519Verify) RequiredGas(input []byte) uint64 {
// You can adjust this value based on your needs
return params.GpgEd25519VerifyGas
}

// Run performs ed25519 signature verification
// Q_TODO: Enforce max message size?
func (c *gpgEd25519Verify) Run(input []byte) ([]byte, error) {
// Input should be: message_len (32 bytes) || message || pubkey_len (32 bytes) || pubkey || sig_len (32 bytes) || signature
if len(input) < 96 { // minimum length for the three length fields
return nil, errMessageTooShort
}

// Extract message length and message
msgLen := new(big.Int).SetBytes(input[:32]).Uint64()
if len(input) < 32+int(msgLen) {
return nil, errMessageTooShort
}
message := input[32 : 32+msgLen]

// Extract public key length and public key
offset := 32 + msgLen
pubKeyLen := new(big.Int).SetBytes(input[offset : offset+32]).Uint64()
if len(input) < int(offset+32+pubKeyLen) {
return nil, errPubKeyTooShort
}
pubKey := input[offset+32 : offset+32+pubKeyLen]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific length the pubKey is supposed to be that we should verify?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the comment on signature length. The armored public key length is non-deterministic (exported via gpg --export --armor <your-key-id>).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, no way to do this. I'm going to leave this comment open as a reminder that I need to talk to the Tea team to figure out a reasonable upper bound to make sure that (a) there is sufficient gas to handle it, and (b) that there is no DOS risk based on whatever gas params we choose.


// Extract signature length and signature
offset = offset + 32 + pubKeyLen
sigLen := new(big.Int).SetBytes(input[offset : offset+32]).Uint64()
if len(input) < int(offset+32+sigLen) {
zobront marked this conversation as resolved.
Show resolved Hide resolved
return nil, errSignatureTooShort
}
signature := input[offset+32 : offset+32+sigLen]

// Convert raw bytes to armored format
armoredPubKey := string(pubKey)
armoredSig := string(signature)

// Create public key object
pubKeyObj, err := pgpcrypto.NewKeyFromArmored(armoredPubKey)
if err != nil {
return nil, errInvalidPublicKey
}

// Create public keyring
pubKeyRing, err := pgpcrypto.NewKeyRing(pubKeyObj)
if err != nil {
return nil, errInvalidPublicKey
}

// Parse the armored signature
signatureObj, err := pgpcrypto.NewPGPSignatureFromArmored(armoredSig)
if err != nil {
return nil, errInvalidSignature
}

// Create message object
messageHex := hex.EncodeToString(message)
messageBytes, err := hex.DecodeString(messageHex)
if err != nil {
return nil, err
}
messageObj := pgpcrypto.NewPlainMessage(messageBytes)

// Verify signature
err = pubKeyRing.VerifyDetached(messageObj, signatureObj, pgpcrypto.GetUnixTime())
zobront marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, errVerificationFailed
zobront marked this conversation as resolved.
Show resolved Hide resolved
}

// Return 32 bytes: 1 for success, 0 for failure
return common.LeftPadBytes([]byte{1}, 32), nil
}
48 changes: 48 additions & 0 deletions core/vm/contracts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ var allPrecompiles = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{8}): &bn256PairingGranite{},
common.BytesToAddress([]byte{9}): &blake2F{},
common.BytesToAddress([]byte{0x0a}): &kzgPointEvaluation{},
common.BytesToAddress([]byte{0xed}): &gpgEd25519Verify{},

common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{},

Expand Down Expand Up @@ -421,3 +422,50 @@ func BenchmarkPrecompiledP256Verify(bench *testing.B) {
}

func TestPrecompiledP256Verify(t *testing.T) { testJson("p256Verify", "100", t) }

// Tests the GPG Ed25519 signature verification. Input format: abi.encodePacked(message.len, message, pubKey.len, pubKey, sig.len, sig)
func TestPrecompiledGpgEd25519Verify(t *testing.T) { testJson("gpgEd25519Verify", "ed", t) }

func BenchmarkPrecompiledGpgEd25519Verify(b *testing.B) {
t := precompiledTest{
Input: "000000000000000000000000000000000000000000000000000000000000000c48656c6c6f2c20576f726c6400000000000000000000000000000000000000000000000000000000000002932d2d2d2d2d424547494e20504750205055424c4943204b455920424c4f434b2d2d2d2d2d0a0a6d444d455a3270644468594a4b7759424241486152773842415164415852437247514d50696a3763724f453944685a6a5a394b563865455537346649387743630a32704d61447575304b30747961584e6f5957356e494535685a4764686457526849447872636d6c7a614746755a7935756233526c5147647459576c734c6d4e760a625436496b77515446676f414f785968424d4443334e69684230626b66485778675a5a326376684652454f3442514a6e616c304f416873444251734a434163430a416949434268554b4351674c4167515741674d42416834484168654141416f4a454a5a326376684652454f34496b49412f33584556616c50354d6775624646760a556a72734764516f562f4636644f48485143514256412b6531777764415034714c6b342f57684e67684c7931716c396f364a6c6164622b4e4370504d416b554a0a3542566b51374e51424c6734424764715851345343697347415151426c31554242514542423041564530447175367235436e336168574b3449585174426f30610a51576764666855753737397a4243796a4c674d42434165496541515946676f4149425968424d4443334e69684230626b66485778675a5a326376684652454f340a42514a6e616c304f4168734d41416f4a454a5a326376684652454f343255674241503268773168454c6856574576344b3931667937726c50366d585a2b5133610a705875724e3267346b4d47664150774a7a323448736a6a3445324874756377526e3868327556396f7167416467776a565059382f6d647a3841673d3d0a3d3367346b0a2d2d2d2d2d454e4420504750205055424c4943204b455920424c4f434b2d2d2d2d2d00000000000000000000000000000000000000000000000000000000000000e32d2d2d2d2d424547494e20504750205349474e41545552452d2d2d2d2d0a0a694855454142594b414230574951544177747a596f5164473548783173594757646e4c3452555244754155435a3271616b41414b43524357646e4c34525552440a7545304841503942314d67646c31364a5463324647554f4e67455a6c746d783439694a6c4a773979756145497551747746414541374636745a7a72505a37366f0a796d705439354366484e327964794d734870424855513270446b4a4f4a67383d0a3d2f692f4d0a2d2d2d2d2d454e4420504750205349474e41545552452d2d2d2d2d",
Expected: "0000000000000000000000000000000000000000000000000000000000000001",
Name: "verify_valid_gpg_ed25519",
}
benchmarkPrecompiled("ed", t, b)
}

// Tests GPG Ed25519 verification with malformed inputs
var gpgEd25519MalformedInputTests = []precompiledFailureTest{
{
Input: "",
ExpectedError: "message too short",
Name: "empty input",
},
{
Input: "000000000000000000000000000000000000000000000000000000000000000c48656c6c6f2c20576f726c6400000000000000000000000000000000000000000000000000000000000002932d2d2d2d2d424547494e20504750205055424c",
ExpectedError: "message too short",
Name: "input shorter than 96 bytes",
},
{
Input: "000000000000000000000000000000000000000000000000000000000000000c48656c6c6f2c20576f726c6400000000000000000000000000000000000000000000000000000000000002942d2d2d2d2d424547494e20504750205055424c4943204b455920424c4f434b2d2d2d2d2d0a700a6d444d455a3270644468594a4b7759424241486152773842415164415852437247514d50696a3763724f453944685a6a5a394b563865455537346649387743630a32704d61447575304b30747961584e6f5957356e494535685a4764686457526849447872636d6c7a614746755a7935756233526c5147647459576c734c6d4e760a625436496b77515446676f414f785968424d4443334e69684230626b66485778675a5a326376684652454f3442514a6e616c304f416873444251734a434163430a416949434268554b4351674c4167515741674d42416834484168654141416f4a454a5a326376684652454f34496b49412f33584556616c50354d6775624646760a556a72734764516f562f4636644f48485143514256412b6531777764415034714c6b342f57684e67684c7931716c396f364a6c6164622b4e4370504d416b554a0a3542566b51374e51424c6734424764715851345343697347415151426c31554242514542423041564530447175367235436e336168574b3449585174426f30610a51576764666855753737397a4243796a4c674d42434165496541515946676f4149425968424d4443334e69684230626b66485778675a5a326376684652454f340a42514a6e616c304f4168734d41416f4a454a5a326376684652454f343255674241503268773168454c6856574576344b3931667937726c50366d585a2b5133610a705875724e3267346b4d47664150774a7a323448736a6a3445324874756377526e3868327556396f7167416467776a565059382f6d647a3841673d3d0a3d3367346b0a2d2d2d2d2d454e4420504750205055424c4943204b455920424c4f434b2d2d2d2d2d00000000000000000000000000000000000000000000000000000000000000e32d2d2d2d2d424547494e20504750205349474e41545552452d2d2d2d2d0a0a694855454142594b414230574951544177747a596f5164473548783173594757646e4c3452555244754155435a3271616b41414b43524357646e4c34525552440a7545304841503942314d67646c31364a5463324647554f4e67455a6c746d783439694a6c4a773979756145497551747746414541374636745a7a72505a37366f0a796d705439354366484e327964794d734870424855513270446b4a4f4a67383d0a3d2f692f4d0a2d2d2d2d2d454e4420504750205349474e41545552452d2d2d2d2d",
ExpectedError: "invalid public key format",
Name: "invalid armored public key",
},
{
Input: "000000000000000000000000000000000000000000000000000000000000000c48656c6c6f2c20576f726c6400000000000000000000000000000000000000000000000000000000000002932d2d2d2d2d424547494e20504750205055424c4943204b455920424c4f434b2d2d2d2d2d0a0a6d444d455a3270644468594a4b7759424241486152773842415164415852437247514d50696a3763724f453944685a6a5a394b563865455537346649387743630a32704d61447575304b30747961584e6f5957356e494535685a4764686457526849447872636d6c7a614746755a7935756233526c5147647459576c734c6d4e760a625436496b77515446676f414f785968424d4443334e69684230626b66485778675a5a326376684652454f3442514a6e616c304f416873444251734a434163430a416949434268554b4351674c4167515741674d42416834484168654141416f4a454a5a326376684652454f34496b49412f33584556616c50354d6775624646760a556a72734764516f562f4636644f48485143514256412b6531777764415034714c6b342f57684e67684c7931716c396f364a6c6164622b4e4370504d416b554a0a3542566b51374e51424c6734424764715851345343697347415151426c31554242514542423041564530447175367235436e336168574b3449585174426f30610a51576764666855753737397a4243796a4c674d42434165496541515946676f4149425968424d4443334e69684230626b66485778675a5a326376684652454f340a42514a6e616c304f4168734d41416f4a454a5a326376684652454f343255674241503268773168454c6856574576344b3931667937726c50366d585a2b5133610a705875724e3267346b4d47664150774a7a323448736a6a3445324874756377526e3868327556396f7167416467776a565059382f6d647a3841673d3d0a3d3367346b0a2d2d2d2d2d454e4420504750205055424c4943204b455920424c4f434b2d2d2d2d2d00000000000000000000000000000000000000000000000000000000000000e42d2d2d2d2d424547494e20504750205349474e41545552452d2d2d2d2d0a700a694855454142594b414230574951544177747a596f5164473548783173594757646e4c3452555244754155435a3271616b41414b43524357646e4c34525552440a7545304841503942314d67646c31364a5463324647554f4e67455a6c746d783439694a6c4a773979756145497551747746414541374636745a7a72505a37366f0a796d705439354366484e327964794d734870424855513270446b4a4f4a67383d0a3d2f692f4d0a2d2d2d2d2d454e4420504750205349474e41545552452d2d2d2d2d",
ExpectedError: "invalid signature format",
Name: "invalid signature format",
},
{
Input: "000000000000000000000000000000000000000000000000000000000000000b48656c6c6f2c20576f726c00000000000000000000000000000000000000000000000000000000000002932d2d2d2d2d424547494e20504750205055424c4943204b455920424c4f434b2d2d2d2d2d0a0a6d444d455a3270644468594a4b7759424241486152773842415164415852437247514d50696a3763724f453944685a6a5a394b563865455537346649387743630a32704d61447575304b30747961584e6f5957356e494535685a4764686457526849447872636d6c7a614746755a7935756233526c5147647459576c734c6d4e760a625436496b77515446676f414f785968424d4443334e69684230626b66485778675a5a326376684652454f3442514a6e616c304f416873444251734a434163430a416949434268554b4351674c4167515741674d42416834484168654141416f4a454a5a326376684652454f34496b49412f33584556616c50354d6775624646760a556a72734764516f562f4636644f48485143514256412b6531777764415034714c6b342f57684e67684c7931716c396f364a6c6164622b4e4370504d416b554a0a3542566b51374e51424c6734424764715851345343697347415151426c31554242514542423041564530447175367235436e336168574b3449585174426f30610a51576764666855753737397a4243796a4c674d42434165496541515946676f4149425968424d4443334e69684230626b66485778675a5a326376684652454f340a42514a6e616c304f4168734d41416f4a454a5a326376684652454f343255674241503268773168454c6856574576344b3931667937726c50366d585a2b5133610a705875724e3267346b4d47664150774a7a323448736a6a3445324874756377526e3868327556396f7167416467776a565059382f6d647a3841673d3d0a3d3367346b0a2d2d2d2d2d454e4420504750205055424c4943204b455920424c4f434b2d2d2d2d2d00000000000000000000000000000000000000000000000000000000000000e32d2d2d2d2d424547494e20504750205349474e41545552452d2d2d2d2d0a0a694855454142594b414230574951544177747a596f5164473548783173594757646e4c3452555244754155435a3271616b41414b43524357646e4c34525552440a7545304841503942314d67646c31364a5463324647554f4e67455a6c746d783439694a6c4a773979756145497551747746414541374636745a7a72505a37366f0a796d705439354366484e327964794d734870424855513270446b4a4f4a67383d0a3d2f692f4d0a2d2d2d2d2d454e4420504750205349474e41545552452d2d2d2d2d",
ExpectedError: "signature verification failed",
Name: "verify incorrect signature",
},
}

func TestPrecompiledGpgEd25519VerifyMalformedInput(t *testing.T) {
for _, test := range gpgEd25519MalformedInputTests {
testPrecompiledFailure("ed", test, t)
}
}
9 changes: 9 additions & 0 deletions core/vm/testdata/precompiles/gpgEd25519Verify.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"Input": "000000000000000000000000000000000000000000000000000000000000000c48656c6c6f2c20576f726c6400000000000000000000000000000000000000000000000000000000000002932d2d2d2d2d424547494e20504750205055424c4943204b455920424c4f434b2d2d2d2d2d0a0a6d444d455a3270644468594a4b7759424241486152773842415164415852437247514d50696a3763724f453944685a6a5a394b563865455537346649387743630a32704d61447575304b30747961584e6f5957356e494535685a4764686457526849447872636d6c7a614746755a7935756233526c5147647459576c734c6d4e760a625436496b77515446676f414f785968424d4443334e69684230626b66485778675a5a326376684652454f3442514a6e616c304f416873444251734a434163430a416949434268554b4351674c4167515741674d42416834484168654141416f4a454a5a326376684652454f34496b49412f33584556616c50354d6775624646760a556a72734764516f562f4636644f48485143514256412b6531777764415034714c6b342f57684e67684c7931716c396f364a6c6164622b4e4370504d416b554a0a3542566b51374e51424c6734424764715851345343697347415151426c31554242514542423041564530447175367235436e336168574b3449585174426f30610a51576764666855753737397a4243796a4c674d42434165496541515946676f4149425968424d4443334e69684230626b66485778675a5a326376684652454f340a42514a6e616c304f4168734d41416f4a454a5a326376684652454f343255674241503268773168454c6856574576344b3931667937726c50366d585a2b5133610a705875724e3267346b4d47664150774a7a323448736a6a3445324874756377526e3868327556396f7167416467776a565059382f6d647a3841673d3d0a3d3367346b0a2d2d2d2d2d454e4420504750205055424c4943204b455920424c4f434b2d2d2d2d2d00000000000000000000000000000000000000000000000000000000000000e32d2d2d2d2d424547494e20504750205349474e41545552452d2d2d2d2d0a0a694855454142594b414230574951544177747a596f5164473548783173594757646e4c3452555244754155435a3271616b41414b43524357646e4c34525552440a7545304841503942314d67646c31364a5463324647554f4e67455a6c746d783439694a6c4a773979756145497551747746414541374636745a7a72505a37366f0a796d705439354366484e327964794d734870424855513270446b4a4f4a67383d0a3d2f692f4d0a2d2d2d2d2d454e4420504750205349474e41545552452d2d2d2d2d",
"Expected": "0000000000000000000000000000000000000000000000000000000000000001",
"Gas": 2000,
"Name": "verify_valid_gpg_ed25519",
"NoBenchmark": false
}
]
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ toolchain go1.22.7
require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0
github.com/Microsoft/go-winio v0.6.2
github.com/ProtonMail/gopenpgp/v2 v2.8.1
github.com/VictoriaMetrics/fastcache v1.12.2
github.com/aws/aws-sdk-go-v2 v1.21.2
github.com/aws/aws-sdk-go-v2/config v1.18.45
Expand Down Expand Up @@ -84,6 +85,8 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 // indirect
Expand All @@ -97,6 +100,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cockroachdb/errors v1.11.3 // indirect
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
Expand Down
Loading