js-waku/src/lib/discovery/enrtree.ts

131 lines
4.1 KiB
TypeScript
Raw Normal View History

2022-03-07 13:50:18 +11:00
import * as secp from "@noble/secp256k1";
2022-02-04 14:12:00 +11:00
import * as base32 from "hi-base32";
import { fromString } from "uint8arrays/from-string";
2022-01-13 11:33:26 +11:00
2022-05-20 11:49:00 +10:00
import { keccak256 } from "../crypto";
2022-02-04 14:12:00 +11:00
import { ENR } from "../enr";
2022-05-20 11:49:00 +10:00
import { utf8ToBytes } from "../utils";
2022-01-13 11:33:26 +11:00
export type ENRRootValues = {
2022-01-13 11:33:26 +11:00
eRoot: string;
lRoot: string;
seq: number;
signature: string;
};
export type ENRTreeValues = {
2022-01-13 11:33:26 +11:00
publicKey: string;
domain: string;
};
export class ENRTree {
public static readonly RECORD_PREFIX = ENR.RECORD_PREFIX;
2022-02-04 14:12:00 +11:00
public static readonly TREE_PREFIX = "enrtree:";
public static readonly BRANCH_PREFIX = "enrtree-branch:";
public static readonly ROOT_PREFIX = "enrtree-root:";
2022-01-13 11:33:26 +11:00
/**
* Extracts the branch subdomain referenced by a DNS tree root string after verifying
* the root record signature with its base32 compressed public key.
*/
static parseAndVerifyRoot(root: string, publicKey: string): string {
2022-05-12 15:16:15 +10:00
if (!root.startsWith(this.ROOT_PREFIX))
throw new Error(
`ENRTree root entry must start with '${this.ROOT_PREFIX}'`
);
2022-01-13 11:33:26 +11:00
const rootValues = ENRTree.parseRootValues(root);
const decodedPublicKey = base32.decode.asBytes(publicKey);
// The signature is a 65-byte secp256k1 over the keccak256 hash
// of the record content, excluding the `sig=` part, encoded as URL-safe base64 string
// (Trailing recovery bit must be trimmed to pass `ecdsaVerify` method)
2022-02-04 14:12:00 +11:00
const signedComponent = root.split(" sig")[0];
2022-02-16 14:08:48 +11:00
const signedComponentBuffer = utf8ToBytes(signedComponent);
const signatureBuffer = fromString(rootValues.signature, "base64url").slice(
0,
64
);
2022-01-13 11:33:26 +11:00
2022-03-07 13:50:18 +11:00
let isVerified;
try {
const _sig = secp.Signature.fromCompact(signatureBuffer.slice(0, 64));
isVerified = secp.verify(
_sig,
2022-05-20 11:49:00 +10:00
keccak256(signedComponentBuffer),
2022-03-07 13:50:18 +11:00
new Uint8Array(decodedPublicKey)
);
} catch {
isVerified = false;
}
2022-01-13 11:33:26 +11:00
2022-05-12 15:16:15 +10:00
if (!isVerified) throw new Error("Unable to verify ENRTree root signature");
2022-01-13 11:33:26 +11:00
return rootValues.eRoot;
}
static parseRootValues(txt: string): ENRRootValues {
const matches = txt.match(
/^enrtree-root:v1 e=([^ ]+) l=([^ ]+) seq=(\d+) sig=([^ ]+)$/
);
2022-05-12 15:16:15 +10:00
if (!Array.isArray(matches))
throw new Error("Could not parse ENRTree root entry");
2022-01-13 11:33:26 +11:00
matches.shift(); // The first entry is the full match
const [eRoot, lRoot, seq, signature] = matches;
2022-05-12 15:16:15 +10:00
if (!eRoot)
throw new Error("Could not parse 'e' value from ENRTree root entry");
if (!lRoot)
throw new Error("Could not parse 'l' value from ENRTree root entry");
if (!seq)
throw new Error("Could not parse 'seq' value from ENRTree root entry");
if (!signature)
throw new Error("Could not parse 'sig' value from ENRTree root entry");
2022-01-13 11:33:26 +11:00
return { eRoot, lRoot, seq: Number(seq), signature };
}
/**
* Returns the public key and top level domain of an ENR tree entry.
* The domain is the starting point for traversing a set of linked DNS TXT records
* and the public key is used to verify the root entry record
*/
static parseTree(tree: string): ENRTreeValues {
2022-05-12 15:16:15 +10:00
if (!tree.startsWith(this.TREE_PREFIX))
throw new Error(
`ENRTree tree entry must start with '${this.TREE_PREFIX}'`
);
2022-01-13 11:33:26 +11:00
const matches = tree.match(/^enrtree:\/\/([^@]+)@(.+)$/);
2022-05-12 15:16:15 +10:00
if (!Array.isArray(matches))
throw new Error("Could not parse ENRTree tree entry");
2022-01-13 11:33:26 +11:00
matches.shift(); // The first entry is the full match
const [publicKey, domain] = matches;
2022-05-12 15:16:15 +10:00
if (!publicKey)
throw new Error("Could not parse public key from ENRTree tree entry");
if (!domain)
throw new Error("Could not parse domain from ENRTree tree entry");
2022-01-13 11:33:26 +11:00
return { publicKey, domain };
}
/**
* Returns subdomains listed in an ENR branch entry. These in turn lead to
* either further branch entries or ENR records.
*/
static parseBranch(branch: string): string[] {
2022-05-12 15:16:15 +10:00
if (!branch.startsWith(this.BRANCH_PREFIX))
throw new Error(
`ENRTree branch entry must start with '${this.BRANCH_PREFIX}'`
);
2022-01-13 11:33:26 +11:00
2022-02-04 14:12:00 +11:00
return branch.split(this.BRANCH_PREFIX)[1].split(",");
2022-01-13 11:33:26 +11:00
}
}