lotus-sdk
Version:
Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem
218 lines (217 loc) • 8.68 kB
JavaScript
import { sha256 } from '@noble/hashes/sha256';
import { bytesToHex } from '@noble/hashes/utils';
import { DiscoverySecurityValidator } from '../discovery/security.js';
import { isValidSignerAdvertisement, isValidSigningRequestAdvertisement, publicKeyToHex, } from './discovery-types.js';
export class MuSig2DiscoverySecurityValidator extends DiscoverySecurityValidator {
async validateAdvertisement(advertisement, criteria) {
const baseResult = await super.validateAdvertisement(advertisement, criteria);
if (!baseResult.valid) {
return baseResult;
}
if (advertisement.protocol === 'musig2') {
return this.validateSignerAdvertisement(advertisement);
}
else if (advertisement.protocol === 'musig2-request') {
return this.validateSigningRequestAdvertisement(advertisement);
}
return baseResult;
}
validateSignerAdvertisement(advertisement) {
if (!isValidSignerAdvertisement(advertisement)) {
return {
valid: false,
error: 'Invalid signer advertisement structure',
securityScore: 0,
details: {
signatureValid: false,
notExpired: true,
reputationAcceptable: false,
criteriaMatch: false,
customValidation: false,
},
};
}
const details = {
signatureValid: true,
notExpired: true,
reputationAcceptable: true,
criteriaMatch: true,
customValidation: true,
};
let securityScore = 100;
if (!advertisement.publicKey ||
typeof advertisement.publicKey !== 'object') {
details.customValidation = false;
securityScore -= 30;
}
if (!Array.isArray(advertisement.transactionTypes) ||
advertisement.transactionTypes.length === 0) {
details.customValidation = false;
securityScore -= 20;
}
if (advertisement.amountRange &&
advertisement.amountRange.min !== undefined &&
advertisement.amountRange.max !== undefined &&
advertisement.amountRange.min > advertisement.amountRange.max) {
details.customValidation = false;
securityScore -= 10;
}
if (advertisement.signature && advertisement.signature.length > 0) {
const isValid = this.verifySignerSignature(advertisement);
if (!isValid) {
details.signatureValid = false;
securityScore -= 40;
}
}
const minScore = 50;
const valid = securityScore >= minScore && details.customValidation;
return {
valid,
error: valid ? undefined : 'MuSig2 signer validation failed',
securityScore,
details,
};
}
validateSigningRequestAdvertisement(advertisement) {
if (!isValidSigningRequestAdvertisement(advertisement)) {
return {
valid: false,
error: 'Invalid signing request advertisement structure',
securityScore: 0,
details: {
signatureValid: false,
notExpired: true,
reputationAcceptable: false,
criteriaMatch: false,
customValidation: false,
},
};
}
const details = {
signatureValid: true,
notExpired: true,
reputationAcceptable: true,
criteriaMatch: true,
customValidation: true,
};
let securityScore = 100;
if (!advertisement.requestId ||
typeof advertisement.requestId !== 'string') {
details.customValidation = false;
securityScore -= 20;
}
if (!Array.isArray(advertisement.requiredPublicKeys) ||
advertisement.requiredPublicKeys.length === 0) {
details.customValidation = false;
securityScore -= 30;
}
if (!advertisement.messageHash ||
typeof advertisement.messageHash !== 'string') {
details.customValidation = false;
securityScore -= 30;
}
if (!advertisement.creatorPeerId ||
typeof advertisement.creatorPeerId !== 'string') {
details.customValidation = false;
securityScore -= 10;
}
if (advertisement.creatorSignature &&
advertisement.creatorSignature.length > 0) {
const isValid = this.verifyRequestSignature(advertisement);
if (!isValid) {
details.signatureValid = false;
securityScore -= 40;
}
}
const minScore = 50;
const valid = securityScore >= minScore && details.customValidation;
return {
valid,
error: valid ? undefined : 'MuSig2 signing request validation failed',
securityScore,
details,
};
}
verifySignerSignature(advertisement) {
if (!advertisement.signature || advertisement.signature.length === 0) {
return true;
}
try {
const publicKeyHex = publicKeyToHex(advertisement.publicKey);
const message = this.constructSignerMessage(advertisement.id, publicKeyHex, advertisement.transactionTypes, advertisement.createdAt);
return true;
}
catch (error) {
return false;
}
}
verifyRequestSignature(advertisement) {
if (!advertisement.creatorSignature ||
advertisement.creatorSignature.length === 0) {
return true;
}
try {
const message = this.constructRequestMessage(advertisement.requestId, advertisement.requiredPublicKeys, advertisement.messageHash, advertisement.createdAt);
return true;
}
catch (error) {
return false;
}
}
constructSignerMessage(id, publicKeyHex, transactionTypes, timestamp) {
const data = `musig2-signer${id}${publicKeyHex}${transactionTypes.sort().join(',')}${timestamp.toString()}`;
return bytesToHex(sha256(new TextEncoder().encode(data)));
}
constructRequestMessage(requestId, requiredPublicKeys, messageHash, timestamp) {
const data = `musig2-request${requestId}${requiredPublicKeys.sort().join(',')}${messageHash}${timestamp.toString()}`;
return bytesToHex(sha256(new TextEncoder().encode(data)));
}
async validateBurnIdentity(advertisement, minBurnAmount, minMaturationBlocks) {
const identity = advertisement.signerMetadata?.identity;
if (!identity) {
return false;
}
if (identity.totalBurned < minBurnAmount) {
return false;
}
if (identity.maturationBlocks < minMaturationBlocks) {
return false;
}
return true;
}
validateRequiredSigner(publicKey, requiredPublicKeys) {
const publicKeyHex = typeof publicKey === 'string' ? publicKey : publicKeyToHex(publicKey);
return requiredPublicKeys.includes(publicKeyHex);
}
validateTransactionTypeSupport(advertisement, requiredType) {
return advertisement.transactionTypes.some(type => type === requiredType);
}
validateAmountRange(advertisement, requiredAmount) {
if (!advertisement.amountRange) {
return true;
}
const { min, max } = advertisement.amountRange;
if (min !== undefined && requiredAmount < min) {
return false;
}
if (max !== undefined && requiredAmount > max) {
return false;
}
return true;
}
}
export function createMuSig2SecurityPolicy(options) {
return {
minReputation: options?.minReputation ?? 50,
maxAdvertisementAge: options?.maxAdvertisementAge ?? 30 * 60 * 1000,
enableSignatureVerification: options?.enableSignatureVerification ?? true,
enableReplayPrevention: options?.enableReplayPrevention ?? true,
enableRateLimiting: options?.enableRateLimiting ?? true,
rateLimits: {
maxAdvertisementsPerPeer: options?.rateLimits?.maxAdvertisementsPerPeer ?? 5,
maxDiscoveryQueriesPerPeer: options?.rateLimits?.maxDiscoveryQueriesPerPeer ?? 20,
windowSizeMs: options?.rateLimits?.windowSizeMs ?? 60 * 1000,
},
customValidators: options?.customValidators ?? [],
};
}