@neuraiproject/neurai-message
Version:
Sign and Verify messages in Neurai
288 lines (241 loc) • 7.69 kB
text/typescript
import { Buffer } from "buffer";
import createHash from "create-hash";
import { bech32m } from "bech32";
import { ml_dsa44 } from "@noble/post-quantum/ml-dsa.js";
import { signLegacyMessage, verifyLegacyCompactMessage } from "./legacy-message";
const MESSAGE_MAGIC = "Neurai Signed Message:\n";
const PQ_MESSAGE_SIGNATURE_PREFIX = 0x35;
const PQ_SERIALIZED_PUBKEY_PREFIX = 0x05;
const PQ_PUBLIC_KEY_LENGTH = 1312;
const PQ_SERIALIZED_PUBKEY_LENGTH = 1 + PQ_PUBLIC_KEY_LENGTH;
const PQ_SIGNATURE_LENGTH = 2420;
const AUTHSCRIPT_PROGRAM_LENGTH = 32;
const AUTHSCRIPT_DEFAULT_AUTH_TYPE = 0x01;
const AUTHSCRIPT_DOMAIN_SEPARATOR = 0x01;
const AUTHSCRIPT_DEFAULT_WITNESS_SCRIPT = Buffer.from([0x51]); // OP_TRUE
const AUTHSCRIPT_TAG = "NeuraiAuthScript";
const LEGACY_MESSAGE_PREFIX =
String.fromCharCode(Buffer.byteLength(MESSAGE_MAGIC, "utf8")) +
MESSAGE_MAGIC;
function encodeCompactSize(value: number): Buffer {
if (!Number.isInteger(value) || value < 0) {
throw new Error("CompactSize value must be a non-negative integer");
}
if (value < 253) {
return Buffer.from([value]);
}
if (value <= 0xffff) {
const buffer = Buffer.alloc(3);
buffer[0] = 0xfd;
buffer.writeUInt16LE(value, 1);
return buffer;
}
if (value <= 0xffffffff) {
const buffer = Buffer.alloc(5);
buffer[0] = 0xfe;
buffer.writeUInt32LE(value, 1);
return buffer;
}
throw new Error("CompactSize values above uint32 are not supported");
}
function decodeCompactSize(buffer: Buffer, offset: number) {
if (offset >= buffer.length) {
throw new Error("Unexpected end of CompactSize data");
}
const first = buffer[offset];
if (first < 253) {
return { value: first, offset: offset + 1 };
}
if (first === 0xfd) {
if (offset + 3 > buffer.length) {
throw new Error("Unexpected end of CompactSize uint16 data");
}
return { value: buffer.readUInt16LE(offset + 1), offset: offset + 3 };
}
if (first === 0xfe) {
if (offset + 5 > buffer.length) {
throw new Error("Unexpected end of CompactSize uint32 data");
}
return { value: buffer.readUInt32LE(offset + 1), offset: offset + 5 };
}
if (first === 0xff) {
throw new Error("CompactSize uint64 is not supported");
}
throw new Error("Invalid CompactSize prefix");
}
function sha256(bytes: Uint8Array) {
return createHash("sha256").update(bytes).digest();
}
function hash256(bytes: Uint8Array) {
return sha256(sha256(bytes));
}
function hash160(bytes: Uint8Array) {
return createHash("ripemd160").update(sha256(bytes)).digest();
}
function taggedHash(tag: string, bytes: Uint8Array) {
const tagHash = sha256(Buffer.from(tag, "utf8"));
return sha256(Buffer.concat([tagHash, tagHash, Buffer.from(bytes)]));
}
function encodeMessageHash(message: string) {
const messageBytes = Buffer.from(message, "utf8");
const magicBytes = Buffer.from(MESSAGE_MAGIC, "utf8");
const payload = Buffer.concat([
encodeCompactSize(magicBytes.length),
magicBytes,
encodeCompactSize(messageBytes.length),
messageBytes,
]);
return hash256(payload);
}
function toSignatureBuffer(signature: string | Uint8Array) {
return typeof signature === "string"
? Buffer.from(signature, "base64")
: Buffer.from(signature);
}
function normalizePQPublicKey(publicKey: Uint8Array) {
const buffer = Buffer.from(publicKey);
if (
buffer.length === PQ_SERIALIZED_PUBKEY_LENGTH &&
buffer[0] === PQ_SERIALIZED_PUBKEY_PREFIX
) {
return buffer;
}
if (buffer.length === PQ_PUBLIC_KEY_LENGTH) {
return Buffer.concat([Buffer.from([PQ_SERIALIZED_PUBKEY_PREFIX]), buffer]);
}
throw new Error("Invalid PQ public key length");
}
function isPQMessageSignature(signature: string | Uint8Array) {
const buffer = toSignatureBuffer(signature);
return buffer.length > 0 && buffer[0] === PQ_MESSAGE_SIGNATURE_PREFIX;
}
function decodePQAddress(address: string) {
const decoded = bech32m.decode(address);
if (decoded.words.length === 0) {
throw new Error("Invalid bech32m address");
}
return {
prefix: decoded.prefix,
version: decoded.words[0],
program: Buffer.from(bech32m.fromWords(decoded.words.slice(1))),
};
}
function getDefaultPQAuthScriptCommitment(serializedPublicKey: Uint8Array) {
const authDescriptor = Buffer.concat([
Buffer.from([AUTHSCRIPT_DEFAULT_AUTH_TYPE]),
hash160(serializedPublicKey),
]);
const witnessScriptHash = sha256(AUTHSCRIPT_DEFAULT_WITNESS_SCRIPT);
const preimage = Buffer.concat([
Buffer.from([AUTHSCRIPT_DOMAIN_SEPARATOR]),
authDescriptor,
witnessScriptHash,
]);
return taggedHash(AUTHSCRIPT_TAG, preimage);
}
/** returns a base64 encoded string representation of the legacy signature */
export function sign(message: string, privateKey: Uint8Array, compressed = true) {
const signature = signLegacyMessage(
message,
Buffer.from(privateKey),
compressed,
LEGACY_MESSAGE_PREFIX
);
return signature.toString("base64");
}
export function signPQMessage(
message: string,
privateKey: Uint8Array,
publicKey: Uint8Array
) {
const serializedPublicKey = normalizePQPublicKey(publicKey);
const hash = encodeMessageHash(message);
const pqSignature = Buffer.from(ml_dsa44.sign(hash, Buffer.from(privateKey)));
const payload = Buffer.concat([
Buffer.from([PQ_MESSAGE_SIGNATURE_PREFIX]),
encodeCompactSize(serializedPublicKey.length),
serializedPublicKey,
encodeCompactSize(pqSignature.length),
pqSignature,
]);
return payload.toString("base64");
}
export function verifyLegacyMessage(
message: string,
address: string,
signature: string | Uint8Array
) {
try {
return verifyLegacyCompactMessage(
message,
address,
toSignatureBuffer(signature),
LEGACY_MESSAGE_PREFIX
);
} catch {
return false;
}
}
export function verifyPQMessage(
message: string,
address: string,
signature: string | Uint8Array
) {
try {
const payload = toSignatureBuffer(signature);
let offset = 0;
if (payload[offset++] !== PQ_MESSAGE_SIGNATURE_PREFIX) {
return false;
}
const publicKeyLength = decodeCompactSize(payload, offset);
offset = publicKeyLength.offset;
const serializedPublicKey = payload.subarray(
offset,
offset + publicKeyLength.value
);
offset += publicKeyLength.value;
const signatureLength = decodeCompactSize(payload, offset);
offset = signatureLength.offset;
const pqSignature = payload.subarray(offset, offset + signatureLength.value);
offset += signatureLength.value;
if (offset !== payload.length) {
return false;
}
if (
serializedPublicKey.length !== PQ_SERIALIZED_PUBKEY_LENGTH ||
serializedPublicKey[0] !== PQ_SERIALIZED_PUBKEY_PREFIX ||
pqSignature.length !== PQ_SIGNATURE_LENGTH
) {
return false;
}
const decodedAddress = decodePQAddress(address);
if (
decodedAddress.version !== 1 ||
decodedAddress.program.length !== AUTHSCRIPT_PROGRAM_LENGTH
) {
return false;
}
const expectedProgram = getDefaultPQAuthScriptCommitment(
serializedPublicKey
);
if (!expectedProgram.equals(decodedAddress.program)) {
return false;
}
return ml_dsa44.verify(
pqSignature,
encodeMessageHash(message),
serializedPublicKey.subarray(1)
);
} catch {
return false;
}
}
export function verifyMessage(
message: string,
address: string,
signature: string | Uint8Array
) {
return isPQMessageSignature(signature)
? verifyPQMessage(message, address, signature)
: verifyLegacyMessage(message, address, signature);
}