import { publicKeyFromMultihash } from '@libp2p/crypto/keys' import { logger } from '@libp2p/logger' import NanoDate from 'timestamp-nano' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' import { InvalidEmbeddedPublicKeyError, RecordExpiredError, RecordTooLargeError, SignatureVerificationError, UnsupportedValidityError } from './errors.ts' import { IpnsEntry } from './pb/ipns.ts' import { extractPublicKeyFromIPNSRecord, ipnsRecordDataForV2Sig, isCodec, multihashFromIPNSRoutingKey, multihashToIPNSRoutingKey, unmarshalIPNSRecord } from './utils.ts' import type { IPNSRecord } from './index.ts' import type { PublicKey } from '@libp2p/interface' const log = logger('ipns:validator') /** * Limit valid IPNS record sizes to 10kb */ const MAX_RECORD_SIZE = 1024 * 10 /** * Validates the given IPNS Record against the given public key. We need a "raw" * record in order to be able to access to all of its fields. */ export async function validate (publicKey: PublicKey, marshalledRecord: Uint8Array): Promise { // unmarshal ensures that (1) SignatureV2 and Data are present, (2) that ValidityType // and Validity are of valid types and have a value, (3) that CBOR data matches protobuf // if it's a V1+V2 record. const record = unmarshalIPNSRecord(marshalledRecord) // Validate Signature V2 let isValid try { const dataForSignature = ipnsRecordDataForV2Sig(record.data) isValid = await publicKey.verify(dataForSignature, record.signatureV2) } catch (err) { isValid = false } if (!isValid) { log.error('record signature verification failed') throw new SignatureVerificationError('Record signature verification failed') } // Validate according to the validity type if (record.validityType === IpnsEntry.ValidityType.EOL) { if (NanoDate.fromString(record.validity).toDate().getTime() < Date.now()) { log.error('record has expired') throw new RecordExpiredError('record has expired') } } else if (record.validityType != null) { log.error('the validity type is unsupported') throw new UnsupportedValidityError('The validity type is unsupported') } log('ipns record for %s is valid', record.value) } /** * Validate the given IPNS record against the given routing key. * * @see https://specs.ipfs.tech/ipns/ipns-record/#routing-record for the binary format of the routing key * * @param routingKey - The routing key in binary format: binary(ascii(IPNS_PREFIX) + multihash(public key)) * @param marshalledRecord - The marshalled record to validate. */ export async function ipnsValidator (routingKey: Uint8Array, marshalledRecord: Uint8Array): Promise { if (marshalledRecord.byteLength > MAX_RECORD_SIZE) { throw new RecordTooLargeError('The record is too large') } // try to extract public key from routing key const routingMultihash = multihashFromIPNSRoutingKey(routingKey) let routingPubKey: PublicKey | undefined // identity hash if (isCodec(routingMultihash, 0x0)) { routingPubKey = publicKeyFromMultihash(routingMultihash) } // extract public key from record const receivedRecord = unmarshalIPNSRecord(marshalledRecord) const recordPubKey = extractPublicKeyFromIPNSRecord(receivedRecord) ?? routingPubKey if (recordPubKey == null) { throw new InvalidEmbeddedPublicKeyError('Could not extract public key from IPNS record or routing key') } // @ts-expect-error @libp2p/crypto needs a new multiformats const expectedRoutingKey = multihashToIPNSRoutingKey(recordPubKey.toMultihash()) if (!uint8ArrayEquals(expectedRoutingKey, routingKey)) { throw new InvalidEmbeddedPublicKeyError('Embedded public key did not match routing key') } // Record validation await validate(recordPubKey, marshalledRecord) } /** * Returns the number of milliseconds until the record expires. * If the record is already expired, returns 0. * * @param record - The IPNS record to validate. * @returns The number of milliseconds until the record expires, or 0 if the record is already expired. */ export function validFor (record: IPNSRecord): number { if (record.validityType !== IpnsEntry.ValidityType.EOL) { throw new UnsupportedValidityError() } if (record.validity == null) { throw new UnsupportedValidityError() } const validUntil = NanoDate.fromString(record.validity).toDate().getTime() const now = Date.now() if (validUntil < now) { return 0 } return validUntil - now }