ipns
Version:
IPNS record definitions
122 lines (100 loc) • 4.42 kB
text/typescript
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<void> {
// 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<void> {
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')
}
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
}