We're using Relayer (Defender 2) to sign authentication messages to guard access to a certain function. According to https://docs.openzeppelin.com/defender/v2/manage/relayers#signing it creates non-deterministic signatures. We can confirm that: with every client call we receive different signature bytes but on the frontend they all are recoverable to the correct relay address (0xd7B298c9fB0377124d01D4E826d9D5beFB7CD6FE
) using viem's recoverMessageAddress
and recoverAddress
. When trying to verify them using the OZ ECDSA contract library, we however cannot recover the relay / signer address correctly. Using the latest version of @openzeppelin/defender-sdk
/ 1.11.0, using Cancun VM.
Client:
const { Defender } = require('@openzeppelin/defender-sdk')
const { hashMessage, recoverMessageAddress, recoverAddress } = require('viem')
require('dotenv-flow').config({
default_node_env: 'development'
})
const main = async () => {
const creds = {
relayerApiKey: process.env.DEFENDER_RELAY_API_KEY,
relayerApiSecret: process.env.DEFENDER_RELAY_SECRET_KEY
}
const client = new Defender(creds)
const validUntil = new Date(Date.now() + 120 * 1000).toISOString()
const signer = await client.relaySigner.getSigner(
client.relaySigner.getProvider(),
{ speed: 'fast', validUntil }
)
const plainMessage = 'hello world'
const signature = await signer.signMessage(plainMessage)
console.log('signature', signature)
const address = await recoverMessageAddress({
message: plainMessage,
signature: signature
})
console.log('recovered address 1', address)
console.log(
'recovered address 2',
await recoverAddress({
hash: hashMessage(plainMessage),
signature
})
)
}
;(async () => {
await main()
})()
Output:
$ yarn ts-node cli/testRelayer.ts
signature 0x5d52923f24f9edb9e0ac19160183c0731b77795b7c4acaa0e81587aa3e64e6e710c0df2567f3396e5be7dd3816b2de9d69270cfb7d34e58c8b5d20711a19498f1b
recovered address 1 0xd7B298c9fB0377124d01D4E826d9D5beFB7CD6FE
recovered address 2 0xd7B298c9fB0377124d01D4E826d9D5beFB7CD6FE
$ yarn ts-node cli/testRelayer.ts
signature 0x5e0c589c5790b134223a866bd253ff73ad6a00482d96234d3b72c0cb64eccbc63cede8769090ed91d62983af41bbd2d200359469b2d77bd0096587a297554cef1b
recovered address 1 0xd7B298c9fB0377124d01D4E826d9D5beFB7CD6FE
recovered address 2 0xd7B298c9fB0377124d01D4E826d9D5beFB7CD6FE
$ yarn ts-node cli/testRelayer.ts
signature 0x36efe70274ca04d2590bfebfe63e2c0099a60c51009bfcfdf63e9734d30de91e3fd861722c7f0d1e727d01fc9eb1e50663ba879c61b08a7ee07ba51f7b635b9d1c
recovered address 1 0xd7B298c9fB0377124d01D4E826d9D5beFB7CD6FE
recovered address 2 0xd7B298c9fB0377124d01D4E826d9D5beFB7CD6FE
Contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
import {SignatureChecker} from "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/utils/cryptography/SignatureChecker.sol";
import {ECDSA} from "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/utils/cryptography/MessageHashUtils.sol";
contract SignedMintAuthorizer {
function authorizeMint(string memory authMessage, bytes memory signature) external pure returns (address) {
bytes32 signedHash = MessageHashUtils.toEthSignedMessageHash(keccak256(bytes(authMessage)));
(address signer,ECDSA.RecoverError err,bytes32 any) = ECDSA.tryRecover(signedHash, signature);
return signer;
}
}
Call Results:
{
"string authMessage": "hello world",
"bytes signature": "0x5e0c589c5790b134223a866bd253ff73ad6a00482d96234d3b72c0cb64eccbc63cede8769090ed91d62983af41bbd2d200359469b2d77bd0096587a297554cef1b"
}
->
{
"0": "address: 0xC548fB656F3CbB7cCd8FAe6335B30883d1EB4008"
}
---
{
"string authMessage": "hello world",
"bytes signature": "0x36efe70274ca04d2590bfebfe63e2c0099a60c51009bfcfdf63e9734d30de91e3fd861722c7f0d1e727d01fc9eb1e50663ba879c61b08a7ee07ba51f7b635b9d1c"
}
->
{
"0": "address: 0x00FB5b0EEE636D748C8B23CdD23C4b54348f0135"
}