diff --git a/docs/domain-verification-key-generation.md b/docs/domain-verification-key-generation.md new file mode 100644 index 0000000..e62bf18 --- /dev/null +++ b/docs/domain-verification-key-generation.md @@ -0,0 +1,101 @@ +# Domain Verification Key Generation + +This script generates a secp256k1 key pair and outputs a JWKS (JSON Web Key Set) file for Base domain verification, as well as a private key for signing. + +## What is JWKS? + +JWKS (JSON Web Key Set) is an industry standard format (RFC 7517) for representing cryptographic keys in JSON. It's widely used in OAuth 2.0, OpenID Connect, and other security protocols. + +## Where is the Public Key? + +Your public key is stored as **x and y coordinates** in the JWKS: + +```json +{ + "x": "base64url-encoded-x-coordinate", + "y": "base64url-encoded-y-coordinate" +} +``` + +### How to Reconstruct the Full Public Key: + +The full public key is: `04 + [x-coordinate] + [y-coordinate]` + +- `04` = uncompressed point prefix +- `x` = 32-byte x-coordinate (base64url decoded) +- `y` = 32-byte y-coordinate (base64url decoded) + +**Total: 65 bytes** (1 + 32 + 32) + +## What does it do? +- Generates a secp256k1 key pair +- Outputs a `base-jwks.json` file in the current directory (for hosting at `https://yourdomain.com/.well-known/base-jwks.json`) +- Outputs a `domain-verification-private-key.txt` file (for secure storage and use with the SDK) + +## Usage + +From the root directory of the repo, run: + +```sh +yarn generate-key-script +``` + +## Output +- `base-jwks.json`: Public key in JWKS format for domain verification +- `domain-verification-private-key.txt`: Private key (keep this secure!) + +## Getting Started with Origin Verification + +### Step 1: Generate Your Key Pair + +First, generate your domain verification keys: + +```sh +yarn generate-key-script +``` + +This will create: +- `base-jwks.json` - Your public key (host this on your domain) +- `domain-verification-private-key.txt` - Your private key (keep secure!) + +### Step 2: Host Your Public Key + +Host the `base-jwks.json` file at: +``` +https://yourdomain.com/.well-known/base-jwks.json +``` + +This allows Base to verify signatures from your domain. + +## Example JWKS Output +```json +{ + "version": "1.0", + "keys": [ + { + "kty": "EC", + "crv": "secp256k1", + "x": "base64url-encoded-x-coordinate", + "y": "base64url-encoded-y-coordinate", + "use": "sig", + "kid": "base-domain-verification", + "alg": "ES256K" + } + ] +} +``` + +### JWKS Field Descriptions: +- `version`: Format version for tracking changes +- `kty`: Key type ("EC" for Elliptic Curve) +- `crv`: Curve name ("secp256k1" for Bitcoin/Ethereum curve) +- `x`, `y`: Base64URL-encoded x and y coordinates of the public key +- `use`: Key usage ("sig" for signing) +- `kid`: Key ID for identification ("base-domain-verification") +- `alg`: Algorithm ("ES256K" for ECDSA with secp256k1 and SHA-256) + +## Why? +This enables secure domain verification for Base Wallet SDK integrations. The SDK will use the private key to sign requests, and Base will verify signatures using the public key hosted at your domain. + +--- +**Keep your private key safe!** Never share it or commit it to source control. \ No newline at end of file diff --git a/package.json b/package.json index 911781e..8c9b365 100644 --- a/package.json +++ b/package.json @@ -6,5 +6,9 @@ "workspaces": [ "packages/*" ], - "packageManager": "yarn@4.4.0" + "packageManager": "yarn@4.4.0", + "scripts": { + "generate-key-script": "yarn workspace @mobile-wallet-protocol/client generate-key-script --cwd \"$PWD\"", + "validate-key-script": "yarn workspace @mobile-wallet-protocol/client validate-key-script --cwd \"$PWD\"" + } } diff --git a/packages/client/package.json b/packages/client/package.json index 3963a5c..1a66528 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -29,7 +29,9 @@ "build": "tsc -p ./tsconfig.build.json && tsc-alias", "dev": "tsc --watch & nodemon --watch dist --delay 1 --exec tsc-alias", "typecheck": "tsc --noEmit", - "lint": "eslint . --ext .ts,.tsx --fix" + "lint": "eslint . --ext .ts,.tsx --fix", + "generate-key-script": "ts-node scripts/generate-domain-keys.ts", + "validate-key-script": "node scripts/verify-signature.js" }, "dependencies": { "@noble/ciphers": "^0.5.3", @@ -76,6 +78,7 @@ "jest-websocket-mock": "^2.4.0", "nodemon": "^3.1.0", "prettier": "^3.0.0", + "secp256k1": "^5.0.0", "ts-jest": "^27.1.5", "ts-node": "^10.9.1", "tsc-alias": "^1.8.8", diff --git a/packages/client/scripts/README.md b/packages/client/scripts/README.md new file mode 100644 index 0000000..123ba72 --- /dev/null +++ b/packages/client/scripts/README.md @@ -0,0 +1,23 @@ +# Scripts + +This directory contains utility scripts for the Mobile Wallet Protocol client. + +## Available Scripts + +### `generate-domain-keys.ts` + +Generates secp256k1 key pairs for Base domain verification. + +**Usage:** +```bash +yarn generate-key-script +``` + +**What it does:** +- Generates a secp256k1 key pair +- Creates a `base-jwks.json` file in JWKS format +- Creates a `domain-verification-private-key.txt` file with the private key + +**Output:** +- `base-jwks.json` - Public key in JWKS format for domain verification +- `domain-verification-private-key.txt` - Private key (keep secure!) \ No newline at end of file diff --git a/packages/client/scripts/generate-domain-keys.ts b/packages/client/scripts/generate-domain-keys.ts new file mode 100644 index 0000000..afd3d51 --- /dev/null +++ b/packages/client/scripts/generate-domain-keys.ts @@ -0,0 +1,74 @@ +#!/usr/bin/env node + +import { secp256k1 } from '@noble/curves/secp256k1'; +import { randomBytes } from '@noble/hashes/utils'; +import { writeFileSync } from 'fs'; +import { join } from 'path'; + +// Base64URL encoding function +function base64url(buffer: Uint8Array): string { + return Buffer.from(buffer) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +} + +function generateDomainVerificationKeys() { + // Generate a random private key + const privateKeyBytes = randomBytes(32); + const privateKey = Buffer.from(privateKeyBytes).toString('hex'); + + // Get the uncompressed public key from the private key + const publicKey = secp256k1.getPublicKey(privateKeyBytes, false); // false = uncompressed + + // Extract x and y coordinates from the public key + // secp256k1 uncompressed public key is 65 bytes: 1 byte prefix + 32 bytes x + 32 bytes y + const x = base64url(publicKey.slice(1, 33)); + const y = base64url(publicKey.slice(33)); + + // Create the JWK + const jwk = { + kty: 'EC', + crv: 'secp256k1', + x, + y, + use: 'sig', + kid: 'base-domain-verification', + alg: 'ES256K' + }; + + // Create the JWKS + const jwks = { + version: "1.0", + keys: [jwk] + }; + + return { jwks, privateKey }; +} + +function main() { + try { + const { jwks, privateKey } = generateDomainVerificationKeys(); + + // Try to get the project root by going up from the current directory + let projectRoot = process.cwd(); + if (projectRoot.includes('packages/client')) { + // If we're in packages/client, go up to the project root + projectRoot = projectRoot.replace('/packages/client', ''); + } + + // Write the JWKS file in the project root directory + const jwksPath = join(projectRoot, 'base-jwks.json'); + writeFileSync(jwksPath, JSON.stringify(jwks, null, 2)); + + // Write the private key to a separate file in the project root directory + const privateKeyPath = join(projectRoot, 'domain-verification-private-key.txt'); + writeFileSync(privateKeyPath, privateKey); + + } catch (error) { + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/packages/client/scripts/verify-signature.js b/packages/client/scripts/verify-signature.js new file mode 100644 index 0000000..e997c6b --- /dev/null +++ b/packages/client/scripts/verify-signature.js @@ -0,0 +1,105 @@ +#!/usr/bin/env node + +const crypto = require('crypto'); +const fs = require('fs'); +const secp256k1 = require('secp256k1'); +const path = require('path'); + +// Mock transaction data +const mockRequestData = { + method: 'eth_sendTransaction', + params: [ + { + to: '0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6', + value: '0xde0b6b3a7640000', + gas: '0x5208', + gasPrice: '0x3b9aca00', + }, + ], +}; + +// Generate a mock nonce +const nonce = crypto.randomUUID(); +const domain = 'www.example.com'; + +// Get the project root directory +let projectRoot = process.cwd(); +if (projectRoot.includes('packages/client')) { + // If we're in packages/client, go up to the project root + projectRoot = projectRoot.replace('/packages/client', ''); +} + +// Check if required files exist +const privateKeyPath = path.join(projectRoot, 'domain-verification-private-key.txt'); +const jwksPath = path.join(projectRoot, 'base-jwks.json'); + +if (!fs.existsSync(privateKeyPath)) { + console.error('āŒ Error: domain-verification-private-key.txt not found!'); + console.error(` Expected location: ${privateKeyPath}`); + console.error(' Please run "yarn generate-key-script" first to generate the required files.'); + process.exit(1); +} + +if (!fs.existsSync(jwksPath)) { + console.error('āŒ Error: base-jwks.json not found!'); + console.error(` Expected location: ${jwksPath}`); + console.error(' Please run "yarn generate-key-script" first to generate the required files.'); + process.exit(1); +} + +try { + // Read the private key from file (now in hex format) + const privateKeyHex = fs.readFileSync(privateKeyPath, 'utf8').trim(); + const privateKey = Buffer.from(privateKeyHex, 'hex'); + + // Read the JWKS file to get public key + const jwksData = JSON.parse(fs.readFileSync(jwksPath, 'utf8')); + const publicKeyData = jwksData.keys[0]; + + // Convert JWKS coordinates to public key buffer + const x = Buffer.from(publicKeyData.x, 'base64url'); + const y = Buffer.from(publicKeyData.y, 'base64url'); + const publicKey = Buffer.concat([Buffer.from([0x04]), x, y]); + + console.log('šŸ”‘ Private Key (hex):', privateKeyHex); + console.log('🌐 Domain:', domain); + console.log('šŸŽ² Nonce:', nonce); + console.log('šŸ“ Request Data:', JSON.stringify(mockRequestData, null, 2)); + + // Create the payload to sign + const payload = { + domain: domain, + nonce: nonce, + requestData: mockRequestData, + }; + + // Serialize payload deterministically + const serializedPayload = JSON.stringify(payload); + console.log('\nšŸ“¦ Serialized Payload:', serializedPayload); + + // Create message hash + const messageHash = crypto.createHash('sha256').update(serializedPayload).digest(); + console.log('šŸ” Message Hash:', messageHash.toString('hex')); + + // Sign the message hash + const signature = secp256k1.ecdsaSign(messageHash, privateKey); + const signatureHex = '0x' + signature.signature.toString('hex'); + console.log('āœļø Signature:', signatureHex); + + // Verify the signature + const isValid = secp256k1.ecdsaVerify(signature.signature, messageHash, publicKey); + console.log('\nāœ… Signature Verification:', isValid ? 'PASSED' : 'FAILED'); + + if (isValid) { + console.log('šŸŽ‰ Domain verification simulation successful!'); + } else { + console.log('āŒ Domain verification simulation failed!'); + } +} catch (error) { + console.error('āŒ Error during signature verification:', error.message); + if (error.code === 'ENOENT') { + console.error(' This usually means one of the required files is missing or corrupted.'); + console.error(' Please run "yarn generate-key-script" to regenerate the files.'); + } + process.exit(1); +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 0259f3b..12c9492 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2075,6 +2075,7 @@ __metadata: jest-websocket-mock: "npm:^2.4.0" nodemon: "npm:^3.1.0" prettier: "npm:^3.0.0" + secp256k1: "npm:^5.0.0" ts-jest: "npm:^27.1.5" ts-node: "npm:^10.9.1" tsc-alias: "npm:^1.8.8" @@ -3343,6 +3344,13 @@ __metadata: languageName: node linkType: hard +"bn.js@npm:^4.11.9": + version: 4.12.2 + resolution: "bn.js@npm:4.12.2" + checksum: 10c0/09a249faa416a9a1ce68b5f5ec8bbca87fe54e5dd4ef8b1cc8a4969147b80035592bddcb1e9cc814c3ba79e573503d5c5178664b722b509fb36d93620dba9b57 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -3371,6 +3379,13 @@ __metadata: languageName: node linkType: hard +"brorand@npm:^1.1.0": + version: 1.1.0 + resolution: "brorand@npm:1.1.0" + checksum: 10c0/6f366d7c4990f82c366e3878492ba9a372a73163c09871e80d82fb4ae0d23f9f8924cb8a662330308206e6b3b76ba1d528b4601c9ef73c2166b440b2ea3b7571 + languageName: node + linkType: hard + "browser-process-hrtime@npm:^1.0.0": version: 1.0.0 resolution: "browser-process-hrtime@npm:1.0.0" @@ -4003,6 +4018,21 @@ __metadata: languageName: node linkType: hard +"elliptic@npm:^6.5.7": + version: 6.6.1 + resolution: "elliptic@npm:6.6.1" + dependencies: + bn.js: "npm:^4.11.9" + brorand: "npm:^1.1.0" + hash.js: "npm:^1.0.0" + hmac-drbg: "npm:^1.0.1" + inherits: "npm:^2.0.4" + minimalistic-assert: "npm:^1.0.1" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: 10c0/8b24ef782eec8b472053793ea1e91ae6bee41afffdfcb78a81c0a53b191e715cbe1292aa07165958a9bbe675bd0955142560b1a007ffce7d6c765bcaf951a867 + languageName: node + linkType: hard + "emittery@npm:^0.13.1": version: 0.13.1 resolution: "emittery@npm:0.13.1" @@ -5012,6 +5042,16 @@ __metadata: languageName: node linkType: hard +"hash.js@npm:^1.0.0, hash.js@npm:^1.0.3": + version: 1.1.7 + resolution: "hash.js@npm:1.1.7" + dependencies: + inherits: "npm:^2.0.3" + minimalistic-assert: "npm:^1.0.1" + checksum: 10c0/41ada59494eac5332cfc1ce6b7ebdd7b88a3864a6d6b08a3ea8ef261332ed60f37f10877e0c825aaa4bddebf164fbffa618286aeeec5296675e2671cbfa746c4 + languageName: node + linkType: hard + "hasown@npm:^2.0.0, hasown@npm:^2.0.1, hasown@npm:^2.0.2": version: 2.0.2 resolution: "hasown@npm:2.0.2" @@ -5021,6 +5061,17 @@ __metadata: languageName: node linkType: hard +"hmac-drbg@npm:^1.0.1": + version: 1.0.1 + resolution: "hmac-drbg@npm:1.0.1" + dependencies: + hash.js: "npm:^1.0.3" + minimalistic-assert: "npm:^1.0.0" + minimalistic-crypto-utils: "npm:^1.0.1" + checksum: 10c0/f3d9ba31b40257a573f162176ac5930109816036c59a09f901eb2ffd7e5e705c6832bedfff507957125f2086a0ab8f853c0df225642a88bf1fcaea945f20600d + languageName: node + linkType: hard + "html-encoding-sniffer@npm:^2.0.1": version: 2.0.1 resolution: "html-encoding-sniffer@npm:2.0.1" @@ -5170,7 +5221,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2": +"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:^2.0.4": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 @@ -6941,6 +6992,20 @@ __metadata: languageName: node linkType: hard +"minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": + version: 1.0.1 + resolution: "minimalistic-assert@npm:1.0.1" + checksum: 10c0/96730e5601cd31457f81a296f521eb56036e6f69133c0b18c13fe941109d53ad23a4204d946a0d638d7f3099482a0cec8c9bb6d642604612ce43ee536be3dddd + languageName: node + linkType: hard + +"minimalistic-crypto-utils@npm:^1.0.1": + version: 1.0.1 + resolution: "minimalistic-crypto-utils@npm:1.0.1" + checksum: 10c0/790ecec8c5c73973a4fbf2c663d911033e8494d5fb0960a4500634766ab05d6107d20af896ca2132e7031741f19888154d44b2408ada0852446705441383e9f8 + languageName: node + linkType: hard + "minimatch@npm:9.0.3": version: 9.0.3 resolution: "minimatch@npm:9.0.3" @@ -7117,6 +7182,26 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^5.0.0": + version: 5.1.0 + resolution: "node-addon-api@npm:5.1.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/0eb269786124ba6fad9df8007a149e03c199b3e5a3038125dfb3e747c2d5113d406a4e33f4de1ea600aa2339be1f137d55eba1a73ee34e5fff06c52a5c296d1d + languageName: node + linkType: hard + +"node-gyp-build@npm:^4.2.0": + version: 4.8.4 + resolution: "node-gyp-build@npm:4.8.4" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 10c0/444e189907ece2081fe60e75368784f7782cfddb554b60123743dfb89509df89f1f29c03bbfa16b3a3e0be3f48799a4783f487da6203245fa5bed239ba7407e1 + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 10.2.0 resolution: "node-gyp@npm:10.2.0" @@ -7948,6 +8033,18 @@ __metadata: languageName: node linkType: hard +"secp256k1@npm:^5.0.0": + version: 5.0.1 + resolution: "secp256k1@npm:5.0.1" + dependencies: + elliptic: "npm:^6.5.7" + node-addon-api: "npm:^5.0.0" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.2.0" + checksum: 10c0/ea977fcd3a21ee10439a546774d4f3f474f065a561fc2247f65cb2a64f09628732fd606c0a62316858abd7c07b41f5aa09c37773537f233590b4cf94d752dbe7 + languageName: node + linkType: hard + "semver@npm:7.x, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4": version: 7.6.3 resolution: "semver@npm:7.6.3"