Converted validate module.
This commit is contained in:
11
src/constants.ts
Normal file
11
src/constants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Matches 64-character lowercase hexadecimal strings.
|
||||
* Used for validating event IDs and cryptographic keys.
|
||||
*/
|
||||
export const HEX_64_PATTERN = /^[a-f0-9]{64}$/;
|
||||
|
||||
/**
|
||||
* Matches 128-character lowercase hexadecimal strings.
|
||||
* Used for validating signatures.
|
||||
*/
|
||||
export const HEX_128_PATTERN = /^[a-f0-9]{128}$/;
|
||||
11
src/crypto_init.ts
Normal file
11
src/crypto_init.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Configures @noble/secp256k1 to use synchronous hash functions.
|
||||
* Required for Schnorr signing and verification operations.
|
||||
* Must be imported before any secp256k1 operations execute.
|
||||
*/
|
||||
import { hmac } from "@noble/hashes/hmac.js";
|
||||
import { sha256 } from "@noble/hashes/sha2.js";
|
||||
import { hashes as secp_hashes } from "@noble/secp256k1";
|
||||
|
||||
secp_hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
|
||||
secp_hashes.sha256 = sha256;
|
||||
@@ -1,3 +1,43 @@
|
||||
/**
|
||||
* Public key is not 64 lowercase hex characters.
|
||||
*/
|
||||
export class MalformedPubKeyError extends Error {
|
||||
constructor() {
|
||||
super("public key must be 64 lowercase hex characters");
|
||||
this.name = "MalformedPubKeyError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private key is not 64 lowercase hex characters.
|
||||
*/
|
||||
export class MalformedPrivKeyError extends Error {
|
||||
constructor() {
|
||||
super("private key must be 64 lowercase hex characters");
|
||||
this.name = "MalformedPrivKeyError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event ID is not 64 hex characters.
|
||||
*/
|
||||
export class MalformedIDError extends Error {
|
||||
constructor() {
|
||||
super("event id must be 64 hex characters");
|
||||
this.name = "MalformedIDError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event signature is not 128 hex characters.
|
||||
*/
|
||||
export class MalformedSigError extends Error {
|
||||
constructor() {
|
||||
super("event signature must be 128 hex characters");
|
||||
this.name = "MalformedSigError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event tag contains fewer than two elements.
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { HEX_64_PATTERN } from "./constants";
|
||||
import { Keys } from "./keys";
|
||||
import { testPK, testSK } from "./util.test";
|
||||
|
||||
const HEX_64_PATTERN = /^[a-f0-9]{64}$/;
|
||||
|
||||
describe("Keys.generatePrivate", () => {
|
||||
test("returns 64 hex characters", () => {
|
||||
const privateKey = Keys.generatePrivateKey();
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { hmac } from "@noble/hashes/hmac.js";
|
||||
import { sha256 } from "@noble/hashes/sha2.js";
|
||||
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
||||
import { hashes as secp_hashes } from "@noble/secp256k1";
|
||||
import { schnorr } from "@noble/secp256k1";
|
||||
|
||||
secp_hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
|
||||
secp_hashes.sha256 = sha256;
|
||||
import "./crypto_init";
|
||||
|
||||
/**
|
||||
* Generates a Schnorr signature for the given event ID using the provided private key.
|
||||
|
||||
@@ -1,3 +1,225 @@
|
||||
import { test } from "vitest";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
test("placeholder", () => {});
|
||||
import type { EventData } from "./types";
|
||||
import { testEvent, testPK } from "./util.test";
|
||||
import { EventValidation } from "./validate";
|
||||
|
||||
interface ValidateEventTestCase {
|
||||
name: string;
|
||||
event: EventData;
|
||||
expectedError: string;
|
||||
}
|
||||
|
||||
const structureTestCases: ValidateEventTestCase[] = [
|
||||
{
|
||||
name: "empty pubkey",
|
||||
event: {
|
||||
...testEvent,
|
||||
pubkey: "",
|
||||
},
|
||||
expectedError: "public key must be 64 lowercase hex characters",
|
||||
},
|
||||
{
|
||||
name: "short pubkey",
|
||||
event: {
|
||||
...testEvent,
|
||||
pubkey: "abc123",
|
||||
},
|
||||
expectedError: "public key must be 64 lowercase hex characters",
|
||||
},
|
||||
{
|
||||
name: "long pubkey",
|
||||
event: {
|
||||
...testEvent,
|
||||
pubkey:
|
||||
"c7a702e6158744ca03508bbb4c90f9dbb0d6e88fefbfaa511d5ab24b4e3c48adabc",
|
||||
},
|
||||
expectedError: "public key must be 64 lowercase hex characters",
|
||||
},
|
||||
{
|
||||
name: "non-hex pubkey",
|
||||
event: {
|
||||
...testEvent,
|
||||
pubkey:
|
||||
"zyx-!2e6158744ca03508bbb4c90f9dbb0d6e88fefbfaa511d5ab24b4e3c48ad",
|
||||
},
|
||||
expectedError: "public key must be 64 lowercase hex characters",
|
||||
},
|
||||
{
|
||||
name: "uppercase pubkey",
|
||||
event: {
|
||||
...testEvent,
|
||||
pubkey:
|
||||
"C7A702E6158744CA03508BBB4C90F9DBB0D6E88FEFBFAA511D5AB24B4E3C48AD",
|
||||
},
|
||||
expectedError: "public key must be 64 lowercase hex characters",
|
||||
},
|
||||
{
|
||||
name: "empty id",
|
||||
event: {
|
||||
...testEvent,
|
||||
id: "",
|
||||
},
|
||||
expectedError: "id must be 64 hex characters",
|
||||
},
|
||||
{
|
||||
name: "short id",
|
||||
event: {
|
||||
...testEvent,
|
||||
id: "abc123",
|
||||
},
|
||||
expectedError: "id must be 64 hex characters",
|
||||
},
|
||||
{
|
||||
name: "empty signature",
|
||||
event: {
|
||||
...testEvent,
|
||||
sig: "",
|
||||
},
|
||||
expectedError: "signature must be 128 hex characters",
|
||||
},
|
||||
{
|
||||
name: "short signature",
|
||||
event: {
|
||||
...testEvent,
|
||||
sig: "abc123",
|
||||
},
|
||||
expectedError: "signature must be 128 hex characters",
|
||||
},
|
||||
{
|
||||
name: "empty tag",
|
||||
event: {
|
||||
...testEvent,
|
||||
tags: [[]],
|
||||
},
|
||||
expectedError: "tags must contain at least two elements",
|
||||
},
|
||||
{
|
||||
name: "single element tag",
|
||||
event: {
|
||||
...testEvent,
|
||||
tags: [["a"]],
|
||||
},
|
||||
expectedError: "tags must contain at least two elements",
|
||||
},
|
||||
{
|
||||
name: "one good tag, one single element tag",
|
||||
event: {
|
||||
...testEvent,
|
||||
tags: [["a", "value"], ["b"]],
|
||||
},
|
||||
expectedError: "tags must contain at least two elements",
|
||||
},
|
||||
];
|
||||
|
||||
describe("EventValidation.validateStructure", () => {
|
||||
test.each(structureTestCases)("$name", ({ event, expectedError }) => {
|
||||
expect(() => EventValidation.validateStructure(event)).toThrow(
|
||||
expectedError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("EventValidation.validateID", () => {
|
||||
test("detects ID mismatch", () => {
|
||||
const event: EventData = {
|
||||
...testEvent,
|
||||
id: "7f661c2a3c1ed67dc959d6cd968d743d5e6e334313df44724bca939e2aa42c9e",
|
||||
};
|
||||
expect(() => EventValidation.validateID(event)).toThrow(
|
||||
"does not match computed id",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("EventValidation.validateSignature", () => {
|
||||
test("accepts valid signature", () => {
|
||||
expect(() => EventValidation.validateSignature(testEvent)).not.toThrow();
|
||||
});
|
||||
|
||||
test("rejects invalid signature", () => {
|
||||
const event: EventData = {
|
||||
...testEvent,
|
||||
sig: "9e43cbcf7e828a21c53fa35371ee79bffbfd7a3063ae46fc05ec623dd3186667c57e3d006488015e19247df35eb41c61013e051aa87860e23fa5ffbd44120482",
|
||||
};
|
||||
expect(() => EventValidation.validateSignature(event)).toThrow(
|
||||
"event signature is invalid",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
interface ValidateSignatureTestCase {
|
||||
name: string;
|
||||
id: string;
|
||||
sig: string;
|
||||
pubkey: string;
|
||||
expectedError: string | RegExp;
|
||||
}
|
||||
|
||||
const validateSignatureTestCases: ValidateSignatureTestCase[] = [
|
||||
{
|
||||
name: "bad event id",
|
||||
id: "badeventid",
|
||||
sig: testEvent.sig,
|
||||
pubkey: testEvent.pubkey,
|
||||
expectedError: /hex string expected.*/,
|
||||
},
|
||||
{
|
||||
name: "bad event signature",
|
||||
id: testEvent.id,
|
||||
sig: "badeventsignature",
|
||||
pubkey: testEvent.pubkey,
|
||||
expectedError: /hex string expected.*/,
|
||||
},
|
||||
{
|
||||
name: "bad public key",
|
||||
id: testEvent.id,
|
||||
sig: testEvent.sig,
|
||||
pubkey: "badpublickey",
|
||||
expectedError: /hex string expected.*/,
|
||||
},
|
||||
{
|
||||
name: "malformed event signature",
|
||||
id: testEvent.id,
|
||||
sig: "abc123",
|
||||
pubkey: testEvent.pubkey,
|
||||
expectedError: /"signature" expected.*/,
|
||||
},
|
||||
{
|
||||
name: "malformed public key",
|
||||
id: testEvent.id,
|
||||
sig: testEvent.sig,
|
||||
pubkey: "abc123",
|
||||
expectedError: /"publicKey" expected.*/,
|
||||
},
|
||||
];
|
||||
|
||||
describe("EventValidation.validateSignature - malformed inputs", () => {
|
||||
test.each(validateSignatureTestCases)(
|
||||
"$name",
|
||||
({ id, sig, pubkey, expectedError }) => {
|
||||
const event: EventData = { ...testEvent, id, sig, pubkey };
|
||||
expect(() => EventValidation.validateSignature(event)).toThrow(
|
||||
expectedError,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("EventValidation.validate", () => {
|
||||
test("validates complete event", () => {
|
||||
const event: EventData = {
|
||||
id: "c9a0f84fcaa889654da8992105eb122eb210c8cbd58210609a5ef7e170b51400",
|
||||
pubkey: testPK,
|
||||
created_at: testEvent.created_at,
|
||||
kind: testEvent.kind,
|
||||
tags: [
|
||||
["a", "value"],
|
||||
["b", "value", "optional"],
|
||||
],
|
||||
content: "valid event",
|
||||
sig: "668a715f1eb983172acf230d17bd283daedb2598adf8de4290bcc7eb0b802fdb60669d1e7d1104ac70393f4dbccd07e8abf897152af6ce6c0a75499874e27f14",
|
||||
};
|
||||
expect(() => EventValidation.validate(event)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
101
src/validate.ts
101
src/validate.ts
@@ -0,0 +1,101 @@
|
||||
import { hexToBytes } from "@noble/hashes/utils.js";
|
||||
import { schnorr } from "@noble/secp256k1";
|
||||
|
||||
import { HEX_64_PATTERN, HEX_128_PATTERN } from "./constants";
|
||||
import "./crypto_init";
|
||||
import {
|
||||
FailedIDCompError,
|
||||
InvalidSigError,
|
||||
MalformedIDError,
|
||||
MalformedPubKeyError,
|
||||
MalformedSigError,
|
||||
MalformedTagError,
|
||||
NoEventIDError,
|
||||
} from "./errors";
|
||||
import { EventID } from "./id";
|
||||
import type { EventData } from "./types";
|
||||
|
||||
/**
|
||||
* Checks event field formats and lengths conform to protocol specification.
|
||||
* @throws {MalformedPubKeyError} If pubkey is not 64 lowercase hex characters
|
||||
* @throws {MalformedIDError} If id is not 64 hex characters
|
||||
* @throws {MalformedSigError} If sig is not 128 hex characters
|
||||
* @throws {MalformedTagError} If any tag has fewer than 2 elements
|
||||
*/
|
||||
function validateStructure(event: EventData): void {
|
||||
if (!HEX_64_PATTERN.test(event.pubkey)) {
|
||||
throw new MalformedPubKeyError();
|
||||
}
|
||||
|
||||
if (!HEX_64_PATTERN.test(event.id)) {
|
||||
throw new MalformedIDError();
|
||||
}
|
||||
|
||||
if (!HEX_128_PATTERN.test(event.sig)) {
|
||||
throw new MalformedSigError();
|
||||
}
|
||||
|
||||
for (const tag of event.tags) {
|
||||
if (tag.length < 2) {
|
||||
throw new MalformedTagError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the event ID matches the computed hash of the serialized event.
|
||||
* @throws {FailedIDCompError} If ID computation fails
|
||||
* @throws {NoEventIDError} If event.id is empty
|
||||
* @throws {Error} If computed ID does not match stored ID
|
||||
*/
|
||||
function validateID(event: EventData): void {
|
||||
let computedID: string;
|
||||
try {
|
||||
computedID = EventID.getID(event);
|
||||
} catch (err) {
|
||||
throw new FailedIDCompError();
|
||||
}
|
||||
|
||||
if (event.id === "") {
|
||||
throw new NoEventIDError();
|
||||
}
|
||||
|
||||
if (computedID !== event.id) {
|
||||
throw new Error(
|
||||
`event id "${event.id}" does not match computed id "${computedID}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the cryptographic signature using Schnorr verification.
|
||||
* @throws {InvalidSigError} If signature verification fails
|
||||
*/
|
||||
function validateSignature(event: EventData): void {
|
||||
const idBytes = hexToBytes(event.id);
|
||||
const sigBytes = hexToBytes(event.sig);
|
||||
const pubkeyBytes = hexToBytes(event.pubkey);
|
||||
|
||||
const isValid = schnorr.verify(sigBytes, idBytes, pubkeyBytes);
|
||||
|
||||
if (!isValid) {
|
||||
throw new InvalidSigError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs complete event validation: structure, ID, and signature.
|
||||
* @throws First validation error encountered
|
||||
*/
|
||||
function validate(event: EventData): void {
|
||||
validateStructure(event);
|
||||
validateID(event);
|
||||
validateSignature(event);
|
||||
}
|
||||
|
||||
export const EventValidation = {
|
||||
validate,
|
||||
validateStructure,
|
||||
validateID,
|
||||
validateSignature,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user