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.
|
* Event tag contains fewer than two elements.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { HEX_64_PATTERN } from "./constants";
|
||||||
import { Keys } from "./keys";
|
import { Keys } from "./keys";
|
||||||
import { testPK, testSK } from "./util.test";
|
import { testPK, testSK } from "./util.test";
|
||||||
|
|
||||||
const HEX_64_PATTERN = /^[a-f0-9]{64}$/;
|
|
||||||
|
|
||||||
describe("Keys.generatePrivate", () => {
|
describe("Keys.generatePrivate", () => {
|
||||||
test("returns 64 hex characters", () => {
|
test("returns 64 hex characters", () => {
|
||||||
const privateKey = Keys.generatePrivateKey();
|
const privateKey = Keys.generatePrivateKey();
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { hmac } from "@noble/hashes/hmac.js";
|
|
||||||
import { sha256 } from "@noble/hashes/sha2.js";
|
import { sha256 } from "@noble/hashes/sha2.js";
|
||||||
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
||||||
import { hashes as secp_hashes } from "@noble/secp256k1";
|
|
||||||
import { schnorr } from "@noble/secp256k1";
|
import { schnorr } from "@noble/secp256k1";
|
||||||
|
|
||||||
secp_hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
|
import "./crypto_init";
|
||||||
secp_hashes.sha256 = sha256;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a Schnorr signature for the given event ID using the provided private key.
|
* 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