2022-02-04 14:12:00 +11:00
|
|
|
import assert from "assert";
|
2022-01-13 11:33:26 +11:00
|
|
|
|
2022-02-04 14:12:00 +11:00
|
|
|
import * as base32 from "hi-base32";
|
|
|
|
|
import { ecdsaVerify } from "secp256k1";
|
2022-01-13 11:33:26 +11:00
|
|
|
|
2022-02-04 14:12:00 +11:00
|
|
|
import { ENR } from "../enr";
|
2022-02-16 14:08:48 +11:00
|
|
|
import { utf8ToBytes } from "../utf8";
|
2022-02-16 12:11:54 +11:00
|
|
|
import { base64ToBytes, keccak256Buf } from "../utils";
|
2022-01-13 11:33:26 +11:00
|
|
|
|
2022-01-13 16:09:01 +11:00
|
|
|
export type ENRRootValues = {
|
2022-01-13 11:33:26 +11:00
|
|
|
eRoot: string;
|
|
|
|
|
lRoot: string;
|
|
|
|
|
seq: number;
|
|
|
|
|
signature: string;
|
|
|
|
|
};
|
|
|
|
|
|
2022-01-13 16:09:01 +11:00
|
|
|
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 {
|
|
|
|
|
assert(
|
|
|
|
|
root.startsWith(this.ROOT_PREFIX),
|
|
|
|
|
`ENRTree root entry must start with '${this.ROOT_PREFIX}'`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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);
|
2022-02-16 12:11:54 +11:00
|
|
|
const signatureBuffer = base64ToBytes(rootValues.signature).slice(0, 64);
|
2022-01-13 11:33:26 +11:00
|
|
|
|
|
|
|
|
const isVerified = ecdsaVerify(
|
|
|
|
|
signatureBuffer,
|
|
|
|
|
keccak256Buf(signedComponentBuffer),
|
2022-02-16 12:11:54 +11:00
|
|
|
new Uint8Array(decodedPublicKey)
|
2022-01-13 11:33:26 +11:00
|
|
|
);
|
|
|
|
|
|
2022-02-04 14:12:00 +11:00
|
|
|
assert(isVerified, "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-02-04 14:12:00 +11:00
|
|
|
assert.ok(Array.isArray(matches), "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;
|
|
|
|
|
|
|
|
|
|
assert.ok(eRoot, "Could not parse 'e' value from ENRTree root entry");
|
|
|
|
|
assert.ok(lRoot, "Could not parse 'l' value from ENRTree root entry");
|
|
|
|
|
assert.ok(seq, "Could not parse 'seq' value from ENRTree root entry");
|
|
|
|
|
assert.ok(signature, "Could not parse 'sig' value from ENRTree root entry");
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
assert(
|
|
|
|
|
tree.startsWith(this.TREE_PREFIX),
|
|
|
|
|
`ENRTree tree entry must start with '${this.TREE_PREFIX}'`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const matches = tree.match(/^enrtree:\/\/([^@]+)@(.+)$/);
|
|
|
|
|
|
2022-02-04 14:12:00 +11:00
|
|
|
assert.ok(Array.isArray(matches), "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-02-04 14:12:00 +11:00
|
|
|
assert.ok(publicKey, "Could not parse public key from ENRTree tree entry");
|
|
|
|
|
assert.ok(domain, "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[] {
|
|
|
|
|
assert(
|
|
|
|
|
branch.startsWith(this.BRANCH_PREFIX),
|
|
|
|
|
`ENRTree branch entry must start with '${this.BRANCH_PREFIX}'`
|
|
|
|
|
);
|
|
|
|
|
|
2022-02-04 14:12:00 +11:00
|
|
|
return branch.split(this.BRANCH_PREFIX)[1].split(",");
|
2022-01-13 11:33:26 +11:00
|
|
|
}
|
|
|
|
|
}
|