mtp/src/sdk/signature-policy.ts

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,
);
}