Cryptography

A collection of contracts and libraries that implement various signature validation schemes and cryptographic primitives. These utilities enable secure authentication, multisignature operations, and advanced cryptographic operations in smart contracts.

Utils

ERC7739

import "@openzeppelin/community-contracts/utils/cryptography/signers/ERC7739.sol";

Validates signatures wrapping the message hash in a nested EIP712 type. See ERC7739Utils.

Linking the signature to the EIP-712 domain separator is a security measure to prevent signature replay across different EIP-712 domains (e.g. a single offchain owner of multiple contracts).

This contract requires implementing the {_rawSignatureValidation} function, which passes the wrapped message hash, which may be either an typed data or a personal sign nested type.

EIP-712 uses ShortStrings to optimize gas costs for short strings (up to 31 characters). Consider that strings longer than that will use storage, which may limit the ability of the signer to be used within the ERC-4337 validation phase (due to ERC-7562 storage access rules).
Functions
  • isValidSignature(hash, signature)

EIP712
  • _domainSeparatorV4()

  • _hashTypedDataV4(structHash)

  • eip712Domain()

  • _EIP712Name()

  • _EIP712Version()

AbstractSigner
  • _rawSignatureValidation(hash, signature)

Events
IERC5267
  • EIP712DomainChanged()

isValidSignature(bytes32 hash, bytes signature) → bytes4 result public

Attempts validating the signature in a nested EIP-712 type.

A nested EIP-712 type might be presented in 2 different ways:

  • As a nested EIP-712 typed data

  • As a personal signature (an EIP-712 mimic of the eth_personalSign for a smart contract)

ERC7739Utils

import "@openzeppelin/community-contracts/utils/cryptography/ERC7739Utils.sol";

Utilities to process ERC-7739 typed data signatures that are specific to an EIP-712 domain.

This library provides methods to wrap, unwrap and operate over typed data signatures with a defensive rehashing mechanism that includes the application’s EIP-712 and preserves readability of the signed content using an EIP-712 nested approach.

A smart contract domain can validate a signature for a typed data structure in two ways:

A provider for a smart contract wallet would need to return this signature as the result of a call to personal_sign or eth_signTypedData, and this may be unsupported by API clients that expect a return value of 129 bytes, or specifically the r,s,v parameters of an ECDSA signature, as is for example specified for EIP-712.
Functions
  • encodeTypedDataSig(signature, appSeparator, contentsHash, contentsDescr)

  • decodeTypedDataSig(encodedSignature)

  • personalSignStructHash(contents)

  • typedDataSignStructHash(contentsName, contentsType, contentsHash, domainBytes)

  • typedDataSignStructHash(contentsDescr, contentsHash, domainBytes)

  • typedDataSignTypehash(contentsName, contentsType)

  • decodeContentsDescr(contentsDescr)

encodeTypedDataSig(bytes signature, bytes32 appSeparator, bytes32 contentsHash, string contentsDescr) → bytes internal

Nest a signature for a given EIP-712 type into a nested signature for the domain of the app.

Counterpart of decodeTypedDataSig to extract the original signature and the nested components.

decodeTypedDataSig(bytes encodedSignature) → bytes signature, bytes32 appSeparator, bytes32 contentsHash, string contentsDescr internal

Parses a nested signature into its components.

Constructed as follows:

signature ‖ APP_DOMAIN_SEPARATOR ‖ contentsHash ‖ contentsDescr ‖ uint16(contentsDescr.length)

  • signature is the signature for the (ERC-7739) nested struct hash. This signature indirectly signs over the original "contents" hash (from the app) and the account’s domain separator.

  • APP_DOMAIN_SEPARATOR is the EIP-712 {EIP712-_domainSeparatorV4} of the application smart contract that is requesting the signature verification (though ERC-1271).

  • contentsHash is the hash of the underlying data structure or message.

  • contentsDescr is a descriptor of the "contents" part of the the EIP-712 type of the nested signature.

This function returns empty if the input format is invalid instead of reverting. data instead.

personalSignStructHash(bytes32 contents) → bytes32 internal

Nests an ERC-191 digest into a PersonalSign EIP-712 struct, and returns the corresponding struct hash. This struct hash must be combined with a domain separator, using {MessageHashUtils-toTypedDataHash} before being verified/recovered.

This is used to simulates the personal_sign RPC method in the context of smart contracts.

typedDataSignStructHash(string contentsName, string contentsType, bytes32 contentsHash, bytes domainBytes) → bytes32 result internal

Nests an EIP-712 hash (contents) into a TypedDataSign EIP-712 struct, and returns the corresponding struct hash. This struct hash must be combined with a domain separator, using {MessageHashUtils-toTypedDataHash} before being verified/recovered.

typedDataSignStructHash(string contentsDescr, bytes32 contentsHash, bytes domainBytes) → bytes32 result internal

Variant of {typedDataSignStructHash-string-string-bytes32-bytes} that takes a content descriptor and decodes the contentsName and contentsType out of it.

typedDataSignTypehash(string contentsName, string contentsType) → bytes32 internal

Compute the EIP-712 typehash of the TypedDataSign structure for a given type (and typename).

decodeContentsDescr(string contentsDescr) → string contentsName, string contentsType internal

Parse the type name out of the ERC-7739 contents type description. Supports both the implicit and explicit modes.

Following ERC-7739 specifications, a contentsName is considered invalid if it’s empty or it contains any of the following bytes , )\x00

If the contentsType is invalid, this returns an empty string. Otherwise, the return string has non-zero length.

ERC7913Utils

import "@openzeppelin/community-contracts/utils/cryptography/ERC7913Utils.sol";

Library that provides common ERC-7913 utility functions.

This library extends the functionality of SignatureChecker to support signature verification for keys that do not have an Ethereum address of their own as with ERC-1271.

See ERC-7913.

Functions
  • isValidSignatureNow(signer, hash, signature)

  • areValidSignaturesNow(hash, signers, signatures)

isValidSignatureNow(bytes signer, bytes32 hash, bytes signature) → bool internal

Verifies a signature for a given signer and hash.

The signer is a bytes object that is the concatenation of an address and optionally a key: verifier || key. A signer must be at least 20 bytes long.

Verification is done as follows: - If signer.length < 20: verification fails - If signer.length == 20: verification is done using {SignatureChecker} - Otherwise: verification is done using IERC7913SignatureVerifier

areValidSignaturesNow(bytes32 hash, bytes[] signers, bytes[] signatures) → bool internal

Verifies multiple signatures for a given hash using a set of signers.

The signers must be ordered by their keccak256 hash to ensure no duplicates and to optimize the verification process. The function will return false if the signers are not properly ordered.

Requirements:

  • The signatures array must be at least the signers array’s length.

ZKEmailUtils

import "@openzeppelin/community-contracts/utils/cryptography/ZKEmailUtils.sol";

Library for ZKEmail signature validation utilities.

ZKEmail is a protocol that enables email-based authentication and authorization for smart contracts using zero-knowledge proofs. It allows users to prove ownership of an email address without revealing the email content or private keys.

The validation process involves several key components:

  • A DKIMRegistry (DomainKeys Identified Mail) verification mechanism to ensure the email was sent from a valid domain. Defined by an IDKIMRegistry interface.

  • A command template validation mechanism to ensure the email command matches the expected format and parameters.

  • A zero-knowledge proof verification mechanism to ensure the email was actually sent and received without revealing its contents. Defined by an IVerifier interface.

Functions
  • isValidZKEmail(emailAuthMsg, dkimregistry, verifier)

  • isValidZKEmail(emailAuthMsg, dkimregistry, verifier, template)

  • isValidZKEmail(emailAuthMsg, dkimregistry, verifier, template, stringCase)

isValidZKEmail(struct EmailAuthMsg emailAuthMsg, contract IDKIMRegistry dkimregistry, contract IVerifier verifier) → enum ZKEmailUtils.EmailProofError internal

Variant of isValidZKEmail that validates the ["signHash", "{uint}"] command template.

isValidZKEmail(struct EmailAuthMsg emailAuthMsg, contract IDKIMRegistry dkimregistry, contract IVerifier verifier, string[] template) → enum ZKEmailUtils.EmailProofError internal

Validates a ZKEmail authentication message.

This function takes an email authentication message, a DKIM registry contract, and a verifier contract as inputs. It performs several validation checks and returns a tuple containing a boolean success flag and an EmailProofError if validation failed. Returns {EmailProofError.NoError} if all validations pass, or false with a specific EmailProofError indicating which validation check failed.

Attempts to validate the command for all possible string Case values.

isValidZKEmail(struct EmailAuthMsg emailAuthMsg, contract IDKIMRegistry dkimregistry, contract IVerifier verifier, string[] template, enum ZKEmailUtils.Case stringCase) → enum ZKEmailUtils.EmailProofError internal

Variant of isValidZKEmail that validates a template with a specific string Case.

Useful for templates with Ethereum address matchers (i.e. {ethAddr}), which are case-sensitive (e.g., ["someCommand", "{address}"]).

Abstract Signers

AbstractSigner

import "@openzeppelin/community-contracts/utils/cryptography/signers/AbstractSigner.sol";

Abstract contract for signature validation.

Developers must implement _rawSignatureValidation and use it as the lowest-level signature validation mechanism.

Functions
  • _rawSignatureValidation(hash, signature)

_rawSignatureValidation(bytes32 hash, bytes signature) → bool internal

Signature validation algorithm.

Implementing a signature validation algorithm is a security-sensitive operation as it involves cryptographic verification. It is important to review and test thoroughly before deployment. Consider using one of the signature verification libraries (ECDSA, P256 or RSA).

SignerECDSA

import "@openzeppelin/community-contracts/utils/cryptography/signers/SignerECDSA.sol";

Implementation of AbstractSigner using ECDSA signatures.

For Account usage, a _setSigner function is provided to set the signer address. Doing so is easier for a factory, who is likely to use initializable clones of this contract.

Example of usage:

contract MyAccountECDSA is Account, SignerECDSA, Initializable {
    function initialize(address signerAddr) public initializer {
      _setSigner(signerAddr);
    }
}
Failing to call _setSigner either during construction (if used standalone) or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
Functions
  • _setSigner(signerAddr)

  • signer()

  • _rawSignatureValidation(hash, signature)

_setSigner(address signerAddr) internal

Sets the signer with the address of the native signer. This function should be called during construction or through an initializer.

signer() → address public

Return the signer’s address.

_rawSignatureValidation(bytes32 hash, bytes signature) → bool internal

Signature validation algorithm.

Implementing a signature validation algorithm is a security-sensitive operation as it involves cryptographic verification. It is important to review and test thoroughly before deployment. Consider using one of the signature verification libraries (ECDSA, P256 or RSA).

SignerP256

import "@openzeppelin/community-contracts/utils/cryptography/signers/SignerP256.sol";

Implementation of AbstractSigner using P256 signatures.

For Account usage, a _setSigner function is provided to set the signer public key. Doing so is easier for a factory, who is likely to use initializable clones of this contract.

Example of usage:

contract MyAccountP256 is Account, SignerP256, Initializable {
    function initialize(bytes32 qx, bytes32 qy) public initializer {
      _setSigner(qx, qy);
    }
}
Failing to call _setSigner either during construction (if used standalone) or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
Functions
  • _setSigner(qx, qy)

  • signer()

  • _rawSignatureValidation(hash, signature)

Errors
  • SignerP256InvalidPublicKey(qx, qy)

_setSigner(bytes32 qx, bytes32 qy) internal

Sets the signer with a P256 public key. This function should be called during construction or through an initializer.

signer() → bytes32 qx, bytes32 qy public

Return the signer’s P256 public key.

_rawSignatureValidation(bytes32 hash, bytes signature) → bool internal

Signature validation algorithm.

Implementing a signature validation algorithm is a security-sensitive operation as it involves cryptographic verification. It is important to review and test thoroughly before deployment. Consider using one of the signature verification libraries (ECDSA, P256 or RSA).

SignerP256InvalidPublicKey(bytes32 qx, bytes32 qy) error

SignerRSA

import "@openzeppelin/community-contracts/utils/cryptography/signers/SignerRSA.sol";

Implementation of AbstractSigner using RSA signatures.

For Account usage, a _setSigner function is provided to set the signer public key. Doing so is easier for a factory, who is likely to use initializable clones of this contract.

Example of usage:

contract MyAccountRSA is Account, SignerRSA, Initializable {
    function initialize(bytes memory e, bytes memory n) public initializer {
      _setSigner(e, n);
    }
}
Failing to call _setSigner either during construction (if used standalone) or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
Functions
  • _setSigner(e, n)

  • signer()

  • _rawSignatureValidation(hash, signature)

_setSigner(bytes e, bytes n) internal

Sets the signer with a RSA public key. This function should be called during construction or through an initializer.

signer() → bytes e, bytes n public

Return the signer’s RSA public key.

_rawSignatureValidation(bytes32 hash, bytes signature) → bool internal

See AbstractSigner._rawSignatureValidation. Verifies a PKCSv1.5 signature by calling RSA.pkcs1Sha256.

Following the RSASSA-PKCS1-V1_5-VERIFY procedure outlined in RFC8017 (section 8.2.2), the provided hash is used as the M (message) and rehashed using SHA256 according to EMSA-PKCS1-v1_5 encoding as per section 9.2 (step 1) of the RFC.

SignerERC7702

import "@openzeppelin/community-contracts/utils/cryptography/signers/SignerERC7702.sol";

Implementation of AbstractSigner for implementation for an EOA. Useful for ERC-7702 accounts.

Functions
  • _rawSignatureValidation(hash, signature)

_rawSignatureValidation(bytes32 hash, bytes signature) → bool internal

Validates the signature using the EOA’s address (i.e. address(this)).

SignerERC7913

import "@openzeppelin/community-contracts/utils/cryptography/signers/SignerERC7913.sol";

Implementation of AbstractSigner using ERC-7913 signature verification.

For Account usage, a _setSigner function is provided to set the ERC-7913 formatted signer. Doing so is easier for a factory, who is likely to use initializable clones of this contract.

The signer is a bytes object that concatenates a verifier address and a key: verifier || key.

Example of usage:

contract MyAccountERC7913 is Account, SignerERC7913, Initializable {
    function initialize(bytes memory signer_) public initializer {
      _setSigner(signer_);
    }
}
Failing to call _setSigner either during construction (if used standalone) or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
Functions
  • signer()

  • _setSigner(signer_)

  • _rawSignatureValidation(hash, signature)

signer() → bytes public

Return the ERC-7913 signer (i.e. verifier || key).

_setSigner(bytes signer_) internal

Sets the signer (i.e. verifier || key) with an ERC-7913 formatted signer.

_rawSignatureValidation(bytes32 hash, bytes signature) → bool internal

Verifies a signature using ERC7913Utils.isValidSignatureNow with signer, hash and signature.

MultiSignerERC7913

import "@openzeppelin/community-contracts/utils/cryptography/signers/MultiSignerERC7913.sol";

Implementation of AbstractSigner using multiple ERC-7913 signers with a threshold-based signature verification system.

This contract allows managing a set of authorized signers and requires a minimum number of signatures (threshold) to approve operations. It uses ERC-7913 formatted signers, which concatenate a verifier address and a key: verifier || key.

Example of usage:

contract MyMultiSignerAccount is Account, MultiSignerERC7913, Initializable {
    constructor() EIP712("MyMultiSignerAccount", "1") {}

    function initialize(bytes[] memory signers, uint256 threshold) public initializer {
        _addSigners(signers);
        _setThreshold(threshold);
    }

    function addSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
        _addSigners(signers);
    }

    function removeSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
        _removeSigners(signers);
    }

    function setThreshold(uint256 threshold) public onlyEntryPointOrSelf {
        _setThreshold(threshold);
    }
}
Failing to properly initialize the signers and threshold either during construction (if used standalone) or during initialization (if used as a clone) may leave the contract either front-runnable or unusable.
Functions
  • signers()

  • isSigner(signer)

  • threshold()

  • _addSigners(newSigners)

  • _removeSigners(oldSigners)

  • _setThreshold(newThreshold)

  • _validateReachableThreshold()

  • _rawSignatureValidation(hash, signature)

  • _validateSignatures(hash, signingSigners, signatures)

  • _validateThreshold(validatingSigners)

Events
  • ERC7913SignerAdded(signers)

  • ERC7913SignerRemoved(signers)

  • ERC7913ThresholdSet(threshold)

Errors
  • MultiSignerERC7913AlreadyExists(signer)

  • MultiSignerERC7913NonexistentSigner(signer)

  • MultiSignerERC7913InvalidSigner(signer)

  • MultiSignerERC7913UnreachableThreshold(signers, threshold)

signers() → bytes[] public

Returns the set of authorized signers. Prefer {_signers} for internal use.

This operation copies the entire signers set to memory, which can be expensive. This is designed for view accessors queried without gas fees. Using it in state-changing functions may become uncallable if the signers set grows too large.

isSigner(bytes signer) → bool public

Returns whether the signer is an authorized signer.

threshold() → uint256 public

Returns the minimum number of signers required to approve a multisignature operation.

_addSigners(bytes[] newSigners) internal

Adds the newSigners to those allowed to sign on behalf of this contract. Internal version without access control.

Requirements:

_removeSigners(bytes[] oldSigners) internal

Removes the oldSigners from the authorized signers. Internal version without access control.

Requirements:

_setThreshold(uint256 newThreshold) internal

Sets the signatures threshold required to approve a multisignature operation. Internal version without access control.

Requirements:

_validateReachableThreshold() internal

Validates the current threshold is reachable.

Requirements:

_rawSignatureValidation(bytes32 hash, bytes signature) → bool internal

Decodes, validates the signature and checks the signers are authorized. See _validateSignatures and _validateThreshold for more details.

Example of signature encoding:

// Encode signers (verifier || key)
bytes memory signer1 = abi.encodePacked(verifier1, key1);
bytes memory signer2 = abi.encodePacked(verifier2, key2);

// Order signers by their id
if (keccak256(signer1) > keccak256(signer2)) {
    (signer1, signer2) = (signer2, signer1);
    (signature1, signature2) = (signature2, signature1);
}

// Assign ordered signers and signatures
bytes[] memory signers = new bytes[](2);
bytes[] memory signatures = new bytes[](2);
signers[0] = signer1;
signatures[0] = signature1;
signers[1] = signer2;
signatures[1] = signature2;

// Encode the multi signature
bytes memory signature = abi.encode(signers, signatures);

Requirements:

  • The signature must be encoded as abi.encode(signers, signatures).

_validateSignatures(bytes32 hash, bytes[] signingSigners, bytes[] signatures) → bool valid internal

Validates the signatures using the signers and their corresponding signatures. Returns whether whether the signers are authorized and the signatures are valid for the given hash.

For simplicity, this contract assumes that the signers are ordered by their keccak256 hash to avoid duplication when iterating through the signers (i.e. keccak256(signer1) < keccak256(signer2)). The function will return false if the signers are not ordered.

Requirements:

  • The signatures arrays must be at least as large as the signingSigners arrays. Panics otherwise.

_validateThreshold(bytes[] validatingSigners) → bool internal

Validates that the number of signers meets the threshold requirement. Assumes the signers were already validated. See _validateSignatures for more details.

ERC7913SignerAdded(bytes indexed signers) event

Emitted when a signer is added.

ERC7913SignerRemoved(bytes indexed signers) event

Emitted when a signers is removed.

ERC7913ThresholdSet(uint256 threshold) event

Emitted when the threshold is updated.

MultiSignerERC7913AlreadyExists(bytes signer) error

The signer already exists.

MultiSignerERC7913NonexistentSigner(bytes signer) error

The signer does not exist.

MultiSignerERC7913InvalidSigner(bytes signer) error

The signer is less than 20 bytes long.

MultiSignerERC7913UnreachableThreshold(uint256 signers, uint256 threshold) error

The threshold is unreachable given the number of signers.

MultiSignerERC7913Weighted

import "@openzeppelin/community-contracts/utils/cryptography/signers/MultiSignerERC7913Weighted.sol";

Extension of MultiSignerERC7913 that supports weighted signatures.

This contract allows assigning different weights to each signer, enabling more flexible governance schemes. For example, some signers could have higher weight than others, allowing for weighted voting or prioritized authorization.

Example of usage:

contract MyWeightedMultiSignerAccount is Account, MultiSignerERC7913Weighted, Initializable {
    constructor() EIP712("MyWeightedMultiSignerAccount", "1") {}

    function initialize(bytes[] memory signers, uint256[] memory weights, uint256 threshold) public initializer {
        _addSigners(signers);
        _setSignerWeights(signers, weights);
        _setThreshold(threshold);
    }

    function addSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
        _addSigners(signers);
    }

    function removeSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
        _removeSigners(signers);
    }

    function setThreshold(uint256 threshold) public onlyEntryPointOrSelf {
        _setThreshold(threshold);
    }

    function setSignerWeights(bytes[] memory signers, uint256[] memory weights) public onlyEntryPointOrSelf {
        _setSignerWeights(signers, weights);
    }
}
When setting a threshold value, ensure it matches the scale used for signer weights. For example, if signers have weights like 1, 2, or 3, then a threshold of 4 would require at least two signers (e.g., one with weight 1 and one with weight 3). See signerWeight.
Functions
  • signerWeight(signer)

  • totalWeight()

  • _signerWeight(signer)

  • _setSignerWeights(signers, newWeights)

  • _addSigners(newSigners)

  • _removeSigners(oldSigners)

  • _validateReachableThreshold()

  • _validateThreshold(signers)

  • _weightSigners(signers)

MultiSignerERC7913
  • signers()

  • isSigner(signer)

  • threshold()

  • _setThreshold(newThreshold)

  • _rawSignatureValidation(hash, signature)

  • _validateSignatures(hash, signingSigners, signatures)

Events
  • ERC7913SignerWeightChanged(signer, weight)

MultiSignerERC7913
  • ERC7913SignerAdded(signers)

  • ERC7913SignerRemoved(signers)

  • ERC7913ThresholdSet(threshold)

Errors
  • MultiSignerERC7913WeightedInvalidWeight(signer, weight)

  • MultiSignerERC7913WeightedMismatchedLength()

MultiSignerERC7913
  • MultiSignerERC7913AlreadyExists(signer)

  • MultiSignerERC7913NonexistentSigner(signer)

  • MultiSignerERC7913InvalidSigner(signer)

  • MultiSignerERC7913UnreachableThreshold(signers, threshold)

signerWeight(bytes signer) → uint256 public

Gets the weight of a signer. Returns 0 if the signer is not authorized.

totalWeight() → uint256 public

Gets the total weight of all signers.

_signerWeight(bytes signer) → uint256 internal

Gets the weight of the current signer. Returns 1 if not explicitly set.

This internal function doesn’t check if the signer is authorized.

_setSignerWeights(bytes[] signers, uint256[] newWeights) internal

Sets weights for multiple signers at once. Internal version without access control.

Requirements:

Emits ERC7913SignerWeightChanged for each signer.

_addSigners(bytes[] newSigners) internal

Adds the newSigners to those allowed to sign on behalf of this contract. Internal version without access control.

Requirements:

_removeSigners(bytes[] oldSigners) internal

Emits ERC7913SignerWeightChanged for each removed signer.

_validateReachableThreshold() internal

Sets the threshold for the multisignature operation. Internal version without access control.

Requirements:

This function intentionally does not call super._validateReachableThreshold because the base implementation assumes each signer has a weight of 1, which is a subset of this weighted implementation. Consider that multiple implementations of this function may exist in the contract, so important side effects may be missed depending on the linearization order.

_validateThreshold(bytes[] signers) → bool internal

Validates that the total weight of signers meets the threshold requirement.

This function intentionally does not call super. _validateThreshold because the base implementation assumes each signer has a weight of 1, which is a subset of this weighted implementation. Consider that multiple implementations of this function may exist in the contract, so important side effects may be missed depending on the linearization order.

_weightSigners(bytes[] signers) → uint256 internal

Calculates the total weight of a set of signers. For all signers weight use totalWeight.

ERC7913SignerWeightChanged(bytes indexed signer, uint256 weight) event

Emitted when a signer’s weight is changed.

MultiSignerERC7913WeightedInvalidWeight(bytes signer, uint256 weight) error

Thrown when a signer’s weight is invalid.

MultiSignerERC7913WeightedMismatchedLength() error

Thrown when the threshold is unreachable.

SignerZKEmail

import "@openzeppelin/community-contracts/utils/cryptography/signers/SignerZKEmail.sol";

Implementation of AbstractSigner using ZKEmail signatures.

ZKEmail enables secure authentication and authorization through email messages, leveraging DKIM signatures from a DKIMRegistry and zero-knowledge proofs enabled by a verifier contract that ensures email authenticity without revealing sensitive information. The DKIM registry is trusted to correctly update DKIM keys, but users can override this behaviour and set their own keys. This contract implements the core functionality for validating email-based signatures in smart contracts.

Developers must set the following components during contract initialization:

  • accountSalt - A unique identifier derived from the user’s email address and account code.

  • DKIMRegistry - An instance of the DKIM registry contract for domain verification.

  • verifier - An instance of the Verifier contract for zero-knowledge proof validation.

  • templateId - The template ID of the sign hash command, defining the expected format.

Example of usage:

contract MyAccountZKEmail is Account, SignerZKEmail, Initializable {
  function initialize(
      bytes32 accountSalt,
      IDKIMRegistry registry,
      IVerifier verifier,
      uint256 templateId
  ) public initializer {
      // Will revert if the signer is already initialized
      _setAccountSalt(accountSalt);
      _setDKIMRegistry(registry);
      _setVerifier(verifier);
      _setTemplateId(templateId);
  }
}
Avoiding to call _setAccountSalt, _setDKIMRegistry, _setVerifier and _setTemplateId either during construction (if used standalone) or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
Functions
  • accountSalt()

  • DKIMRegistry()

  • verifier()

  • templateId()

  • _setAccountSalt(accountSalt_)

  • _setDKIMRegistry(registry_)

  • _setVerifier(verifier_)

  • _setTemplateId(templateId_)

  • _rawSignatureValidation(hash, signature)

Errors
  • InvalidEmailProof(err)

accountSalt() → bytes32 public

Unique identifier for owner of this contract defined as a hash of an email address and an account code.

An account code is a random integer in a finite scalar field of BN254 curve. It is a private randomness to derive a CREATE2 salt of the user’s Ethereum address from the email address, i.e., userEtherAddr := CREATE2(hash(userEmailAddr, accountCode)).

The account salt is used for:

  • Privacy: Enables email address privacy on-chain so long as the randomly generated account code is not revealed to an adversary.

  • Security: Provides a unique identifier that cannot be easily guessed or brute-forced, as it’s derived from both the email address and a random account code.

  • Deterministic Address Generation: Enables the creation of deterministic addresses based on email addresses, allowing users to recover their accounts using only their email.

DKIMRegistry() → contract IDKIMRegistry public

An instance of the DKIM registry contract. See DKIM Verification.

verifier() → contract IVerifier public

An instance of the Verifier contract. See ZK Proofs.

templateId() → uint256 public

The command template of the sign hash command.

_setAccountSalt(bytes32 accountSalt_) internal

Set the accountSalt.

_setDKIMRegistry(contract IDKIMRegistry registry_) internal

Set the DKIMRegistry contract address.

_setVerifier(contract IVerifier verifier_) internal

Set the verifier contract address.

_setTemplateId(uint256 templateId_) internal

Set the command’s templateId.

_rawSignatureValidation(bytes32 hash, bytes signature) → bool internal

See AbstractSigner._rawSignatureValidation. Validates a raw signature by:

  1. Decoding the email authentication message from the signature

  2. Verifying the hash matches the command parameters

  3. Checking the template ID matches

  4. Validating the account salt

  5. Verifying the email proof

InvalidEmailProof(enum ZKEmailUtils.EmailProofError err) error

Proof verification error.

Verifiers

ERC7913P256Verifier

import "@openzeppelin/community-contracts/utils/cryptography/verifiers/ERC7913P256Verifier.sol";

ERC-7913 signature verifier that support P256 (secp256r1) keys.

Functions
  • verify(key, hash, signature)

verify(bytes key, bytes32 hash, bytes signature) → bytes4 public

Verifies signature as a valid signature of hash by key.

MUST return the bytes4 magic value IERC7913SignatureVerifier.verify.selector if the signature is valid. SHOULD return 0xffffffff or revert if the signature is not valid. SHOULD return 0xffffffff or revert if the key is empty

ERC7913RSAVerifier

import "@openzeppelin/community-contracts/utils/cryptography/verifiers/ERC7913RSAVerifier.sol";

ERC-7913 signature verifier that support RSA keys.

Functions
  • verify(key, hash, signature)

verify(bytes key, bytes32 hash, bytes signature) → bytes4 public

Verifies signature as a valid signature of hash by key.

MUST return the bytes4 magic value IERC7913SignatureVerifier.verify.selector if the signature is valid. SHOULD return 0xffffffff or revert if the signature is not valid. SHOULD return 0xffffffff or revert if the key is empty

ERC7913ZKEmailVerifier

import "@openzeppelin/community-contracts/utils/cryptography/verifiers/ERC7913ZKEmailVerifier.sol";

ERC-7913 signature verifier that supports ZKEmail accounts.

This contract verifies signatures produced through ZKEmail’s zero-knowledge proofs which allows users to authenticate using their email addresses.

The key decoding logic is customizable: users may override the _decodeKey function to enforce restrictions or validation on the decoded values (e.g., requiring a specific verifier, templateId, or registry). To remain compliant with ERC-7913’s statelessness, it is recommended to enforce such restrictions using immutable variables only.

Example of overriding _decodeKey to enforce a specific verifier, registry, (or templateId):

  function _decodeKey(bytes calldata key) internal view override returns (
      IDKIMRegistry registry,
      bytes32 accountSalt,
      IVerifier verifier,
      uint256 templateId
  ) {
      (registry, accountSalt, verifier, templateId) = super._decodeKey(key);
      require(verifier == _verifier, "Invalid verifier");
      require(registry == _registry, "Invalid registry");
      return (registry, accountSalt, verifier, templateId);
  }
Functions
  • verify(key, hash, signature)

  • _decodeKey(key)

verify(bytes key, bytes32 hash, bytes signature) → bytes4 public

Verifies a zero-knowledge proof of an email signature validated by a DKIMRegistry contract.

The key format is ABI-encoded (IDKIMRegistry, bytes32, IVerifier, uint256) where:

  • IDKIMRegistry: The registry contract that validates DKIM public key hashes

  • bytes32: The account salt that uniquely identifies the user’s email address

  • IVerifier: The verifier contract instance for ZK proof verification.

  • uint256: The template ID for the command

See _decodeKey for the key encoding format.

The signature is an ABI-encoded {ZKEmailUtils-EmailAuthMsg} struct containing the command parameters, template ID, and proof details.

Signature encoding:

bytes memory signature = abi.encode(EmailAuthMsg({
    templateId: 1,
    commandParams: [hash],
    proof: {
        domainName: "example.com", // The domain name of the email sender
        publicKeyHash: bytes32(0x...), // Hash of the DKIM public key used to sign the email
        timestamp: block.timestamp, // When the email was sent
        maskedCommand: "Sign hash", // The command being executed, with sensitive data masked
        emailNullifier: bytes32(0x...), // Unique identifier for the email to prevent replay attacks
        accountSalt: bytes32(0x...), // Unique identifier derived from email and account code
        isCodeExist: true, // Whether the account code exists in the proof
        proof: bytes(0x...) // The zero-knowledge proof verifying the email's authenticity
    }
}));

_decodeKey(bytes key) → contract IDKIMRegistry registry, bytes32 accountSalt, contract IVerifier verifier, uint256 templateId internal

Decodes the key into its components.

bytes memory key = abi.encode(registry, accountSalt, verifier, templateId);