Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Derivation paths for private key operations #18

Merged
merged 1 commit into from
Jan 10, 2025
Merged
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
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,40 @@ The `useOpenSecret` hook provides access to the OpenSecret API. It returns an ob
- `generateThirdPartyToken(audience: string): Promise<{ token: string }>`: Generates a JWT token for use with pre-authorized third-party services (e.g. "https://api.devservice.com"). Developers must register this URL in advance (coming soon).

#### Cryptographic Methods
- `getPrivateKey(): Promise<PrivateKeyResponse>`: Retrieves the user's private key mnemonic phrase. This is used for cryptographic operations and should be kept secure.

- `getPublicKey(algorithm: 'schnorr' | 'ecdsa'): Promise<PublicKeyResponse>`: Retrieves the user's public key for the specified signing algorithm. Supports two algorithms:
- `getPrivateKey(): Promise<{ mnemonic: string }>`: Retrieves the user's private key mnemonic phrase. This is used for cryptographic operations and should be kept secure.

- `getPrivateKeyBytes(derivationPath?: string): Promise<{ private_key: string }>`: Retrieves the private key bytes for a given BIP32 derivation path. If no path is provided, returns the master private key bytes.
- Supports both absolute (starting with "m/") and relative paths
- Supports hardened derivation using either ' or h notation
Examples:
- Absolute path: "m/44'/0'/0'/0/0"
- Relative path: "0'/0'/0'/0/0"
- Hardened notation: "44'" or "44h"
- Common paths:
- BIP44 (Legacy): `m/44'/0'/0'/0/0`
- BIP49 (SegWit): `m/49'/0'/0'/0/0`
- BIP84 (Native SegWit): `m/84'/0'/0'/0/0`
- BIP86 (Taproot): `m/86'/0'/0'/0/0`

- `getPublicKey(algorithm: 'schnorr' | 'ecdsa', derivationPath?: string): Promise<PublicKeyResponse>`: Retrieves the user's public key for the specified signing algorithm and optional derivation path. The derivation path determines which child key pair is used, allowing different public keys to be generated from the same master key. This is useful for:
- Separating keys by purpose (e.g., different chains or applications)
- Generating deterministic addresses
- Supporting different address formats (Legacy, SegWit, Native SegWit, Taproot)

Supports two algorithms:
- `'schnorr'`: For Schnorr signatures
- `'ecdsa'`: For ECDSA signatures

- `signMessage(messageBytes: Uint8Array, algorithm: 'schnorr' | 'ecdsa'): Promise<SignatureResponse>`: Signs a message using the specified algorithm. The message must be provided as a Uint8Array of bytes. Returns a signature that can be verified using the corresponding public key.
- `signMessage(messageBytes: Uint8Array, algorithm: 'schnorr' | 'ecdsa', derivationPath?: string): Promise<SignatureResponse>`: Signs a message using the specified algorithm and optional derivation path. The message must be provided as a Uint8Array of bytes. Returns a signature that can be verified using the corresponding public key.

Example message preparation:
```typescript
// From string
const messageBytes = new TextEncoder().encode("Hello, World!");

// From hex
const messageBytes = new Uint8Array(Buffer.from("deadbeef", "hex"));
```

### AI Integration

Expand Down
109 changes: 106 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function App() {
message: string;
} | null>(null);
const [verificationResult, setVerificationResult] = useState<boolean | null>(null);
const [derivationPath, setDerivationPath] = useState<string>("");

const handleSignup = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
Expand Down Expand Up @@ -261,7 +262,7 @@ function App() {

try {
const messageBytes = new TextEncoder().encode(message);
const response = await os.signMessage(messageBytes, algorithm);
const response = await os.signMessage(messageBytes, algorithm, derivationPath || undefined);

setLastSignature({
signature: response.signature,
Expand Down Expand Up @@ -489,8 +490,54 @@ function App() {
}}
style={{ marginBottom: "1rem" }}
>
Show Private Key
Show Private Key Mnemonic
</button>

<div>
<h3>Private Key Bytes</h3>
<p>Get private key bytes for a specific BIP32 derivation path.</p>
<details>
<summary>Common derivation paths</summary>
<ul>
<li>BIP44 (Legacy): m/44'/0'/0'/0/0</li>
<li>BIP49 (SegWit): m/49'/0'/0'/0/0</li>
<li>BIP84 (Native SegWit): m/84'/0'/0'/0/0</li>
<li>BIP86 (Taproot): m/86'/0'/0'/0/0</li>
</ul>
<p><small>
Note: Supports both absolute (starting with "m/") and relative paths.
Supports hardened derivation using either ' or h notation.
</small></p>
<p><small>
Examples:
<ul>
<li>Absolute path: "m/44'/0'/0'/0/0"</li>
<li>Relative path: "0'/0'/0'/0/0"</li>
<li>Hardened notation: "44'" or "44h"</li>
</ul>
</small></p>
</details>
<form onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const derivationPath = formData.get("derivationPath") as string;

try {
const response = await os.getPrivateKeyBytes(derivationPath || undefined);
alert(`Private key bytes (hex):\n\n${response.private_key}`);
} catch (error) {
console.error("Failed to get private key bytes:", error);
alert("Failed to get private key bytes: " + (error as Error).message);
}
}} className="auth-form">
<input
type="text"
name="derivationPath"
placeholder="Derivation path (e.g. m/44'/0'/0'/0/0)"
/>
<button type="submit">Get Private Key Bytes</button>
</form>
</div>
</section>

<section>
Expand All @@ -511,14 +558,70 @@ function App() {
</label>
</div>

<button onClick={handleGetPublicKey} style={{ marginBottom: "1rem" }}>Get Public Key</button>
<div style={{ marginBottom: "1rem" }}>
<details style={{ marginBottom: "0.5rem" }}>
<summary>About derivation paths</summary>
<p><small>
The derivation path determines which private key is used for signing.
Different paths generate different key pairs from the same master key,
useful for separating keys by purpose or application.
</small></p>
<p><small>Common paths and their uses:</small></p>
<ul>
<li><small>BIP44 (Legacy): m/44'/0'/0'/0/0 - For legacy Bitcoin addresses</small></li>
<li><small>BIP49 (SegWit): m/49'/0'/0'/0/0 - For SegWit addresses</small></li>
<li><small>BIP84 (Native SegWit): m/84'/0'/0'/0/0 - For Native SegWit</small></li>
<li><small>BIP86 (Taproot): m/86'/0'/0'/0/0 - For Taproot addresses</small></li>
</ul>
<p><small>
Path format examples:
<ul>
<li>Absolute: "m/44'/0'/0'/0/0"</li>
<li>Relative: "0'/0'/0'/0/0"</li>
<li>Hardened: "44'" or "44h"</li>
</ul>
Leave empty to use the master key.
</small></p>
</details>
<input
type="text"
value={derivationPath}
onChange={(e) => setDerivationPath(e.target.value)}
placeholder="Derivation path (optional)"
style={{ marginRight: "0.5rem", padding: "0.5rem" }}
/>
<button onClick={async () => {
try {
const response = await os.getPublicKey(algorithm, derivationPath || undefined);
setPublicKey(response.public_key);
setVerificationResult(null);
} catch (error) {
console.error("Failed to get public key:", error);
alert("Failed to get public key: " + (error as Error).message);
}
}}>Get Public Key</button>
</div>

{publicKey && (
<div className="data-display" style={{ wordBreak: "break-all", marginBottom: "1rem" }}>
<strong>Public Key:</strong> {publicKey}
</div>
)}

<form onSubmit={handleSignMessage} className="auth-form">
<details style={{ marginBottom: "0.5rem" }}>
<summary>About message signing</summary>
<p><small>
Messages are converted to bytes before signing. Examples:
</small></p>
<pre style={{ fontSize: "small" }}>
{`// From string
const messageBytes = new TextEncoder().encode("Hello, World!");

// From hex
const messageBytes = new Uint8Array(Buffer.from("deadbeef", "hex"));`}
</pre>
</details>
<textarea
name="message"
placeholder="Enter message to sign"
Expand Down
96 changes: 91 additions & 5 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,9 +389,15 @@ export async function handleGoogleCallback(
}

export type PrivateKeyResponse = {
/** 12-word BIP39 mnemonic phrase */
mnemonic: string;
};

export type PrivateKeyBytesResponse = {
/** 32-byte hex string (64 characters) representing the private key */
private_key: string;
};

export async function fetchPrivateKey(): Promise<PrivateKeyResponse> {
return authenticatedApiCall<void, PrivateKeyResponse>(
`${API_URL}/protected/private_key`,
Expand All @@ -401,34 +407,114 @@ export async function fetchPrivateKey(): Promise<PrivateKeyResponse> {
);
}

/**
* Fetches private key bytes for a given derivation path
* @param derivationPath - Optional BIP32 derivation path
*
* Supports both absolute and relative paths with hardened derivation:
* - Absolute path: "m/44'/0'/0'/0/0"
* - Relative path: "0'/0'/0'/0/0"
* - Hardened notation: "44'" or "44h"
*
* Common paths:
* - BIP44 (Legacy): m/44'/0'/0'/0/0
* - BIP49 (SegWit): m/49'/0'/0'/0/0
* - BIP84 (Native SegWit): m/84'/0'/0'/0/0
* - BIP86 (Taproot): m/86'/0'/0'/0/0
*/
export async function fetchPrivateKeyBytes(derivationPath?: string): Promise<PrivateKeyBytesResponse> {
const url = derivationPath
? `${API_URL}/protected/private_key_bytes?derivation_path=${encodeURIComponent(derivationPath)}`
: `${API_URL}/protected/private_key_bytes`;

return authenticatedApiCall<void, PrivateKeyBytesResponse>(
url,
"GET",
undefined,
"Failed to fetch private key bytes"
);
}

export type SignMessageResponse = {
/** Signature in hex format */
signature: string;
/** Message hash in hex format */
message_hash: string;
};

type SigningAlgorithm = "schnorr" | "ecdsa"

export async function signMessage(message_bytes: Uint8Array, algorithm: SigningAlgorithm): Promise<SignMessageResponse> {
export type SignMessageRequest = {
/** Base64-encoded message to sign */
message_base64: string;
/** Signing algorithm to use (schnorr or ecdsa) */
algorithm: SigningAlgorithm;
/** Optional BIP32 derivation path (e.g., "m/44'/0'/0'/0/0") */
derivation_path?: string;
};

/**
* Signs a message using the specified algorithm and derivation path
* @param message_bytes - Message to sign as Uint8Array
* @param algorithm - Signing algorithm (schnorr or ecdsa)
* @param derivationPath - Optional BIP32 derivation path
*
* Example message preparation:
* ```typescript
* // From string
* const messageBytes = new TextEncoder().encode("Hello, World!");
*
* // From hex
* const messageBytes = new Uint8Array(Buffer.from("deadbeef", "hex"));
* ```
*/
export async function signMessage(
message_bytes: Uint8Array,
algorithm: SigningAlgorithm,
derivationPath?: string
): Promise<SignMessageResponse> {
const message_base64 = encode(message_bytes);
return authenticatedApiCall<{message_base64: string, algorithm: SigningAlgorithm}, SignMessageResponse>(
return authenticatedApiCall<SignMessageRequest, SignMessageResponse>(
`${API_URL}/protected/sign_message`,
"POST",
{
message_base64,
algorithm
algorithm,
derivation_path: derivationPath
},
"Failed to sign message"
);
}

export type PublicKeyResponse = {
/** Public key in hex format */
public_key: string;
/** The algorithm used (schnorr or ecdsa) */
algorithm: SigningAlgorithm;
};

export async function fetchPublicKey(algorithm: SigningAlgorithm): Promise<PublicKeyResponse> {
/**
* Retrieves the public key for a given algorithm and derivation path
* @param algorithm - Signing algorithm (schnorr or ecdsa)
* @param derivationPath - Optional BIP32 derivation path
*
* The derivation path determines which child key pair is used,
* allowing different public keys to be generated from the same master key.
* This is useful for:
* - Separating keys by purpose (e.g., different chains or applications)
* - Generating deterministic addresses
* - Supporting different address formats (Legacy, SegWit, Native SegWit, Taproot)
*/
export async function fetchPublicKey(
algorithm: SigningAlgorithm,
derivationPath?: string
): Promise<PublicKeyResponse> {
const url = derivationPath
? `${API_URL}/protected/public_key?algorithm=${algorithm}&derivation_path=${encodeURIComponent(derivationPath)}`
: `${API_URL}/protected/public_key?algorithm=${algorithm}`;

return authenticatedApiCall<void, PublicKeyResponse>(
`${API_URL}/protected/public_key?algorithm=${algorithm}`,
url,
"GET",
undefined,
"Failed to fetch public key"
Expand Down
24 changes: 24 additions & 0 deletions src/lib/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,30 @@ export type OpenSecretContextType = {
*/
getPrivateKey: typeof api.fetchPrivateKey;

/**
* Retrieves the private key bytes for a given derivation path
* @param derivationPath - Optional BIP32 derivation path (e.g. "m/44'/0'/0'/0/0")
* @returns A promise resolving to the private key bytes response
* @throws {Error} If:
* - The private key bytes cannot be retrieved
* - The derivation path is invalid
*
* @description
* - If no derivation path is provided, returns the master private key bytes
* - Supports both absolute (starting with "m/") and relative paths
* - Supports hardened derivation using either ' or h notation
* - Common paths:
* - BIP44 (Legacy): m/44'/0'/0'/0/0
* - BIP49 (SegWit): m/49'/0'/0'/0/0
* - BIP84 (Native SegWit): m/84'/0'/0'/0/0
* - BIP86 (Taproot): m/86'/0'/0'/0/0
*/
getPrivateKeyBytes: typeof api.fetchPrivateKeyBytes;

/**
* Retrieves the user's public key for the specified algorithm
* @param algorithm - The signing algorithm ('schnorr' or 'ecdsa')
* @param derivationPath - Optional BIP32 derivation path
* @returns A promise resolving to the public key response
* @throws {Error} If the public key cannot be retrieved
*/
Expand All @@ -199,6 +220,7 @@ export type OpenSecretContextType = {
* Signs a message using the specified algorithm
* @param messageBytes - The message to sign as a Uint8Array
* @param algorithm - The signing algorithm ('schnorr' or 'ecdsa')
* @param derivationPath - Optional BIP32 derivation path
* @returns A promise resolving to the signature response
* @throws {Error} If the message signing fails
*/
Expand Down Expand Up @@ -329,6 +351,7 @@ export const OpenSecretContext = createContext<OpenSecretContextType>({
initiateGoogleAuth: async () => ({ auth_url: "", csrf_token: "" }),
handleGoogleCallback: async () => { },
getPrivateKey: api.fetchPrivateKey,
getPrivateKeyBytes: api.fetchPrivateKeyBytes,
getPublicKey: api.fetchPublicKey,
signMessage: api.signMessage,
aiCustomFetch: async () => new Response(),
Expand Down Expand Up @@ -605,6 +628,7 @@ export function OpenSecretProvider({
initiateGoogleAuth,
handleGoogleCallback,
getPrivateKey: api.fetchPrivateKey,
getPrivateKeyBytes: api.fetchPrivateKeyBytes,
getPublicKey: api.fetchPublicKey,
signMessage: api.signMessage,
aiCustomFetch: aiCustomFetch || (async () => new Response()),
Expand Down
Loading
Loading