diff --git a/.gitignore b/.gitignore index 21f39a9..674fdd2 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,7 @@ ignition/deployments/chain-84532 *apps/notes/ .claudeignore *.claude/ -*.md +/*.md *myplans .vercel/ /tests/scan-engine.test.ts diff --git a/README.md b/README.md index af32588..a7aab29 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@
-
+
@@ -32,28 +32,63 @@
- [**TweetNaCl**](https://tweetnacl.js.org/) – for encryption/decryption with NaCl box
- [**Ethers v6**](https://docs.ethers.org/v6/) – for all Ethereum interactions
- [**Viem**](https://viem.sh/) – specific for EIP-1271/6492 verification
-- [**Dexie**](https://dexie.org/) – used for the local IndexedDB storage in the [demo app](https://verbeth-demo.vercel.app/)
----
+
## How it works
-To start a conversation, Alice emits a `Handshake` event with her ephemeral keys and an identity proof. Bob sees it, verifies her, and replies with a `HandshakeResponse`. They combine X25519 and ML-KEM-768 secrets to derive a shared root key that's secure even against future quantum computers.
+To start a conversation, Alice emits a `Handshake` event with her ephemeral keys and an identity proof. Bob sees it, verifies her, and replies with a `HandshakeResponse`. They combine X25519 and ML-KEM-768 secrets to derive a shared root key that's secure against future quantum computers.
+
+From there it's just encrypted `MessageSent` events back and forth. A Double Ratchet keeps churning keys forward so old messages stay safe even if something leaks later. Topics rotate too, making it hard for observers to link conversations across time.
+
+
+### Deployed Address
+
+Verbeth goes through the proxy at `0x82C9c5475D63e4C9e959280e9066aBb24973a663`. The current implementation behind it is `VerbethV1` at `0x51670aB6eDE1d1B11C654CCA53b7D42080802326`. Every deployment uses deterministic CREATE2, so the same address shows up on every supported chain:
+
+| Chain | Chain ID |
+| --- | --- |
+| Base mainnet | 8453 |
+| Base Sepolia | 84532 |
+| Ethereum Sepolia | 11155111 |
+
+For mor in-depth explanations on like discoverability, identity key binding and non-repudiation head over to the [docs](https://docs.verbeth.xyz).
+
+
+## Install
+
+The SDK is published on npm as [`@verbeth/sdk`](https://www.npmjs.com/package/@verbeth/sdk). Drop it into any project with
+
+```bash
+npm install @verbeth/sdk
+```
-From there it's just encrypted `MessageSent` events back and forth. A Double Ratchet keeps churning keys forward so old messages stay safe even if something leaks later. Topics rotate too, making it hard for observers to link conversations across time. More info [here]().
+or with pnpm
+```bash
+pnpm add @verbeth/sdk
+```
-### Deployed Addresses (base mainnet)
-VerbethV1 (singleton) `0x51670aB6eDE1d1B11C654CCA53b7D42080802326`
+## Build from source
-ERC1967Proxy `0x82C9c5475D63e4C9e959280e9066aBb24973a663`
+If you want to hack on the protocol locally, clone the repo and build everything from scratch. You'll need pnpm since the workspace relies on it.
+```bash
+git clone https://github.com/okrame/verbeth.git
+cd verbeth
+pnpm install
+pnpm run build
+```
-### Notes on the current model
+That compiles both the SDK and the contracts. The SDK lands in `packages/sdk/dist` with CJS and ESM outputs ready to be consumed.
-**Discoverability**: If the sender does not yet know the recipient’s long-term public key (X25519), the sender (i.e. initiator) must emit a `Handshake` event. The recipient (i.e. reponder) replies with their keys and identity proof, after which the sender caches the verified mapping. If the key is already known (from a past `HandshakeResponse`, an on-chain announcement, or a static mapping), the handshake can be skipped.
+To run the test suite
-**Identity key binding**: The message (es. “VerbEth Key Binding v1\nAddress: …\nPkEd25519: …\nPkX25519: …\nContext: …\nVersion: …”) is signed by the evm account directly binding its address to the long-term keys (i.e. preventing impersonation).
+```bash
+pnpm run test:unit
+pnpm run test:contracts
+pnpm run test:integration
+```
-**Non-repudiation**: By default, confidentiality and integrity are guaranteed by AEAD with NaCl box. Additionally, the sender can attach a detached Ed25519 signature over using the Ed25519 key bound in the handshake. This effectively provides per-message origin authentication that is verifiable: a recipient (or any third party) can prove the message was produced by the holder of that specific Ed25519 key. Otherwise, attribution relies on context, making sender spoofing at the application layer harder to detect. |
+The integration tests need Anvil running, so run it in another terminal first with `pnpm run anvil:start`.
diff --git a/erc/ERC-pre-draft.md b/erc/ERC-pre-draft.md
new file mode 100644
index 0000000..7b5dfce
--- /dev/null
+++ b/erc/ERC-pre-draft.md
@@ -0,0 +1,569 @@
+---
+
+title: Event-Log Encrypted Messaging
+description: A minimal application-layer standard for post-quantum-resistant encrypted messaging over EVM event logs.
+author: Marco (@okrame)
+discussions-to: https://ethereum-magicians.org/t/hybrid-post-quantum-e2ee-messaging-over-evm-event-logs/28198
+status: Draft
+type: Standards Track
+category: ERC
+created: 2026-04-08
+
+---
+
+## Abstract
+
+This proposal defines a minimal application-layer standard for post-quantum-resistant encrypted messaging over EVM event logs.
+
+It standardizes:
+
+- a transport contract interface with three functions and three events for handshake initiation, handshake response, and post-handshake message delivery;
+- canonical wire formats for long-term public keys, handshake payloads, handshake response payloads, and ratcheted message payloads;
+- derivation rules for recipient discovery hashes and post-handshake message topics; and
+- a wallet-bound identity proof format that binds messaging keys to an Ethereum account.
+
+The design aims to preserve message confidentiality while still supporting disclosure-time authorship and accountability: the chain proves that an account authorized publication of a ciphertext, and disclosed protocol artifacts can later bind that ciphertext to a wallet-authorized messaging identity.
+
+The proposal does not standardize local storage, notifications, indexers, private retrieval, wallet UX, gas sponsorship, session modules, or deployment addresses.
+
+## Motivation
+
+Ethereum applications lack a widely documented application-layer standard for interoperable encrypted messaging based purely on onchain transport. Existing applications typically rely on proprietary relays, application-specific servers, or unpublished wire formats, which makes interoperation between independent clients difficult.
+
+This proposal is motivated by five goals:
+
+1. Define a minimal transport primitive that any EVM application can implement without depending on an operator-controlled message server.
+2. Standardize enough of the wire format to allow independent implementations to parse, verify, and decrypt the same handshake and message flows.
+3. Bind messaging keys to Ethereum accounts using wallet signatures, so recipients can authenticate long-term messaging keys before trusting them.
+4. Preserve accountable privacy by combining encrypted payloads with public transport-level authorship and signed transcript artifacts that can be verified after voluntary disclosure.
+5. Preserve a narrow scope suitable for standardization by excluding storage, indexing, private retrieval, and account-abstraction-specific UX choices.
+
+This proposal is deliberately narrower than a full messaging stack. It does not attempt to standardize contact lists, inbox sync, push notifications, message history export, anti-spam systems, or application-specific presentation logic.
+
+Unlike deniable offchain messaging systems, this proposal treats public transport-level attribution as a first-class design constraint. That tradeoff enables applications where message contents should remain private during normal operation, but participants may later need portable proof of who authorized a given encrypted exchange. Example use cases include private deal negotiation, agent-to-agent commerce, delegated service procurement, dispute resolution, and compliance-sensitive messaging where confidentiality and accountable authorship are both required.
+
+This proposal also differs from registry-centric messaging approaches such as [ERC-7627](https://eips.ethereum.org/EIPS/eip-7627) by focusing on event-log transport and interoperable message exchange rather than requiring an onchain public-key directory as a mandatory prerequisite. A future extension MAY define optional registry mechanisms compatible with this transport.
+
+This proposal standardizes transport interoperability, not full metadata anonymity. In particular, it distinguishes two privacy goals that are often conflated:
+
+1. recipient privacy against public onchain observers; and
+2. query privacy against malicious RPC providers or indexers.
+
+The current design provides strong unlinkability between `HandshakeResponse` and later `MessageSent` events, but it does not fully hide attempted first contact when recipient discovery uses a deterministic `recipientHash`.
+
+### Design goals
+
+- Minimal onchain logic.
+- Offchain cryptographic verification.
+- Interoperable wire formats.
+- Support for EOAs and smart accounts as transport senders.
+- Compatibility with event-log retrieval via `eth_getLogs`.
+
+### Non-goals
+
+- Onchain message decryption or validation.
+- Onchain nonce enforcement or replay prevention.
+- Standardizing a specific indexer architecture.
+- Standardizing private mailbox retrieval.
+- Standardizing Safe modules, paymaster flows, or bundler flows.
+- Standardizing contract upgrade governance.
+- Standardizing deterministic deployment addresses.
+
+## Specification
+
+The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
+
+### 1. Transport contract
+
+A compliant transport contract MUST expose the following functions:
+
+```solidity
+function sendMessage(
+ bytes calldata ciphertext,
+ bytes32 topic,
+ uint256 timestamp,
+ uint256 nonce
+) external;
+
+function initiateHandshake(
+ bytes32 recipientHash,
+ bytes calldata pubKeys,
+ bytes calldata ephemeralPubKey,
+ bytes calldata plaintextPayload
+) external;
+
+function respondToHandshake(
+ bytes32 inResponseTo,
+ bytes32 responderEphemeralR,
+ bytes calldata ciphertext
+) external;
+```
+
+A compliant transport contract MUST emit the following events:
+
+```solidity
+event MessageSent(
+ address indexed sender,
+ bytes ciphertext,
+ uint256 timestamp,
+ bytes32 indexed topic,
+ uint256 nonce
+);
+
+event Handshake(
+ bytes32 indexed recipientHash,
+ address indexed sender,
+ bytes pubKeys,
+ bytes ephemeralPubKey,
+ bytes plaintextPayload
+);
+
+event HandshakeResponse(
+ bytes32 indexed inResponseTo,
+ address indexed responder,
+ bytes32 responderEphemeralR,
+ bytes ciphertext
+);
+```
+
+The transport contract:
+
+- MUST set `sender` or `responder` to `msg.sender`;
+- MUST NOT require recipient registration;
+- MUST NOT parse or validate encrypted payload contents onchain;
+- MUST NOT enforce nonce uniqueness; and
+- MAY include non-messaging administrative functions, but such functions are outside the scope of this proposal.
+
+`timestamp` and `nonce` are sender-supplied metadata. Receivers MUST treat them as application-level ordering and deduplication hints, not as globally trusted consensus facts.
+
+### 2. Recipient discovery hash
+
+Handshake discovery uses a recipient hash derived from the intended recipient address.
+
+The canonical derivation is:
+
+```text
+recipientHash = keccak256(utf8("contact:" + lowercase(recipientAddress)))
+```
+
+Implementations MUST lowercase the hex address before concatenation.
+
+The `recipientHash` is a discovery selector for efficiency, not a guarantee of recipient anonymity. Because it is deterministic, observers who can guess candidate recipient addresses can test those guesses offline.
+
+Implementations:
+
+- MUST treat `recipientHash` as a privacy tradeoff rather than a privacy proof;
+- SHOULD assume that server-side filtering on `recipientHash` reveals recipient interest to any RPC or indexer that sees the query; and
+- SHOULD prefer client-side scanning or equivalent private retrieval mechanisms when query privacy against a third-party RPC or indexer is a primary goal.
+
+### 3. Long-term public key format
+
+The `pubKeys` field in `Handshake` MUST encode the sender's long-term messaging public keys as a versioned 65-byte blob:
+
+```text
+Byte 0 Bytes 1..32 Bytes 33..64
+0x01 X25519 public key Ed25519 public key
+```
+
+- Version `0x01` is REQUIRED for this proposal.
+- The X25519 key MUST be 32 bytes.
+- The Ed25519 key MUST be 32 bytes.
+
+Implementations MAY accept legacy unversioned 64-byte formats for backwards compatibility, but compliant emitters MUST emit the 65-byte versioned format above.
+
+### 4. Handshake payload
+
+The `ephemeralPubKey` field in `Handshake` MUST contain:
+
+```text
+Bytes 0..31 Bytes 32..1215
+X25519 ephemeral key ML-KEM-768 public key
+```
+
+Total length: `1216` bytes.
+
+The `plaintextPayload` field in `Handshake` MUST be a UTF-8 encoded JSON object with the following shape:
+
+```json
+{
+ "plaintextPayload": "string",
+ "identityProof": {
+ "message": "string",
+ "signature": "0x..."
+ }
+}
+```
+
+The payload is intentionally plaintext. Implementations MUST assume its contents are publicly visible.
+
+### 5. Wallet-bound identity proof
+
+The `identityProof` object binds the sender's long-term messaging keys to an Ethereum account using a wallet signature.
+
+The canonical message format is a newline-delimited UTF-8 string:
+
+```text
+VerbEth Key Binding v1
+Address: 0xabc...
+PkEd25519: 0x123...
+PkX25519: 0x456...
+ExecutorAddress: 0xdef...
+ChainId: 8453
+RpId: example.app
+```
+
+Rules:
+
+- `Address` MUST be the signing Ethereum account.
+- `PkEd25519` MUST equal the Ed25519 public key published in `pubKeys`.
+- `PkX25519` MUST equal the X25519 public key published in `pubKeys`.
+- `ExecutorAddress` MUST identify the account expected to appear as the transport sender.
+- `ChainId` is OPTIONAL but RECOMMENDED.
+- `RpId` is OPTIONAL but RECOMMENDED.
+
+For direct EOA transport, `Address` and `ExecutorAddress` MAY be equal. For smart-account transport, `Address` MAY identify the signing controller while `ExecutorAddress` identifies the account that emits the transport events.
+
+How implementations derive long-term messaging keys is out of scope. This proposal standardizes the binding proof format, not the seed derivation algorithm.
+
+Implementations verifying identity proofs:
+
+- MUST verify that the signature authorizes the exact binding message;
+- MUST verify that the bound public keys equal the keys carried by the transport payload;
+- MUST verify that `ExecutorAddress` matches the transport sender expected by the receiver's context; and
+- SHOULD support smart-account verification methods in addition to EOA verification.
+
+### 6. Handshake response tag
+
+The `inResponseTo` field in `HandshakeResponse` MUST be derived from both:
+
+- an X25519 shared secret computed from a dedicated response tag keypair; and
+- an ML-KEM-768 shared secret derived from the initiator's ML-KEM public key.
+
+The canonical derivation is:
+
+```text
+ecdhShared = X25519(r, A)
+okm = HKDF-SHA256(
+ ikm=kemSecret,
+ salt=ecdhShared,
+ info="verbeth:hsr-hybrid:v1",
+ len=32
+ )
+tag = keccak256(okm)
+```
+
+Where:
+
+- `A` is the initiator's X25519 ephemeral public key from `Handshake`;
+- `r` is the responder's dedicated response-tag private key; and
+- `kemSecret` is the ML-KEM-768 shared secret produced by encapsulation to the initiator's ML-KEM public key.
+
+The responder MUST publish the corresponding tag public key `R` as `responderEphemeralR`.
+
+### 7. Two-key handshake response requirement
+
+To prevent linkability between the handshake response and the first post-handshake ratchet key, the responder MUST use two distinct X25519 keypairs:
+
+1. a response-tag keypair `(r, R)` used only for `inResponseTo` derivation; and
+2. a ratchet keypair `(rk_s, rk_p)` used only for encrypted response delivery and ratchet session bootstrapping.
+
+The public key `R` MUST be published onchain as `responderEphemeralR`.
+
+The public key `rk_p` MUST be carried only inside the encrypted handshake response payload.
+
+### 8. Handshake response payload
+
+The `ciphertext` field in `HandshakeResponse` MUST be a UTF-8 encoded JSON envelope with the following shape:
+
+```json
+{
+ "v": 1,
+ "epk": "