Skip to content

Commit

Permalink
[lightspark-sdk] add remote signing support in lightspark client (#6089)
Browse files Browse the repository at this point in the history
- adds enum for signing key types
- adds property to `NodeKeyCache` to save signing key type
- changes `Requester` to add signing data specific to signing key type
- adds wasm packed `lightspark_crypto` lib
- related PR:
lightsparkdev/lightspark-crypto-uniffi#31
- adds `loadNodeSigningKey` function to client which handles both rsa
and secp key types for OSK and remote signing
- updates documentation to reflect new `loadNodeSigningKey` function
- adds `SigningKeyLoader` classes to handle loading logic

Tested using lightspark-cli
lightsparkdev/webdev#6203

GitOrigin-RevId: 07daa2cd7c01610a65f5a809d37c6e69130d5330
  • Loading branch information
bsiaotickchong authored and Lightspark Eng committed Sep 19, 2023
1 parent 425d1f9 commit 2dd5ba9
Show file tree
Hide file tree
Showing 23 changed files with 1,753 additions and 90 deletions.
2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"dayjs": "^1.11.7",
"graphql": "^16.6.0",
"graphql-ws": "^5.11.3",
"secp256k1": "^5.0.0",
"text-encoding": "^0.7.0",
"ws": "^8.12.1",
"zen-observable-ts": "^1.1.0"
Expand All @@ -76,6 +77,7 @@
"@lightsparkdev/eslint-config": "*",
"@lightsparkdev/tsconfig": "0.0.0",
"@types/crypto-js": "^4.1.1",
"@types/secp256k1": "^4.0.3",
"@types/ws": "^8.5.4",
"eslint": "^8.3.0",
"eslint-watch": "^8.0.0",
Expand Down
55 changes: 45 additions & 10 deletions packages/core/src/crypto/NodeKeyCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ import autoBind from "auto-bind";

import { b64decode } from "../utils/base64.js";
import type { CryptoInterface } from "./crypto.js";
import { DefaultCrypto } from "./crypto.js";
import { DefaultCrypto, LightsparkSigningException } from "./crypto.js";
import type { KeyOrAliasType } from "./KeyOrAlias.js";
import {
RSASigningKey,
Secp256k1SigningKey,
type SigningKey,
} from "./SigningKey.js";
import { SigningKeyType } from "./types.js";

class NodeKeyCache {
private idToKey: Map<string, CryptoKey | string>;
private idToKey: Map<string, SigningKey>;

constructor(private readonly cryptoImpl: CryptoInterface = DefaultCrypto) {
this.idToKey = new Map();
autoBind(this);
Expand All @@ -17,23 +24,51 @@ class NodeKeyCache {
public async loadKey(
id: string,
keyOrAlias: KeyOrAliasType,
): Promise<CryptoKey | string | null> {
signingKeyType: SigningKeyType,
): Promise<SigningKey | null> {
let signingKey: SigningKey;

if (keyOrAlias.alias !== undefined) {
this.idToKey.set(id, keyOrAlias.alias);
return keyOrAlias.alias;
switch (signingKeyType) {
case SigningKeyType.RSASigningKey:
signingKey = new RSASigningKey(
{ alias: keyOrAlias.alias },
this.cryptoImpl,
);
break;
default:
throw new LightsparkSigningException(
`Aliases are not supported for signing key type ${signingKeyType}`,
);
}

this.idToKey.set(id, signingKey);
return signingKey;
}
const decoded = b64decode(this.stripPemTags(keyOrAlias.key));

try {
const key = await this.cryptoImpl.importPrivateSigningKey(decoded);
this.idToKey.set(id, key);
return key;
if (signingKeyType === SigningKeyType.Secp256k1SigningKey) {
signingKey = new Secp256k1SigningKey(keyOrAlias.key);
} else {
const decoded = b64decode(this.stripPemTags(keyOrAlias.key));
const cryptoKeyOrAlias =
await this.cryptoImpl.importPrivateSigningKey(decoded);
const key =
typeof cryptoKeyOrAlias === "string"
? { alias: cryptoKeyOrAlias }
: cryptoKeyOrAlias;
signingKey = new RSASigningKey(key, this.cryptoImpl);
}

this.idToKey.set(id, signingKey);
return signingKey;
} catch (e) {
console.log("Error importing key: ", e);
}
return null;
}

public getKey(id: string): CryptoKey | string | undefined {
public getKey(id: string): SigningKey | undefined {
return this.idToKey.get(id);
}

Expand Down
50 changes: 50 additions & 0 deletions packages/core/src/crypto/SigningKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createHash } from "crypto";
import secp256k1 from "secp256k1";
import { hexToBytes, SigningKeyType, type CryptoInterface } from "../index.js";

interface Alias {
alias: string;
}

function isAlias(key: CryptoKey | Alias): key is Alias {
return "alias" in key;
}

export abstract class SigningKey {
readonly type: SigningKeyType;

constructor(type: SigningKeyType) {
this.type = type;
}

abstract sign(data: Uint8Array): Promise<ArrayBuffer>;
}

export class RSASigningKey extends SigningKey {
constructor(
private readonly privateKey: CryptoKey | Alias,
private readonly cryptoImpl: CryptoInterface,
) {
super(SigningKeyType.RSASigningKey);
}

async sign(data: Uint8Array) {
const key = isAlias(this.privateKey)
? this.privateKey.alias
: this.privateKey;
return this.cryptoImpl.sign(key, data);
}
}

export class Secp256k1SigningKey extends SigningKey {
constructor(private readonly privateKey: string) {
super(SigningKeyType.Secp256k1SigningKey);
}

async sign(data: Uint8Array) {
const keyBytes = new Uint8Array(hexToBytes(this.privateKey));
const hash = createHash("sha256").update(data).digest();
const signResult = secp256k1.ecdsaSign(hash, keyBytes);
return signResult.signature;
}
}
2 changes: 2 additions & 0 deletions packages/core/src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export * from "./crypto.js";
export * from "./KeyOrAlias.js";
export { default as LightsparkSigningException } from "./LightsparkSigningException.js";
export { default as NodeKeyCache } from "./NodeKeyCache.js";
export * from "./SigningKey.js";
export * from "./types.js";
4 changes: 4 additions & 0 deletions packages/core/src/crypto/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum SigningKeyType {
RSASigningKey = "RSASigningKey",
Secp256k1SigningKey = "Secp256k1SigningKey",
}
5 changes: 3 additions & 2 deletions packages/core/src/requester/Requester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,10 @@ class Requester {
const encodedPayload = new TextEncoderImpl().encode(
JSON.stringify(payload),
);
const signedPayload = await this.cryptoImpl.sign(key, encodedPayload);
const encodedSignedPayload = b64encode(signedPayload);

const signedPayload = await key.sign(encodedPayload);

const encodedSignedPayload = b64encode(signedPayload);
headers["X-Lightspark-Signing"] = JSON.stringify({
v: "1",
signature: encodedSignedPayload,
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/utils/hex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const bytesToHex = (bytes: Uint8Array): string => {
return bytes.reduce((acc: string, byte: number) => {
return (acc += ("0" + byte.toString(16)).slice(-2));
}, "");
};

export const hexToBytes = (hex: string): Uint8Array => {
const bytes: number[] = [];

for (let c = 0; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substr(c, 2), 16));
}

return Uint8Array.from(bytes);
};
1 change: 1 addition & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
export * from "./base64.js";
export * from "./currency.js";
export * from "./environment.js";
export * from "./hex.js";
export * from "./types.js";
2 changes: 1 addition & 1 deletion packages/core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": "@lightsparkdev/tsconfig/base.json",
"include": ["src"],
"include": ["src", "src/crypto/types.ts"],
"exclude": ["test", "node_modules", "dist"]
}
12 changes: 6 additions & 6 deletions packages/lightspark-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ const initEnv = async (options: OptionValues) => {

let content = `export ${RequiredCredentials.ClientId}="${clientId}"\n`;
content += `export ${RequiredCredentials.ClientSecret}="${clientSecret}"\n`;
let baseUrl: string | undefined;
let baseApiUrl: string | undefined;
if (options.env === "dev") {
baseUrl = "api.dev.dev.sparkinfra.net";
content += `export LIGHTSPARK_BASE_URL="${baseUrl}"\n`;
baseApiUrl = "api.dev.dev.sparkinfra.net";
content += `export LIGHTSPARK_BASE_URL="${baseApiUrl}" # API url for dev deployment environment\n`;
}

const client = new LightsparkClient(
new AccountTokenAuthProvider(clientId, clientSecret),
baseUrl,
baseApiUrl,
);

let tokenBitcoinNetwork;
Expand Down Expand Up @@ -360,7 +360,7 @@ const payInvoice = async (
"\n",
);

await client.unlockNode(nodeId, nodePassword);
await client.loadNodeSigningKey(nodeId, { password: nodePassword });
const payment = await client.payInvoice(
nodeId,
encodedInvoice,
Expand Down Expand Up @@ -392,7 +392,7 @@ const createTestModePayment = async (
throw new Error("Node password not found in environment.");
}

await client.unlockNode(nodeId, nodePassword);
await client.loadNodeSigningKey(nodeId, { password: nodePassword });
const payment = await client.createTestModePayment(
nodeId,
encodedInvoice,
Expand Down
2 changes: 1 addition & 1 deletion packages/lightspark-sdk/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
extends: ["@lightsparkdev/eslint-config/base"],
ignorePatterns: ["jest.config.ts"],
ignorePatterns: ["jest.config.ts", "lightspark_crypto.js"],
overrides: [
{
files: ["./src/objects/**/*.ts?(x)"],
Expand Down
2 changes: 1 addition & 1 deletion packages/lightspark-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const nodeID = <the node ID of a node to unlock>;
const nodePassword = <the password used to unlock the node>;

try {
await lightsparkClient.unlockNode(nodeID, nodePassword);
await lightsparkClient.loadNodeSigningKey(nodeID, { password: nodePassword });
} catch (e) {
console.error("Failed to unlock node", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ console.log("");
// Let's send the payment.

// First, we need to recover the signing key.
await client.unlockNode(node2Id, credentials.node2Password!);
await client.loadNodeSigningKey(node2Id, { password: credentials.node2Password! });
console.log(`${credentials.node2Name}'s signing key has been loaded.`);

// Then we can send the payment
Expand Down
87 changes: 87 additions & 0 deletions packages/lightspark-sdk/src/NodeKeyLoaderCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
LightsparkSigningException,
type CryptoInterface,
type NodeKeyCache,
type Requester,
type SigningKey,
} from "@lightsparkdev/core";
import {
isMasterSeedSigningKeyLoaderArgs,
isNodeIdAndPasswordSigningKeyLoaderArgs,
MasterSeedSigningKeyLoader,
NodeIdAndPasswordSigningKeyLoader,
type SigningKeyLoader,
type SigningKeyLoaderArgs,
} from "./SigningKeyLoader.js";

/**
* A cache for SigningKeyLoaders associated with nodes.
*/
export default class NodeKeyLoaderCache {
private idToLoader: Map<string, SigningKeyLoader>;

constructor(
private readonly nodeKeyCache: NodeKeyCache,
private readonly cryptoImpl: CryptoInterface = DefaultCrypto,
) {
this.idToLoader = new Map();
}

/**
* Sets the signing key loader for a node.
* Instantiates a signing key loader based on the type of args passed in by the user.
*
* @param nodeId The ID of the node to get the key for
* @param loaderArgs Loader arguments for loading the key
* @param requester Requester used for loading the key
*/
setLoader(
nodeId: string,
loaderArgs: SigningKeyLoaderArgs,
requester: Requester,
) {
let loader: SigningKeyLoader;
if (isNodeIdAndPasswordSigningKeyLoaderArgs(loaderArgs)) {
loader = new NodeIdAndPasswordSigningKeyLoader(
{ nodeId, ...loaderArgs },
requester,
this.cryptoImpl,
);
} else if (isMasterSeedSigningKeyLoaderArgs(loaderArgs)) {
loader = new MasterSeedSigningKeyLoader({ ...loaderArgs });
} else {
throw new LightsparkSigningException("Invalid signing key loader args");
}

this.idToLoader.set(nodeId, loader);
}

/**
* Gets the key for a node using the loader set by [setLoader]
*
* @param id The ID of the node to get the key for
* @returns The loaded key
*/
async getKeyWithLoader(id: string): Promise<SigningKey | undefined> {
if (this.nodeKeyCache.hasKey(id)) {
return this.nodeKeyCache.getKey(id);
}

const loader = this.idToLoader.get(id);
if (!loader) {
throw new LightsparkSigningException(
"No signing key loader found for node " + id,
);
}
const loaderResult = await loader.loadSigningKey();
if (!loaderResult) {
return;
}

return this.nodeKeyCache.loadKey(
id,
{ key: loaderResult.key },
loaderResult.type,
);
}
}
Loading

0 comments on commit 2dd5ba9

Please sign in to comment.