Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions docs/domain-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Domain Verification

Domain verification enables secure authentication for Coinbase Smart Wallet RPC requests across all platforms (web, mobile, server-side). This system uses cryptographic signatures to verify domain ownership, allowing applications to authenticate with CBSW without relying on browser APIs.

## Key Generation

### 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.

### Generate Your Key Pair

From the root directory of the repo, run:

```sh
yarn generate-key-script
```

This will create:
- `.well-known/base-jwks.json` - Your public key (host this on your domain)
- `domain-verification-private-key.txt` - Your private key (keep secure!)

### 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)

## SDK Integration

### Basic Setup

Add the `domainVerification` parameter to your provider configuration:

```typescript
import { EIP1193Provider, Wallets } from '@mobile-wallet-protocol/client';

const provider = new EIP1193Provider({
metadata: {
name: 'My App',
customScheme: 'myapp://',
},
wallet: Wallets.CoinbaseSmartWallet,
domainVerification: {
domain: "www.example.com",
generateSignature: async (nonce, requestData) => {
// Developer implements signing with their private key
const dataToSign = JSON.stringify({
domain: "www.example.com",
nonce,
requestData
});
return await privateKey.sign(dataToSign);
}
}
});
```

### Signature Implementation

The `generateSignature` function receives:
- `nonce`: A unique identifier provided by the wallet to prevent replay attacks
- `requestData`: The RPC request being made

You must sign the payload: `JSON.stringify({ domain, nonce, requestData })`

### Example with secp256k1

```typescript
import { EIP1193Provider, Wallets } from '@mobile-wallet-protocol/client';
import { secp256k1 } from '@noble/curves/secp256k1';

// Load your private key (keep this secure!)
const privateKeyBase64 = 'your-private-key-here';
const privateKeyBytes = Buffer.from(privateKeyBase64, 'base64');

const provider = new EIP1193Provider({
metadata: {
name: 'My App',
customScheme: 'myapp://',
},
wallet: Wallets.CoinbaseSmartWallet,
domainVerification: {
domain: "www.example.com",
generateSignature: async (nonce, requestData) => {
const payload = JSON.stringify({
domain: "www.example.com",
nonce,
requestData
});

// Create message hash
const messageHash = new TextEncoder().encode(payload);

// Sign with secp256k1
const signature = secp256k1.sign(messageHash, privateKeyBytes);

// Return signature as hex string
return `0x${Buffer.from(signature.toCompactRawBytes()).toString('hex')}`;
}
}
});
```

### Wagmi Integration

```typescript
import { createConnectorFromWallet, Wallets } from '@mobile-wallet-protocol/wagmi-connectors';

const connector = createConnectorFromWallet({
metadata: {
name: 'My App',
customScheme: 'myapp://',
},
wallet: Wallets.CoinbaseSmartWallet,
domainVerification: {
domain: "www.example.com",
generateSignature: async (nonce, requestData) => {
// Your signature implementation
return await signWithPrivateKey(nonce, requestData);
}
}
});
```

## Security Best Practices

**Keep your private key safe!** Never share it or commit it to source control.

- Store private keys in secure environment variables or key management systems
- Generate new key pairs periodically and update well-known files
- Limit access to private keys to authorized personnel only
- Monitor well-known file accessibility and alert on unauthorized changes
- Use HTTPS enforcement for well-known file fetches

## How It Works

1. **Nonce Request**: Before each RPC request, the SDK requests a nonce from the wallet
2. **Signature Generation**: Your `generateSignature` function creates a cryptographic signature
3. **Request Enhancement**: The signature is added to the RPC request
4. **Verification**: Base verifies the signature against your published public key
5. **Processing**: If verification passes, the RPC request is processed normally

This enables secure domain verification for Base Wallet SDK integrations across all platforms.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
"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\""
}
}
3 changes: 2 additions & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"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"
},
"dependencies": {
"@noble/ciphers": "^0.5.3",
Expand Down
23 changes: 23 additions & 0 deletions packages/client/scripts/README.md
Original file line number Diff line number Diff line change
@@ -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 `.well-known/base-jwks.json` file in JWKS format
- Creates a `domain-verification-private-key.txt` file with the private key

**Output:**
- `.well-known/base-jwks.json` - Public key in JWKS format for domain verification
- `domain-verification-private-key.txt` - Private key (keep secure!)
74 changes: 74 additions & 0 deletions packages/client/scripts/generate-domain-keys.ts
Original file line number Diff line number Diff line change
@@ -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 = base64url(privateKeyBytes);

// 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();
25 changes: 16 additions & 9 deletions packages/client/src/MWPClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { postRequestToWallet } from './components/communication/postRequestToWallet';
import { KeyManager } from './components/key/KeyManager';
import { LIB_VERSION } from './version';
import {
decryptContent,
encryptContent,
Expand All @@ -7,24 +9,22 @@ import {
} from ':core/cipher/cipher';
import { standardErrors } from ':core/error';
import { RPCRequestMessage, RPCResponse, RPCResponseMessage } from ':core/message';
import { AppMetadata, RequestArguments } from ':core/provider/interface';
import { AppMetadata, DomainVerification, RequestArguments } from ':core/provider/interface';
import { ScopedAsyncStorage } from ':core/storage/ScopedAsyncStorage';
import { AddressString } from ':core/type';
import { ensureIntNumber, hexStringFromNumber } from ':core/type/util';

const ACCOUNTS_KEY = 'accounts';
const ACTIVE_CHAIN_STORAGE_KEY = 'activeChain';
const AVAILABLE_CHAINS_STORAGE_KEY = 'availableChains';
const WALLET_CAPABILITIES_STORAGE_KEY = 'walletCapabilities';
import { postRequestToWallet } from './components/communication/postRequestToWallet';
import { LIB_VERSION } from './version';
import {
appendMWPResponsePath,
checkErrorForInvalidRequestArgs,
fetchRPCRequest,
} from ':core/util/utils';
import { Wallet } from ':core/wallet';

const ACCOUNTS_KEY = 'accounts';
const ACTIVE_CHAIN_STORAGE_KEY = 'activeChain';
const AVAILABLE_CHAINS_STORAGE_KEY = 'availableChains';
const WALLET_CAPABILITIES_STORAGE_KEY = 'walletCapabilities';

type Chain = {
id: number;
rpcUrl?: string;
Expand All @@ -33,6 +33,7 @@ type Chain = {
type MWPClientOptions = {
metadata: AppMetadata;
wallet: Wallet;
domainVerification?: DomainVerification;
};

export class MWPClient {
Expand All @@ -44,14 +45,20 @@ export class MWPClient {
private accounts: AddressString[];
private chain: Chain;

private constructor({ metadata, wallet }: MWPClientOptions) {
private constructor({
metadata,
wallet,
domainVerification: _domainVerification,
}: MWPClientOptions) {
this.metadata = {
...metadata,
name: metadata.name || 'Dapp',
customScheme: appendMWPResponsePath(metadata.customScheme),
};

this.wallet = wallet;
// Domain verification will be implemented in a future update
// this.domainVerification = domainVerification;
this.keyManager = new KeyManager({ wallet: this.wallet });
this.storage = new ScopedAsyncStorage(this.wallet.name, 'MWPClient');

Expand Down
19 changes: 19 additions & 0 deletions packages/client/src/core/provider/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ export interface ProviderInterface extends ProviderEventEmitter {

export type ProviderEventCallback = ProviderInterface['emit'];

export interface DomainVerification {
/**
* @param domain
* @type string
* @description The domain for which domain verification is enabled
* @example "www.example.com"
*/
domain: string;
/**
* @param generateSignature
* @type function
* @description Async function that generates a signature for domain verification
* @param nonce - The nonce provided by the wallet for this request
* @param requestData - The request data to be signed
* @returns Promise<string> - The signature as a string
*/
generateSignature: (nonce: string, requestData: unknown) => Promise<string>;
}

export interface AppMetadata {
/**
* @param name
Expand Down
Loading