176 lines
5.1 KiB
TypeScript
176 lines
5.1 KiB
TypeScript
import * as bindings from "mtp/raw";
|
|
|
|
export type MTPSignatureVerificationPolicy =
|
|
| "ed25519"
|
|
| "dual"
|
|
| "any-supported";
|
|
|
|
/**
|
|
* The SDK default is deliberately fixed. Senders also default to the
|
|
* interoperable Ed25519 suite; dual signatures require an explicit sender
|
|
* suite and receiver policy.
|
|
*/
|
|
export const DEFAULT_SIGNATURE_VERIFICATION_POLICY: MTPSignatureVerificationPolicy =
|
|
"ed25519";
|
|
|
|
export type MTPSignatureVerificationErrorCode =
|
|
| "unsupported-suite"
|
|
| "policy-rejected"
|
|
| "invalid-signature"
|
|
| "signer-keys-unavailable";
|
|
|
|
const POLICY_NAMES: Record<
|
|
MTPSignatureVerificationErrorCode,
|
|
string
|
|
> = {
|
|
"unsupported-suite": "unsupported signature suite",
|
|
"policy-rejected": "signature rejected by policy",
|
|
"invalid-signature": "signature cryptographically invalid",
|
|
"signer-keys-unavailable": "signer public keys unavailable",
|
|
};
|
|
|
|
/** Caller-facing signature verification failure without cryptographic detail. */
|
|
export class MTPSignatureVerificationError extends Error {
|
|
readonly code: MTPSignatureVerificationErrorCode;
|
|
readonly signerId?: bigint;
|
|
|
|
constructor(
|
|
code: MTPSignatureVerificationErrorCode,
|
|
signerId?: bigint,
|
|
) {
|
|
super(
|
|
signerId == null
|
|
? POLICY_NAMES[code]
|
|
: `${POLICY_NAMES[code]} for signer ${signerId}`,
|
|
);
|
|
this.name = "MTPSignatureVerificationError";
|
|
this.code = code;
|
|
this.signerId = signerId;
|
|
}
|
|
}
|
|
|
|
function validPolicy(
|
|
value: unknown,
|
|
): value is MTPSignatureVerificationPolicy {
|
|
return (
|
|
value === "ed25519" ||
|
|
value === "dual" ||
|
|
value === "any-supported"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolve receiver policy in operation, client, library order.
|
|
*/
|
|
export function resolveSignatureVerificationPolicy(
|
|
operationPolicy: MTPSignatureVerificationPolicy | undefined,
|
|
clientDefaultPolicy?: MTPSignatureVerificationPolicy,
|
|
): MTPSignatureVerificationPolicy {
|
|
if (operationPolicy != null && !validPolicy(operationPolicy)) {
|
|
throw new TypeError(
|
|
"signaturePolicy must be 'ed25519', 'dual', or 'any-supported'",
|
|
);
|
|
}
|
|
if (clientDefaultPolicy != null && !validPolicy(clientDefaultPolicy)) {
|
|
throw new TypeError(
|
|
"defaultSignatureVerificationPolicy must be 'ed25519', 'dual', or 'any-supported'",
|
|
);
|
|
}
|
|
return (
|
|
operationPolicy ??
|
|
clientDefaultPolicy ??
|
|
DEFAULT_SIGNATURE_VERIFICATION_POLICY
|
|
);
|
|
}
|
|
|
|
/** Convert the SDK policy into the raw WASM verifier's policy value. */
|
|
export function signatureVerificationPolicyValue(
|
|
policy: MTPSignatureVerificationPolicy,
|
|
): number {
|
|
switch (policy) {
|
|
case "ed25519":
|
|
return bindings.mtp_protection_signature_suite_ed25519();
|
|
case "dual":
|
|
return bindings.mtp_protection_signature_suite_dual();
|
|
case "any-supported": {
|
|
const compatibility = (
|
|
bindings as typeof bindings & {
|
|
mtp_protection_signature_suite_any_supported?: () => number;
|
|
}
|
|
).mtp_protection_signature_suite_any_supported;
|
|
return compatibility?.() ?? 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Verify one protected value and sanitize raw WASM failure details. */
|
|
export function verifyDataValueWithPolicy(
|
|
value: Uint8Array,
|
|
publicKeyBundle: Uint8Array,
|
|
expectedSignerId: bigint,
|
|
expectedPurpose: number,
|
|
policy: MTPSignatureVerificationPolicy,
|
|
): void {
|
|
try {
|
|
bindings.verify_data_value_with_policy(
|
|
value,
|
|
publicKeyBundle,
|
|
expectedSignerId,
|
|
expectedPurpose,
|
|
signatureVerificationPolicyValue(policy),
|
|
);
|
|
} catch (error) {
|
|
throw classifySignatureVerificationFailure(error, expectedSignerId);
|
|
}
|
|
}
|
|
|
|
function rawErrorMessage(error: unknown): string {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|
|
|
|
/** Classify a raw verifier failure without returning its cryptographic cause. */
|
|
export function classifySignatureVerificationFailure(
|
|
error: unknown,
|
|
signerId?: bigint,
|
|
): MTPSignatureVerificationError {
|
|
const message = rawErrorMessage(error).toLowerCase();
|
|
if (message.includes("unknown") && message.includes("signature suite")) {
|
|
return new MTPSignatureVerificationError("unsupported-suite", signerId);
|
|
}
|
|
if (message.includes("policy")) {
|
|
return new MTPSignatureVerificationError("policy-rejected", signerId);
|
|
}
|
|
return new MTPSignatureVerificationError("invalid-signature", signerId);
|
|
}
|
|
|
|
/** Select the most useful sanitized error after trying key history. */
|
|
export function signatureVerificationFailure(
|
|
errors: readonly unknown[],
|
|
signerId?: bigint,
|
|
): MTPSignatureVerificationError {
|
|
const classified = errors.map((error) =>
|
|
error instanceof MTPSignatureVerificationError
|
|
? error
|
|
: classifySignatureVerificationFailure(error, signerId),
|
|
);
|
|
const preferredCode = [
|
|
"unsupported-suite",
|
|
"policy-rejected",
|
|
"invalid-signature",
|
|
].find((code) =>
|
|
classified.some((error) => error.code === code),
|
|
) as MTPSignatureVerificationErrorCode | undefined;
|
|
return new MTPSignatureVerificationError(
|
|
preferredCode ?? "invalid-signature",
|
|
signerId,
|
|
);
|
|
}
|
|
|
|
export function signerKeysUnavailable(
|
|
signerId?: bigint,
|
|
): MTPSignatureVerificationError {
|
|
return new MTPSignatureVerificationError(
|
|
"signer-keys-unavailable",
|
|
signerId,
|
|
);
|
|
}
|