From 975eacaf7adf43bb4383c7b0318d9af860bbe508 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Wed, 11 Sep 2024 16:27:52 +0100 Subject: [PATCH 1/3] fix!: update to libp2p@2.x.x deps - Updates method names so they can be invidually imported but still be legible - Operates on PrivateKey/PublicKeys instead of PeerIds to integrate with the libp2p@2.x.x keychain will less friction BREAKING CHANGE: uses libp2p@2.x.x deps, operates on PrivateKey/PublicKeys instead of PeerIds --- README.md | 15 --- package.json | 24 +++-- src/errors.ts | 84 ++++++++++++++--- src/index.ts | 85 ++++++----------- src/pb/ipns.ts | 39 +++++--- src/selector.ts | 4 +- src/utils.ts | 96 ++++++++++--------- src/validator.ts | 41 +++++--- test/conformance.spec.ts | 48 ++++++---- test/index.spec.ts | 197 ++++++++++++++++++++------------------- test/selector.spec.ts | 31 +++--- test/validator.spec.ts | 74 +++++++-------- 12 files changed, 391 insertions(+), 347 deletions(-) diff --git a/README.md b/README.md index a5d9d17..f8d61c3 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ - [Validate record](#validate-record) - [Embed public key to record](#embed-public-key-to-record) - [Extract public key from record](#extract-public-key-from-record) - - [Datastore key](#datastore-key) - [Marshal data with proto buffer](#marshal-data-with-proto-buffer) - [Unmarshal data from proto buffer](#unmarshal-data-from-proto-buffer) - [Validator](#validator) @@ -82,20 +81,6 @@ import * as ipns from 'ipns' const publicKey = await ipns.extractPublicKey(peerId, ipnsRecord) ``` -### Datastore key - -```js -import * as ipns from 'ipns' - -ipns.getLocalKey(peerId) -``` - -Returns a key to be used for storing the IPNS record locally, that is: - -``` -/ipns/${base32()} -``` - ### Marshal data with proto buffer ```js diff --git a/package.json b/package.json index 339dced..337a02a 100644 --- a/package.json +++ b/package.json @@ -166,23 +166,21 @@ "docs:no-publish": "NODE_OPTIONS=--max_old_space_size=8192 aegir docs --publish false" }, "dependencies": { - "@libp2p/crypto": "^4.0.0", - "@libp2p/interface": "^1.1.0", - "@libp2p/logger": "^4.0.3", - "@libp2p/peer-id": "^4.0.3", - "cborg": "^4.0.1", - "err-code": "^3.0.1", - "interface-datastore": "^8.1.0", - "multiformats": "^13.0.0", - "protons-runtime": "^5.2.1", - "timestamp-nano": "^1.0.0", + "@libp2p/crypto": "^5.0.0", + "@libp2p/interface": "^2.0.0", + "@libp2p/logger": "^5.0.0", + "cborg": "^4.2.3", + "interface-datastore": "^8.3.0", + "multiformats": "^13.2.2", + "protons-runtime": "^5.5.0", + "timestamp-nano": "^1.0.1", "uint8arraylist": "^2.4.8", - "uint8arrays": "^5.0.1" + "uint8arrays": "^5.1.0" }, "devDependencies": { - "@libp2p/peer-id-factory": "^4.0.2", + "@libp2p/peer-id": "^5.0.0", "aegir": "^44.1.1", - "protons": "^7.3.3" + "protons": "^7.6.0" }, "sideEffects": false } diff --git a/src/errors.ts b/src/errors.ts index 969b5eb..4075c76 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,13 +1,71 @@ -export const ERR_IPNS_EXPIRED_RECORD = 'ERR_IPNS_EXPIRED_RECORD' -export const ERR_UNRECOGNIZED_VALIDITY = 'ERR_UNRECOGNIZED_VALIDITY' -export const ERR_SIGNATURE_CREATION = 'ERR_SIGNATURE_CREATION' -export const ERR_SIGNATURE_VERIFICATION = 'ERR_SIGNATURE_VERIFICATION' -export const ERR_UNRECOGNIZED_FORMAT = 'ERR_UNRECOGNIZED_FORMAT' -export const ERR_PEER_ID_FROM_PUBLIC_KEY = 'ERR_PEER_ID_FROM_PUBLIC_KEY' -export const ERR_PUBLIC_KEY_FROM_ID = 'ERR_PUBLIC_KEY_FROM_ID' -export const ERR_UNDEFINED_PARAMETER = 'ERR_UNDEFINED_PARAMETER' -export const ERR_INVALID_RECORD_DATA = 'ERR_INVALID_RECORD_DATA' -export const ERR_INVALID_VALUE = 'ERR_INVALID_VALUE' -export const ERR_INVALID_EMBEDDED_KEY = 'ERR_INVALID_EMBEDDED_KEY' -export const ERR_MISSING_PRIVATE_KEY = 'ERR_MISSING_PRIVATE_KEY' -export const ERR_RECORD_TOO_LARGE = 'ERR_RECORD_TOO_LARGE' +export class SignatureCreationError extends Error { + static name = 'SignatureCreationError' + + constructor (message = 'Record signature creation failed') { + super(message) + this.name = 'SignatureCreationError' + } +} + +export class SignatureVerificationError extends Error { + static name = 'SignatureVerificationError' + + constructor (message = 'Record signature verification failed') { + super(message) + this.name = 'SignatureVerificationError' + } +} + +export class RecordExpiredError extends Error { + static name = 'RecordExpiredError' + + constructor (message = 'Record has expired') { + super(message) + this.name = 'RecordExpiredError' + } +} + +export class UnsupportedValidityError extends Error { + static name = 'UnsupportedValidityError' + + constructor (message = 'The validity type is unsupported') { + super(message) + this.name = 'UnsupportedValidityError' + } +} + +export class RecordTooLargeError extends Error { + static name = 'RecordTooLargeError' + + constructor (message = 'The record is too large') { + super(message) + this.name = 'RecordTooLargeError' + } +} + +export class InvalidValueError extends Error { + static name = 'InvalidValueError' + + constructor (message = 'Value must be a valid content path starting with /') { + super(message) + this.name = 'InvalidValueError' + } +} + +export class InvalidRecordDataError extends Error { + static name = 'InvalidRecordDataError' + + constructor (message = 'Invalid record data') { + super(message) + this.name = 'InvalidRecordDataError' + } +} + +export class InvalidEmbeddedPublicKeyError extends Error { + static name = 'InvalidEmbeddedPublicKeyError' + + constructor (message = 'Invalid embedded public key') { + super(message) + this.name = 'InvalidEmbeddedPublicKeyError' + } +} diff --git a/src/index.ts b/src/index.ts index f6373cc..7dca7c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,15 @@ -import { unmarshalPrivateKey } from '@libp2p/crypto/keys' +import { publicKeyToProtobuf } from '@libp2p/crypto/keys' import { logger } from '@libp2p/logger' -import errCode from 'err-code' -import { Key } from 'interface-datastore/key' -import { base32upper } from 'multiformats/bases/base32' -import * as Digest from 'multiformats/hashes/digest' -import { identity } from 'multiformats/hashes/identity' +import { type Key } from 'interface-datastore/key' import NanoDate from 'timestamp-nano' -import { equals as uint8ArrayEquals } from 'uint8arrays/equals' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import * as ERRORS from './errors.js' +import { SignatureCreationError } from './errors.js' import { IpnsEntry } from './pb/ipns.js' import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, normalizeValue } from './utils.js' -import type { PrivateKey, PeerId } from '@libp2p/interface' +import type { PrivateKey, PublicKey } from '@libp2p/interface' import type { CID } from 'multiformats/cid' const log = logger('ipns') -const ID_MULTIHASH_CODE = identity.code const DEFAULT_TTL_NS = 60 * 60 * 1e+9 // 1 Hour or 3600 Seconds export const namespace = '/ipns/' @@ -157,22 +151,22 @@ const defaultCreateOptions: CreateOptions = { * * PeerIDs will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}` * * String paths will be stored in the record as-is, but they must start with `"/"` * - * @param {PeerId} peerId - peer id containing private key for signing the record. - * @param {CID | PeerId | string} value - content to be stored in the record. + * @param {PrivateKey} privateKey - the private key for signing the record. + * @param {CID | PublicKey | string} value - content to be stored in the record. * @param {number | bigint} seq - number representing the current version of the record. * @param {number} lifetime - lifetime of the record (in milliseconds). * @param {CreateOptions} options - additional create options. */ -export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise -export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise -export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise -export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise { +export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise +export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise +export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise +export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise { // Validity in ISOString with nanoseconds precision and validity type EOL const expirationDate = new NanoDate(Date.now() + Number(lifetime)) const validityType = IpnsEntry.ValidityType.EOL const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS) - return _create(peerId, value, seq, validityType, expirationDate.toString(), ttlNs, options) + return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options) } /** @@ -185,34 +179,28 @@ export async function create (peerId: PeerId, value: CID | PeerId | string, seq: * * PeerIDs will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}` * * String paths will be stored in the record as-is, but they must start with `"/"` * - * @param {PeerId} peerId - PeerId containing private key for signing the record. - * @param {CID | PeerId | string} value - content to be stored in the record. + * @param {PrivateKey} privateKey - the private key for signing the record. + * @param {CID | PublicKey | string} value - content to be stored in the record. * @param {number | bigint} seq - number representing the current version of the record. * @param {string} expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision. * @param {CreateOptions} options - additional creation options. */ -export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise -export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise -export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateOptions): Promise -export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise { +export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise +export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise +export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateOptions): Promise +export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise { const expirationDate = NanoDate.fromString(expiration) const validityType = IpnsEntry.ValidityType.EOL const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS) - return _create(peerId, value, seq, validityType, expirationDate.toString(), ttlNs, options) + return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options) } -const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise => { +const _create = async (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise => { seq = BigInt(seq) const isoValidity = uint8ArrayFromString(validity) const normalizedValue = normalizeValue(value) const encodedValue = uint8ArrayFromString(normalizedValue) - - if (peerId.privateKey == null) { - throw errCode(new Error('Missing private key'), ERRORS.ERR_MISSING_PRIVATE_KEY) - } - - const privateKey = await unmarshalPrivateKey(peerId.privateKey) const data = createCborData(encodedValue, validityType, isoValidity, seq, ttl) const sigData = ipnsRecordDataForV2Sig(data) const signatureV2 = await privateKey.sign(sigData) @@ -220,12 +208,8 @@ const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number // if we cannot derive the public key from the PeerId (e.g. RSA PeerIDs), // we have to embed it in the IPNS record - if (peerId.publicKey != null) { - const digest = Digest.decode(peerId.toBytes()) - - if (digest.code !== ID_MULTIHASH_CODE || !uint8ArrayEquals(peerId.publicKey, digest.digest)) { - pubKey = peerId.publicKey - } + if (privateKey.type === 'RSA') { + pubKey = publicKeyToProtobuf(privateKey.publicKey) } if (options.v1Compatible === true) { @@ -266,24 +250,13 @@ const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number } } -/** - * rawStdEncoding with RFC4648 - */ -const rawStdEncoding = (key: Uint8Array): string => base32upper.encode(key).slice(1) - -/** - * Get key for storing the record locally. - * Format: /ipns/${base32()} - * - * @param {Uint8Array} key - peer identifier object. - */ -export const getLocalKey = (key: Uint8Array): Key => new Key(`/ipns/${rawStdEncoding(key)}`) - -export { unmarshal } from './utils.js' -export { marshal } from './utils.js' -export { peerIdToRoutingKey } from './utils.js' -export { peerIdFromRoutingKey } from './utils.js' -export { extractPublicKey } from './utils.js' +export { unmarshalIPNSRecord } from './utils.js' +export { marshalIPNSRecord } from './utils.js' +export { multihashToIPNSRoutingKey } from './utils.js' +export { multihashFromIPNSRoutingKey } from './utils.js' +export { publicKeyToIPNSRoutingKey } from './utils.js' +export { publicKeyFromIPNSRoutingKey } from './utils.js' +export { extractPublicKeyFromIPNSRecord } from './utils.js' /** * Sign ipns record data using the legacy V1 signature scheme @@ -295,6 +268,6 @@ const signLegacyV1 = async (privateKey: PrivateKey, value: Uint8Array, validityT return await privateKey.sign(dataForSignature) } catch (error: any) { log.error('record signature creation failed', error) - throw errCode(new Error('record signature creation failed'), ERRORS.ERR_SIGNATURE_CREATION) + throw new SignatureCreationError('Record signature creation failed') } } diff --git a/src/pb/ipns.ts b/src/pb/ipns.ts index fb18014..db74fb7 100644 --- a/src/pb/ipns.ts +++ b/src/pb/ipns.ts @@ -4,8 +4,7 @@ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ /* eslint-disable @typescript-eslint/no-empty-interface */ -import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime' -import type { Codec } from 'protons-runtime' +import { type Codec, decodeMessage, type DecodeOptions, encodeMessage, enumeration, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' export interface IpnsEntry { @@ -92,7 +91,7 @@ export namespace IpnsEntry { if (opts.lengthDelimited !== false) { w.ldelim() } - }, (reader, length) => { + }, (reader, length, opts = {}) => { const obj: any = {} const end = length == null ? reader.len : reader.pos + length @@ -101,36 +100,46 @@ export namespace IpnsEntry { const tag = reader.uint32() switch (tag >>> 3) { - case 1: + case 1: { obj.value = reader.bytes() break - case 2: + } + case 2: { obj.signatureV1 = reader.bytes() break - case 3: + } + case 3: { obj.validityType = IpnsEntry.ValidityType.codec().decode(reader) break - case 4: + } + case 4: { obj.validity = reader.bytes() break - case 5: + } + case 5: { obj.sequence = reader.uint64() break - case 6: + } + case 6: { obj.ttl = reader.uint64() break - case 7: + } + case 7: { obj.pubKey = reader.bytes() break - case 8: + } + case 8: { obj.signatureV2 = reader.bytes() break - case 9: + } + case 9: { obj.data = reader.bytes() break - default: + } + default: { reader.skipType(tag & 7) break + } } } @@ -145,7 +154,7 @@ export namespace IpnsEntry { return encodeMessage(obj, IpnsEntry.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): IpnsEntry => { - return decodeMessage(buf, IpnsEntry.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions): IpnsEntry => { + return decodeMessage(buf, IpnsEntry.codec(), opts) } } diff --git a/src/selector.ts b/src/selector.ts index 5fab036..c669a5b 100644 --- a/src/selector.ts +++ b/src/selector.ts @@ -1,10 +1,10 @@ import NanoDate from 'timestamp-nano' import { IpnsEntry } from './pb/ipns.js' -import { unmarshal } from './utils.js' +import { unmarshalIPNSRecord } from './utils.js' export function ipnsSelector (key: Uint8Array, data: Uint8Array[]): number { const entries = data.map((buf, index) => ({ - record: unmarshal(buf), + record: unmarshalIPNSRecord(buf), index })) diff --git a/src/utils.ts b/src/utils.ts index 7df0c78..f210830 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,60 +1,42 @@ -import { unmarshalPublicKey } from '@libp2p/crypto/keys' -import { isPeerId } from '@libp2p/interface' +import { publicKeyFromMultihash, publicKeyFromProtobuf } from '@libp2p/crypto/keys' +import { InvalidMultihashError } from '@libp2p/interface' import { logger } from '@libp2p/logger' -import { peerIdFromBytes, peerIdFromKeys } from '@libp2p/peer-id' import * as cborg from 'cborg' -import errCode from 'err-code' import { base36 } from 'multiformats/bases/base36' -import { CID } from 'multiformats/cid' +import { CID, type MultihashDigest } from 'multiformats/cid' +import * as Digest from 'multiformats/hashes/digest' import { concat as uint8ArrayConcat } from 'uint8arrays/concat' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import * as ERRORS from './errors.js' +import { InvalidRecordDataError, InvalidValueError, SignatureVerificationError, UnsupportedValidityError } from './errors.js' import { IpnsEntry } from './pb/ipns.js' import type { IPNSRecord, IPNSRecordV2, IPNSRecordData } from './index.js' -import type { PublicKey, PeerId } from '@libp2p/interface' +import type { PublicKey, Ed25519PublicKey, Secp256k1PublicKey } from '@libp2p/interface' const log = logger('ipns:utils') const IPNS_PREFIX = uint8ArrayFromString('/ipns/') const LIBP2P_CID_CODEC = 114 /** - * Extracts a public key from the passed PeerId, falling - * back to the pubKey embedded in the ipns record + * Extracts a public key from the passed PeerId, falling back to the pubKey + * embedded in the ipns record */ -export const extractPublicKey = async (peerId: PeerId, record: IPNSRecord | IPNSRecordV2): Promise => { - if (record == null || peerId == null) { - const error = new Error('one or more of the provided parameters are not defined') - - log.error(error) - throw errCode(error, ERRORS.ERR_UNDEFINED_PARAMETER) - } - +export const extractPublicKeyFromIPNSRecord = (record: IPNSRecord | IPNSRecordV2): PublicKey | undefined => { let pubKey: PublicKey | undefined if (record.pubKey != null) { try { - pubKey = unmarshalPublicKey(record.pubKey) + pubKey = publicKeyFromProtobuf(record.pubKey) } catch (err) { log.error(err) throw err } - - const otherId = await peerIdFromKeys(record.pubKey) - - if (!otherId.equals(peerId)) { - throw errCode(new Error('Embedded public key did not match PeerID'), ERRORS.ERR_INVALID_EMBEDDED_KEY) - } - } else if (peerId.publicKey != null) { - pubKey = unmarshalPublicKey(peerId.publicKey) } if (pubKey != null) { return pubKey } - - throw errCode(new Error('no public key is available'), ERRORS.ERR_UNDEFINED_PARAMETER) } /** @@ -75,7 +57,7 @@ export const ipnsRecordDataForV2Sig = (data: Uint8Array): Uint8Array => { return uint8ArrayConcat([entryData, data]) } -export const marshal = (obj: IPNSRecord | IPNSRecordV2): Uint8Array => { +export const marshalIPNSRecord = (obj: IPNSRecord | IPNSRecordV2): Uint8Array => { if ('signatureV1' in obj) { return IpnsEntry.encode({ value: uint8ArrayFromString(obj.value), @@ -97,7 +79,7 @@ export const marshal = (obj: IPNSRecord | IPNSRecordV2): Uint8Array => { } } -export function unmarshal (buf: Uint8Array): IPNSRecord { +export function unmarshalIPNSRecord (buf: Uint8Array): IPNSRecord { const message = IpnsEntry.decode(buf) // protobufjs returns bigints as numbers @@ -114,7 +96,7 @@ export function unmarshal (buf: Uint8Array): IPNSRecord { // V1+V2 records for quite a while and we don't support V1-only records during // validation any more if (message.signatureV2 == null || message.data == null) { - throw errCode(new Error('missing data or signatureV2'), ERRORS.ERR_SIGNATURE_VERIFICATION) + throw new SignatureVerificationError('Missing data or signatureV2') } const data = parseCborData(message.data) @@ -153,15 +135,33 @@ export function unmarshal (buf: Uint8Array): IPNSRecord { } } -export const peerIdToRoutingKey = (peerId: PeerId): Uint8Array => { +export const publicKeyToIPNSRoutingKey = (publicKey: PublicKey): Uint8Array => { + return multihashToIPNSRoutingKey(publicKey.toMultihash()) +} + +export const multihashToIPNSRoutingKey = (digest: MultihashDigest): Uint8Array => { return uint8ArrayConcat([ IPNS_PREFIX, - peerId.toBytes() + digest.bytes ]) } -export const peerIdFromRoutingKey = (key: Uint8Array): PeerId => { - return peerIdFromBytes(key.slice(IPNS_PREFIX.length)) +export const publicKeyFromIPNSRoutingKey = (key: Uint8Array): Ed25519PublicKey | Secp256k1PublicKey | undefined => { + try { + // @ts-expect-error digest code may not be 0 + return publicKeyFromMultihash(multihashFromIPNSRoutingKey(key)) + } catch {} +} + +export const multihashFromIPNSRoutingKey = (key: Uint8Array): MultihashDigest<0x00> | MultihashDigest<0x12> => { + const digest = Digest.decode(key.slice(IPNS_PREFIX.length)) + + if (digest.code !== 0x00 && digest.code !== 0x12) { + throw new InvalidMultihashError('Multihash in IPNS key was not identity or sha2-256') + } + + // @ts-expect-error digest may not have correct code even though we just checked + return digest } export const createCborData = (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array, sequence: bigint, ttl: bigint): Uint8Array => { @@ -170,7 +170,7 @@ export const createCborData = (value: Uint8Array, validityType: IpnsEntry.Validi if (validityType === IpnsEntry.ValidityType.EOL) { ValidityType = 0 } else { - throw errCode(new Error('Unknown validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY) + throw new UnsupportedValidityError('The validity type is unsupported') } const data = { @@ -190,7 +190,7 @@ export const parseCborData = (buf: Uint8Array): IPNSRecordData => { if (data.ValidityType === 0) { data.ValidityType = IpnsEntry.ValidityType.EOL } else { - throw errCode(new Error('Unknown validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY) + throw new UnsupportedValidityError('The validity type is unsupported') } if (Number.isInteger(data.Sequence)) { @@ -211,10 +211,10 @@ export const parseCborData = (buf: Uint8Array): IPNSRecordData => { * string starting with '/'. PeerIDs become `/ipns/${cidV1Libp2pKey}`, * CIDs become `/ipfs/${cidAsV1}`. */ -export const normalizeValue = (value?: CID | PeerId | string | Uint8Array): string => { +export const normalizeValue = (value?: CID | PublicKey | string | Uint8Array): string => { if (value != null) { // if we have a PeerId, turn it into an ipns path - if (isPeerId(value)) { + if (hasToCID(value)) { return `/ipns/${value.toCID().toString(base36)}` } @@ -256,33 +256,37 @@ export const normalizeValue = (value?: CID | PeerId | string | Uint8Array): stri } } - throw errCode(new Error('Value must be a valid content path starting with /'), ERRORS.ERR_INVALID_VALUE) + throw new InvalidValueError('Value must be a valid content path starting with /') } const validateCborDataMatchesPbData = (entry: IpnsEntry): void => { if (entry.data == null) { - throw errCode(new Error('Record data is missing'), ERRORS.ERR_INVALID_RECORD_DATA) + throw new InvalidRecordDataError('Record data is missing') } const data = parseCborData(entry.data) if (!uint8ArrayEquals(data.Value, entry.value ?? new Uint8Array(0))) { - throw errCode(new Error('Field "value" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) + throw new SignatureVerificationError('Field "value" did not match between protobuf and CBOR') } if (!uint8ArrayEquals(data.Validity, entry.validity ?? new Uint8Array(0))) { - throw errCode(new Error('Field "validity" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) + throw new SignatureVerificationError('Field "validity" did not match between protobuf and CBOR') } if (data.ValidityType !== entry.validityType) { - throw errCode(new Error('Field "validityType" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) + throw new SignatureVerificationError('Field "validityType" did not match between protobuf and CBOR') } if (data.Sequence !== entry.sequence) { - throw errCode(new Error('Field "sequence" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) + throw new SignatureVerificationError('Field "sequence" did not match between protobuf and CBOR') } if (data.TTL !== entry.ttl) { - throw errCode(new Error('Field "ttl" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) + throw new SignatureVerificationError('Field "ttl" did not match between protobuf and CBOR') } } + +function hasToCID (obj?: any): obj is { toCID(): CID } { + return typeof obj?.toCID === 'function' +} diff --git a/src/validator.ts b/src/validator.ts index e24745d..c4a41ac 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -1,9 +1,9 @@ import { logger } from '@libp2p/logger' -import errCode from 'err-code' import NanoDate from 'timestamp-nano' -import * as ERRORS from './errors.js' +import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { InvalidEmbeddedPublicKeyError, RecordExpiredError, RecordTooLargeError, SignatureVerificationError, UnsupportedValidityError } from './errors.js' import { IpnsEntry } from './pb/ipns.js' -import { extractPublicKey, ipnsRecordDataForV2Sig, unmarshal, peerIdFromRoutingKey } from './utils.js' +import { extractPublicKeyFromIPNSRecord, ipnsRecordDataForV2Sig, publicKeyFromIPNSRoutingKey, publicKeyToIPNSRoutingKey, unmarshalIPNSRecord } from './utils.js' import type { PublicKey } from '@libp2p/interface' const log = logger('ipns:validator') @@ -21,30 +21,32 @@ export const validate = async (publicKey: PublicKey, buf: Uint8Array): Promise { if (marshalledData.byteLength > MAX_RECORD_SIZE) { - throw errCode(new Error('record too large'), ERRORS.ERR_RECORD_TOO_LARGE) + throw new RecordTooLargeError('The record is too large') } - const peerId = peerIdFromRoutingKey(key) - const receivedRecord = unmarshal(marshalledData) + // try to extract public key from routing key + const routingPubKey = publicKeyFromIPNSRoutingKey(key) - // extract public key - const pubKey = await extractPublicKey(peerId, receivedRecord) + // extract public key from record + const receivedRecord = unmarshalIPNSRecord(marshalledData) + const recordPubKey = extractPublicKeyFromIPNSRecord(receivedRecord) ?? routingPubKey + + if (recordPubKey == null) { + throw new InvalidEmbeddedPublicKeyError('Could not extract public key from IPNS record or routing key') + } + + const routingKey = publicKeyToIPNSRoutingKey(recordPubKey) + + if (!uint8ArrayEquals(key, routingKey)) { + throw new InvalidEmbeddedPublicKeyError('Embedded public key did not match routing key') + } // Record validation - await validate(pubKey, marshalledData) + await validate(recordPubKey, marshalledData) } diff --git a/test/conformance.spec.ts b/test/conformance.spec.ts index ee1e046..78df60b 100644 --- a/test/conformance.spec.ts +++ b/test/conformance.spec.ts @@ -1,30 +1,34 @@ /* eslint-env mocha */ -import { unmarshalPublicKey } from '@libp2p/crypto/keys' import { peerIdFromCID } from '@libp2p/peer-id' import { expect } from 'aegir/chai' import loadFixture from 'aegir/fixtures' import { base36 } from 'multiformats/bases/base36' import { CID } from 'multiformats/cid' -import * as ERRORS from '../src/errors.js' -import * as ipns from '../src/index.js' +import { SignatureVerificationError } from '../src/errors.js' +import { marshalIPNSRecord, unmarshalIPNSRecord } from '../src/index.js' import { validate } from '../src/validator.js' describe('conformance', function () { it('should reject a v1 only record', async () => { const buf = loadFixture('test/fixtures/k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record') - expect(() => ipns.unmarshal(buf)).to.throw(/missing data or signatureV2/) - .with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) + expect(() => unmarshalIPNSRecord(buf)).to.throw(/Missing data or signatureV2/) + .with.property('name', SignatureVerificationError.name) }) it('should validate a record with v1 and v2 signatures', async () => { const buf = loadFixture('test/fixtures/k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record') - const record = ipns.unmarshal(buf) + const record = unmarshalIPNSRecord(buf) const cid = CID.parse('k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w', base36) const peerId = peerIdFromCID(cid) - const publicKey = unmarshalPublicKey(peerId.publicKey ?? new Uint8Array()) + + if (peerId.publicKey == null) { + throw new Error('Peer ID embedded in CID had no public key') + } + + const publicKey = peerId.publicKey await validate(publicKey, buf) expect(record.value).to.equal('/ipfs/bafkqaddwgevxmmraojswg33smq') @@ -33,34 +37,44 @@ describe('conformance', function () { it('should reject a record with inconsistent value fields', async () => { const buf = loadFixture('test/fixtures/k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw_v1-v2-broken-v1-value.ipns-record') - expect(() => ipns.unmarshal(buf)).to.throw(/Field "value" did not match/) - .with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) + expect(() => unmarshalIPNSRecord(buf)).to.throw(/Field "value" did not match/) + .with.property('name', SignatureVerificationError.name) }) it('should reject a record with v1 and v2 signatures but invalid v2', async () => { const buf = loadFixture('test/fixtures/k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record') const cid = CID.parse('k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c', base36) const peerId = peerIdFromCID(cid) - const publicKey = unmarshalPublicKey(peerId.publicKey ?? new Uint8Array()) - await expect(validate(publicKey, buf)).to.eventually.be.rejectedWith(/record signature verification failed/) - .with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) + if (peerId.publicKey == null) { + throw new Error('Peer ID embedded in CID had no public key') + } + + const publicKey = peerId.publicKey + + await expect(validate(publicKey, buf)).to.eventually.be.rejectedWith(/Record signature verification failed/) + .with.property('name', SignatureVerificationError.name) }) it('should reject a record with v1 and v2 signatures but invalid v1', async () => { const buf = loadFixture('test/fixtures/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record') - const record = ipns.unmarshal(buf) + const record = unmarshalIPNSRecord(buf) expect(record.value).to.equal('/ipfs/bafkqahtwgevxmmrao5uxi2bamjzg623fnyqhg2lhnzqxi5lsmuqhmmi') }) it('should validate a record with only v2 signature', async () => { const buf = loadFixture('test/fixtures/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record') - const record = ipns.unmarshal(buf) + const record = unmarshalIPNSRecord(buf) const cid = CID.parse('k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f', base36) const peerId = peerIdFromCID(cid) - const publicKey = unmarshalPublicKey(peerId.publicKey ?? new Uint8Array()) + + if (peerId.publicKey == null) { + throw new Error('Peer ID embedded in CID had no public key') + } + + const publicKey = peerId.publicKey await validate(publicKey, buf) expect(record.value).to.equal('/ipfs/bafkqadtwgiww63tmpeqhezldn5zgi') @@ -76,8 +90,8 @@ describe('conformance', function () { for (const fixture of fixtures) { const buf = loadFixture(fixture) - const record = ipns.unmarshal(buf) - const marshalled = ipns.marshal(record) + const record = unmarshalIPNSRecord(buf) + const marshalled = marshalIPNSRecord(record) expect(buf).to.equalBytes(marshalled) } diff --git a/test/index.spec.ts b/test/index.spec.ts index 1800945..cdc9552 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,33 +1,32 @@ /* eslint-env mocha */ import { randomBytes } from '@libp2p/crypto' -import { generateKeyPair, unmarshalPrivateKey } from '@libp2p/crypto/keys' -import { peerIdFromKeys, peerIdFromString } from '@libp2p/peer-id' -import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { generateKeyPair, publicKeyToProtobuf } from '@libp2p/crypto/keys' +import { peerIdFromPrivateKey } from '@libp2p/peer-id' import { expect } from 'aegir/chai' import * as cbor from 'cborg' import { base36 } from 'multiformats/bases/base36' import { base58btc } from 'multiformats/bases/base58' import { CID } from 'multiformats/cid' +import * as Digest from 'multiformats/hashes/digest' import { toString as uint8ArrayToString } from 'uint8arrays' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import * as ERRORS from '../src/errors.js' -import * as ipns from '../src/index.js' +import { InvalidEmbeddedPublicKeyError, InvalidValueError, RecordExpiredError, SignatureVerificationError } from '../src/errors.js' +import { createIPNSRecord, createIPNSRecordWithExpiration } from '../src/index.js' import { IpnsEntry } from '../src/pb/ipns.js' -import { extractPublicKey, peerIdToRoutingKey, parseCborData, createCborData, ipnsRecordDataForV2Sig } from '../src/utils.js' +import { extractPublicKeyFromIPNSRecord, parseCborData, createCborData, ipnsRecordDataForV2Sig, marshalIPNSRecord, unmarshalIPNSRecord, publicKeyToIPNSRoutingKey, multihashToIPNSRoutingKey, multihashFromIPNSRoutingKey } from '../src/utils.js' import { ipnsValidator } from '../src/validator.js' import { kuboRecord } from './fixtures/records.js' -import type { PeerId } from '@libp2p/interface' +import type { PrivateKey } from '@libp2p/interface' describe('ipns', function () { this.timeout(20 * 1000) const contentPath = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' - let peerId: PeerId + let privateKey: PrivateKey before(async () => { - const rsa = await generateKeyPair('RSA', 2048) - peerId = await peerIdFromKeys(rsa.public.bytes, rsa.bytes) + privateKey = await generateKeyPair('RSA', 2048) }) it('should create an ipns record (V1+V2) correctly', async () => { @@ -35,7 +34,7 @@ describe('ipns', function () { const ttl = BigInt(60 * 60 * 1e+9) const validity = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, validity) + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) expect(record.value).to.equal(contentPath) expect(record.validityType).to.equal(IpnsEntry.ValidityType.EOL) @@ -47,7 +46,7 @@ describe('ipns', function () { expect(record.data).to.exist() // Protobuf must have all fields! - const pb = IpnsEntry.decode(ipns.marshal(record)) + const pb = IpnsEntry.decode(marshalIPNSRecord(record)) expect(pb.value).to.equalBytes(uint8ArrayFromString(contentPath)) expect(pb.validityType).to.equal(IpnsEntry.ValidityType.EOL) expect(pb.validity).to.exist() @@ -71,7 +70,7 @@ describe('ipns', function () { const ttl = BigInt(60 * 60 * 1e+9) const validity = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, validity, { v1Compatible: false }) + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, { v1Compatible: false }) expect(record.value).to.equal(contentPath) expect(record.validityType).to.equal(IpnsEntry.ValidityType.EOL) @@ -83,7 +82,7 @@ describe('ipns', function () { expect(record.data).to.exist() // PB must only have signature and data. - const pb = IpnsEntry.decode(ipns.marshal(record)) + const pb = IpnsEntry.decode(marshalIPNSRecord(record)) expect(pb.value).to.not.exist() expect(pb.validityType).to.not.exist() expect(pb.validity).to.not.exist() @@ -106,10 +105,10 @@ describe('ipns', function () { const sequence = 0 const expiration = '2033-05-18T03:33:20.000000000Z' - const record = await ipns.createWithExpiration(peerId, contentPath, sequence, expiration) - const marshalledRecord = ipns.marshal(record) + const record = await createIPNSRecordWithExpiration(privateKey, contentPath, sequence, expiration) + const marshalledRecord = marshalIPNSRecord(record) - await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord) + await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord) const pb = IpnsEntry.decode(marshalledRecord) expect(pb).to.have.property('validity') @@ -120,12 +119,12 @@ describe('ipns', function () { const sequence = 0 const expiration = '2033-05-18T03:33:20.000000000Z' - const record = await ipns.createWithExpiration(peerId, contentPath, sequence, expiration, { v1Compatible: false }) - const marshalledRecord = ipns.marshal(record) + const record = await createIPNSRecordWithExpiration(privateKey, contentPath, sequence, expiration, { v1Compatible: false }) + const marshalledRecord = marshalIPNSRecord(record) - await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord) + await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord) - const pb = IpnsEntry.decode(ipns.marshal(record)) + const pb = IpnsEntry.decode(marshalIPNSRecord(record)) expect(pb).to.not.have.property('validity') const data = parseCborData(pb.data ?? new Uint8Array(0)) @@ -137,12 +136,12 @@ describe('ipns', function () { const ttl = BigInt(0.6e+12) const validity = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, validity, { + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, { ttlNs: ttl }) - const marshalledRecord = ipns.marshal(record) + const marshalledRecord = marshalIPNSRecord(record) - await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord) + await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord) const pb = IpnsEntry.decode(marshalledRecord) const data = parseCborData(pb.data ?? new Uint8Array(0)) @@ -154,13 +153,13 @@ describe('ipns', function () { const ttl = BigInt(1.6e+12) const validity = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, validity, { + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, { ttlNs: ttl, v1Compatible: false }) - const marshalledRecord = ipns.marshal(record) + const marshalledRecord = marshalIPNSRecord(record) - await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord) + await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord) const pb = IpnsEntry.decode(marshalledRecord) expect(pb).to.not.have.property('ttl') @@ -173,99 +172,107 @@ describe('ipns', function () { const sequence = 0 const validity = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, validity) - await ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record)) + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) + await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record)) }) it('should create an ipns record (V2) and validate it correctly', async () => { const sequence = 0 const validity = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, validity, { v1Compatible: false }) - await ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record)) + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, { v1Compatible: false }) + await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record)) }) it('should normalize value when creating an ipns record (arbitrary string path)', async () => { const inputValue = '/foo/bar/baz' const expectedValue = '/foo/bar/baz' - const record = await ipns.create(peerId, inputValue, 0, 1000000) + const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000) expect(record.value).to.equal(expectedValue) }) - it('should normalize value when creating a recursive ipns record (peer id)', async () => { - const inputValue = await createEd25519PeerId() - const expectedValue = `/ipns/${inputValue.toCID().toString(base36)}` - const record = await ipns.create(peerId, inputValue, 0, 1000000) + it('should normalize value when creating a recursive ipns record (Ed25519 public key)', async () => { + const otherKey = await generateKeyPair('Ed25519') + const expectedValue = `/ipns/${otherKey.publicKey.toCID().toString(base36)}` + const record = await createIPNSRecord(privateKey, otherKey.publicKey, 0, 1000000) + expect(record.value).to.equal(expectedValue) + }) + + it('should normalize value when creating a recursive ipns record (RSA public key)', async () => { + const otherKey = await generateKeyPair('RSA', 512) + const expectedValue = `/ipns/${otherKey.publicKey.toCID().toString(base36)}` + const record = await createIPNSRecord(privateKey, otherKey.publicKey, 0, 1000000) expect(record.value).to.equal(expectedValue) }) it('should normalize value when creating a recursive ipns record (peer id as CID)', async () => { - const inputValue = await createEd25519PeerId() - const expectedValue = `/ipns/${inputValue.toCID().toString(base36)}` - const record = await ipns.create(peerId, inputValue.toCID(), 0, 1000000) + const otherKey = await generateKeyPair('Ed25519') + const peerId = peerIdFromPrivateKey(otherKey) + const expectedValue = `/ipns/${peerId.toCID().toString(base36)}` + const record = await createIPNSRecord(privateKey, peerId.toCID(), 0, 1000000) expect(record.value).to.equal(expectedValue) }) it('should normalize value when creating an ipns record (v0 cid)', async () => { const inputValue = CID.parse('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq') const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' - const record = await ipns.create(peerId, inputValue, 0, 1000000) + const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000) expect(record.value).to.equal(expectedValue) }) it('should normalize value when creating an ipns record (v1 cid)', async () => { const inputValue = CID.parse('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' - const record = await ipns.create(peerId, inputValue, 0, 1000000) + const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000) expect(record.value).to.equal(expectedValue) }) it('should normalize value when reading an ipns record (string v0 cid path)', async () => { const inputValue = '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq' const expectedValue = '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq' - const record = await ipns.create(peerId, inputValue, 0, 1000000) + const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000) - const pb = IpnsEntry.decode(ipns.marshal(record)) + const pb = IpnsEntry.decode(marshalIPNSRecord(record)) pb.data = createCborData(uint8ArrayFromString(inputValue), pb.validityType ?? IpnsEntry.ValidityType.EOL, pb.validity ?? new Uint8Array(0), pb.sequence ?? 0n, pb.ttl ?? 0n) pb.value = uint8ArrayFromString(inputValue) - const modifiedRecord = ipns.unmarshal(IpnsEntry.encode(pb)) + const modifiedRecord = unmarshalIPNSRecord(IpnsEntry.encode(pb)) expect(modifiedRecord.value).to.equal(expectedValue) }) it('should normalize value when reading an ipns record (string v1 cid path)', async () => { const inputValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' - const record = await ipns.create(peerId, inputValue, 0, 1000000) + const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000) - const pb = IpnsEntry.decode(ipns.marshal(record)) + const pb = IpnsEntry.decode(marshalIPNSRecord(record)) pb.data = createCborData(uint8ArrayFromString(inputValue), pb.validityType ?? IpnsEntry.ValidityType.EOL, pb.validity ?? new Uint8Array(0), pb.sequence ?? 0n, pb.ttl ?? 0n) pb.value = uint8ArrayFromString(inputValue) - const modifiedRecord = ipns.unmarshal(IpnsEntry.encode(pb)) + const modifiedRecord = unmarshalIPNSRecord(IpnsEntry.encode(pb)) expect(modifiedRecord.value).to.equal(expectedValue) }) it('should fail to normalize non-path value', async () => { const inputValue = 'hello' - await expect(ipns.create(peerId, inputValue, 0, 1000000)).to.eventually.be.rejected - .with.property('code', ERRORS.ERR_INVALID_VALUE) + await expect(createIPNSRecord(privateKey, inputValue, 0, 1000000)).to.eventually.be.rejected + .with.property('name', InvalidValueError.name) }) it('should fail to normalize path value that is too short', async () => { const inputValue = '/' - await expect(ipns.create(peerId, inputValue, 0, 1000000)).to.eventually.be.rejected - .with.property('code', ERRORS.ERR_INVALID_VALUE) + await expect(createIPNSRecord(privateKey, inputValue, 0, 1000000)).to.eventually.be.rejected + .with.property('name', InvalidValueError.name) }) it('should fail to validate a v1 (deprecated legacy) message', async () => { const sequence = 0 const validity = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, validity) - const pb = IpnsEntry.decode(ipns.marshal(record)) + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) + const pb = IpnsEntry.decode(marshalIPNSRecord(record)) // remove the extra fields added for v2 sigs delete pb.data @@ -274,15 +281,16 @@ describe('ipns', function () { // confirm a v1 exists expect(pb).to.have.property('signatureV1') - await expect(ipnsValidator(peerIdToRoutingKey(peerId), IpnsEntry.encode(pb))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) + await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), IpnsEntry.encode(pb))).to.eventually.be.rejected() + .with.property('name', SignatureVerificationError.name) }) it('should fail to validate a v2 without v2 signature (ignore v1)', async () => { const sequence = 0 const validity = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, validity) - const pb = IpnsEntry.decode(ipns.marshal(record)) + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) + const pb = IpnsEntry.decode(marshalIPNSRecord(record)) // remove v2 sig delete pb.signatureV2 @@ -290,40 +298,43 @@ describe('ipns', function () { // confirm a v1 exists expect(pb).to.have.property('signatureV1') - await expect(ipnsValidator(peerIdToRoutingKey(peerId), IpnsEntry.encode(pb))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) + await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), IpnsEntry.encode(pb))).to.eventually.be.rejected() + .with.property('name', SignatureVerificationError.name) }) it('should fail to validate a bad record', async () => { const sequence = 0 const validity = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, validity) + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) // corrupt the record by changing the value to random bytes record.value = uint8ArrayToString(randomBytes(46)) - await expect(ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) + await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record))).to.eventually.be.rejected() + .with.property('name', SignatureVerificationError.name) }) it('should create an ipns record with a validity of 1 nanosecond correctly and it should not be valid 1ms later', async () => { const sequence = 0 const validity = 0.00001 - const record = await ipns.create(peerId, contentPath, sequence, validity) + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) await new Promise(resolve => setTimeout(resolve, 1)) - await expect(ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_IPNS_EXPIRED_RECORD) + await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record))).to.eventually.be.rejected() + .with.property('name', RecordExpiredError.name) }) it('should create an ipns record, marshal and unmarshal it, as well as validate it correctly', async () => { const sequence = 0 const validity = 1000000 - const createdRecord = await ipns.create(peerId, contentPath, sequence, validity) + const createdRecord = await createIPNSRecord(privateKey, contentPath, sequence, validity) - const marshalledData = ipns.marshal(createdRecord) - const unmarshalledData = ipns.unmarshal(marshalledData) + const marshalledData = marshalIPNSRecord(createdRecord) + const unmarshalledData = unmarshalIPNSRecord(marshalledData) expect(createdRecord.value).to.equal(unmarshalledData.value) expect(createdRecord.validity.toString()).to.equal(unmarshalledData.validity.toString()) @@ -334,14 +345,7 @@ describe('ipns', function () { expect(createdRecord.signatureV2).to.equalBytes(unmarshalledData.signatureV2) expect(createdRecord.data).to.equalBytes(unmarshalledData.data) - await ipnsValidator(peerIdToRoutingKey(peerId), marshalledData) - }) - - it('should get datastore key correctly', () => { - const datastoreKey = ipns.getLocalKey(base58btc.decode(`z${peerId.toString()}`)) - - expect(datastoreKey).to.exist() - expect(datastoreKey.toString()).to.startWith('/ipns/CIQ') + await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledData) }) it('should be able to turn routing key back into id', () => { @@ -351,10 +355,11 @@ describe('ipns', function () { ] keys.forEach(key => { - const routingKey = ipns.peerIdToRoutingKey(peerIdFromString(key)) - const id = ipns.peerIdFromRoutingKey(routingKey) + const digest = Digest.decode(base58btc.decode(`z${key}`)) + const routingKey = multihashToIPNSRoutingKey(digest) + const id = multihashFromIPNSRoutingKey(routingKey) - expect(id.toString()).to.equal(key) + expect(base58btc.encode(id.bytes)).to.equal(`z${key}`) }) }) @@ -362,11 +367,11 @@ describe('ipns', function () { const sequence = 0 const validity = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, validity) - expect(record.pubKey).to.equalBytes(peerId.publicKey) + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) + expect(record.pubKey).to.equalBytes(publicKeyToProtobuf(privateKey.publicKey)) - const pb = IpnsEntry.decode(ipns.marshal(record)) - expect(pb.pubKey).to.equalBytes(peerId.publicKey) + const pb = IpnsEntry.decode(marshalIPNSRecord(record)) + expect(pb.pubKey).to.equalBytes(publicKeyToProtobuf(privateKey.publicKey)) }) // It should have a public key embedded for newer ed25519 keys @@ -379,8 +384,8 @@ describe('ipns', function () { const sequence = 0 const validity = 1000000 - const ed25519 = await createEd25519PeerId() - const record = await ipns.create(ed25519, contentPath, sequence, validity) + const privateKey = await generateKeyPair('Ed25519') + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) expect(record).to.not.have.property('pubKey') // ed25519 keys should not be embedded }) @@ -389,25 +394,24 @@ describe('ipns', function () { const sequence = 0 const validity = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, validity) + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) delete record.pubKey - const marshalledData = ipns.marshal(record) - const key = peerIdToRoutingKey(peerId) + const marshalledData = marshalIPNSRecord(record) + const key = publicKeyToIPNSRoutingKey(privateKey.publicKey) - await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected().with.property('code', ERRORS.ERR_UNDEFINED_PARAMETER) + await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() + .with.property('name', InvalidEmbeddedPublicKeyError.name) }) it('should be able to export a previously embedded public key from an ipns record', async () => { const sequence = 0 const validity = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, validity) + const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) - const publicKey = await extractPublicKey(peerId, record) - expect(publicKey).to.deep.include({ - bytes: peerId.publicKey - }) + const publicKey = extractPublicKeyFromIPNSRecord(record) + expect(publicKey?.equals(privateKey.publicKey)).to.be.true() }) it('should unmarshal a record with raw CID bytes', async () => { @@ -415,7 +419,7 @@ describe('ipns', function () { // but IPNS records should have string path values // create a dummy record with an arbitrary string path - const input = await ipns.create(peerId, '/foo', 0n, 10000, { + const input = await createIPNSRecord(privateKey, '/foo', 0n, 10000, { v1Compatible: false }) @@ -428,12 +432,11 @@ describe('ipns', function () { input.data = cbor.encode(data) // re-sign record - const privateKey = await unmarshalPrivateKey(peerId.privateKey ?? new Uint8Array(0)) const sigData = ipnsRecordDataForV2Sig(input.data) input.signatureV2 = await privateKey.sign(sigData) - const buf = ipns.marshal(input) - const record = ipns.unmarshal(buf) + const buf = marshalIPNSRecord(input) + const record = unmarshalIPNSRecord(buf) expect(record).to.have.property('value', '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') }) @@ -444,10 +447,10 @@ describe('ipns', function () { // uses microsecond precision. The value is a timestamp as defined by // rfc3339 which doesn't have a strong opinion on fractions of seconds so // both are valid but we must be able to round trip them intact. - const unmarshalled = ipns.unmarshal(kuboRecord.bytes) - const remarhshalled = ipns.marshal(unmarshalled) + const unmarshalled = unmarshalIPNSRecord(kuboRecord.bytes) + const remarhshalled = marshalIPNSRecord(unmarshalled) - const reUnmarshalled = ipns.unmarshal(remarhshalled) + const reUnmarshalled = unmarshalIPNSRecord(remarhshalled) expect(unmarshalled).to.deep.equal(reUnmarshalled) expect(remarhshalled).to.equalBytes(kuboRecord.bytes) diff --git a/test/selector.spec.ts b/test/selector.spec.ts index 20295c8..e63909d 100644 --- a/test/selector.spec.ts +++ b/test/selector.spec.ts @@ -1,35 +1,32 @@ /* eslint-env mocha */ import { generateKeyPair } from '@libp2p/crypto/keys' -import { peerIdFromKeys } from '@libp2p/peer-id' import { expect } from 'aegir/chai' -import * as ipns from '../src/index.js' +import { createIPNSRecord, marshalIPNSRecord, publicKeyToIPNSRoutingKey } from '../src/index.js' import { ipnsSelector } from '../src/selector.js' -import { marshal, peerIdToRoutingKey } from '../src/utils.js' -import type { PeerId } from '@libp2p/interface' +import type { PrivateKey } from '@libp2p/interface' describe('selector', function () { this.timeout(20 * 1000) const contentPath = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' - let peerId: PeerId + let privateKey: PrivateKey before(async () => { - const rsa = await generateKeyPair('RSA', 2048) - peerId = await peerIdFromKeys(rsa.public.bytes, rsa.bytes) + privateKey = await generateKeyPair('RSA', 2048) }) it('should use validator.select to select the record with the highest sequence number', async () => { const sequence = 0 const lifetime = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, lifetime) - const newRecord = await ipns.create(peerId, contentPath, (sequence + 1), lifetime) + const record = await createIPNSRecord(privateKey, contentPath, sequence, lifetime) + const newRecord = await createIPNSRecord(privateKey, contentPath, (sequence + 1), lifetime) - const marshalledData = marshal(record) - const marshalledNewData = marshal(newRecord) + const marshalledData = marshalIPNSRecord(record) + const marshalledNewData = marshalIPNSRecord(newRecord) - const key = peerIdToRoutingKey(peerId) + const key = publicKeyToIPNSRoutingKey(privateKey.publicKey) let valid = ipnsSelector(key, [marshalledNewData, marshalledData]) expect(valid).to.equal(0) // new data is the selected one @@ -42,13 +39,13 @@ describe('selector', function () { const sequence = 0 const lifetime = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, lifetime) - const newRecord = await ipns.create(peerId, contentPath, sequence, (lifetime + 1)) + const record = await createIPNSRecord(privateKey, contentPath, sequence, lifetime) + const newRecord = await createIPNSRecord(privateKey, contentPath, sequence, (lifetime + 1)) - const marshalledData = marshal(record) - const marshalledNewData = marshal(newRecord) + const marshalledData = marshalIPNSRecord(record) + const marshalledNewData = marshalIPNSRecord(newRecord) - const key = peerIdToRoutingKey(peerId) + const key = publicKeyToIPNSRoutingKey(privateKey.publicKey) let valid = ipnsSelector(key, [marshalledNewData, marshalledData]) expect(valid).to.equal(0) // new data is the selected one diff --git a/test/validator.spec.ts b/test/validator.spec.ts index 11a0c2e..fe73028 100644 --- a/test/validator.spec.ts +++ b/test/validator.spec.ts @@ -1,43 +1,32 @@ /* eslint-env mocha */ import { randomBytes } from '@libp2p/crypto' -import { generateKeyPair } from '@libp2p/crypto/keys' -import { peerIdFromKeys } from '@libp2p/peer-id' +import { generateKeyPair, publicKeyToProtobuf } from '@libp2p/crypto/keys' import { expect } from 'aegir/chai' -import { base58btc } from 'multiformats/bases/base58' -import { concat as uint8ArrayConcat } from 'uint8arrays/concat' -import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import * as ERRORS from '../src/errors.js' -import * as ipns from '../src/index.js' -import { marshal, peerIdToRoutingKey } from '../src/utils.js' +import { InvalidEmbeddedPublicKeyError, RecordTooLargeError, SignatureVerificationError } from '../src/errors.js' +import { createIPNSRecord, marshalIPNSRecord, publicKeyToIPNSRoutingKey } from '../src/index.js' import { ipnsValidator } from '../src/validator.js' -import type { PeerId } from '@libp2p/interface' +import type { PrivateKey } from '@libp2p/interface' describe('validator', function () { this.timeout(20 * 1000) const contentPath = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' - let peerId1: PeerId - let peerId2: PeerId + let privateKey1: PrivateKey + let privateKey2: PrivateKey before(async () => { - const rsa = await generateKeyPair('RSA', 2048) - peerId1 = await peerIdFromKeys(rsa.public.bytes, rsa.bytes) - - const rsa2 = await generateKeyPair('RSA', 2048) - peerId2 = await peerIdFromKeys(rsa2.public.bytes, rsa2.bytes) + privateKey1 = await generateKeyPair('RSA', 2048) + privateKey2 = await generateKeyPair('RSA', 2048) }) it('should validate a (V2) record', async () => { const sequence = 0 const validity = 1000000 - - const record = await ipns.create(peerId1, contentPath, sequence, validity, { v1Compatible: false }) - const marshalledData = marshal(record) - - const keyBytes = base58btc.decode(`z${peerId1.toString()}`) - const key = uint8ArrayConcat([uint8ArrayFromString('/ipns/'), keyBytes]) + const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: false }) + const marshalledData = marshalIPNSRecord(record) + const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey) await ipnsValidator(key, marshalledData) }) @@ -45,12 +34,9 @@ describe('validator', function () { it('should validate a (V1+V2) record', async () => { const sequence = 0 const validity = 1000000 - - const record = await ipns.create(peerId1, contentPath, sequence, validity, { v1Compatible: true }) - const marshalledData = marshal(record) - - const keyBytes = base58btc.decode(`z${peerId1.toString()}`) - const key = uint8ArrayConcat([uint8ArrayFromString('/ipns/'), keyBytes]) + const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: true }) + const marshalledData = marshalIPNSRecord(record) + const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey) await ipnsValidator(key, marshalledData) }) @@ -59,46 +45,50 @@ describe('validator', function () { const sequence = 0 const validity = 1000000 - const record = await ipns.create(peerId1, contentPath, sequence, validity) + const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity) // corrupt the record by changing the value to random bytes record.value = uint8ArrayToString(randomBytes(record.value?.length ?? 0)) - const marshalledData = marshal(record) + const marshalledData = marshalIPNSRecord(record) - const key = peerIdToRoutingKey(peerId1) + const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey) - await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) + await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() + .with.property('name', SignatureVerificationError.name) }) it('should use validator.validate to verify that a record is not valid when it is passed with the wrong IPNS key', async () => { const sequence = 0 const validity = 1000000 - const record = await ipns.create(peerId1, contentPath, sequence, validity) - const marshalledData = marshal(record) + const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity) + const marshalledData = marshalIPNSRecord(record) - const key = peerIdToRoutingKey(peerId2) + const key = publicKeyToIPNSRoutingKey(privateKey2.publicKey) - await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected().with.property('code', ERRORS.ERR_INVALID_EMBEDDED_KEY) + await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() + .with.property('name', InvalidEmbeddedPublicKeyError.name) }) it('should use validator.validate to verify that a record is not valid when the wrong key is embedded', async () => { const sequence = 0 const validity = 1000000 - const record = await ipns.create(peerId1, contentPath, sequence, validity) - record.pubKey = peerId2.publicKey - const marshalledData = marshal(record) + const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity) + record.pubKey = publicKeyToProtobuf(privateKey2.publicKey) + const marshalledData = marshalIPNSRecord(record) - const key = peerIdToRoutingKey(peerId1) + const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey) - await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected().with.property('code', ERRORS.ERR_INVALID_EMBEDDED_KEY) + await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() + .with.property('name', InvalidEmbeddedPublicKeyError.name) }) it('should limit the size of incoming records', async () => { const marshalledData = new Uint8Array(1024 * 1024) const key = new Uint8Array() - await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected().with.property('code', ERRORS.ERR_RECORD_TOO_LARGE) + await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() + .with.property('name', RecordTooLargeError.name) }) }) From 927cc47d2f975eba7da9b3b2e1e62e56193a8a9a Mon Sep 17 00:00:00 2001 From: achingbrain Date: Wed, 11 Sep 2024 16:59:20 +0100 Subject: [PATCH 2/3] chore: be stricter about cid types --- src/utils.ts | 4 ++-- test/index.spec.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index f210830..e7a8424 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -139,7 +139,7 @@ export const publicKeyToIPNSRoutingKey = (publicKey: PublicKey): Uint8Array => { return multihashToIPNSRoutingKey(publicKey.toMultihash()) } -export const multihashToIPNSRoutingKey = (digest: MultihashDigest): Uint8Array => { +export const multihashToIPNSRoutingKey = (digest: MultihashDigest<0x00 | 0x12>): Uint8Array => { return uint8ArrayConcat([ IPNS_PREFIX, digest.bytes @@ -153,7 +153,7 @@ export const publicKeyFromIPNSRoutingKey = (key: Uint8Array): Ed25519PublicKey | } catch {} } -export const multihashFromIPNSRoutingKey = (key: Uint8Array): MultihashDigest<0x00> | MultihashDigest<0x12> => { +export const multihashFromIPNSRoutingKey = (key: Uint8Array): MultihashDigest<0x00 | 0x12> => { const digest = Digest.decode(key.slice(IPNS_PREFIX.length)) if (digest.code !== 0x00 && digest.code !== 0x12) { diff --git a/test/index.spec.ts b/test/index.spec.ts index cdc9552..abb5b22 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -356,6 +356,7 @@ describe('ipns', function () { keys.forEach(key => { const digest = Digest.decode(base58btc.decode(`z${key}`)) + // @ts-expect-error digest may have the wrong hash type const routingKey = multihashToIPNSRoutingKey(digest) const id = multihashFromIPNSRoutingKey(routingKey) From 67bdddf0726111a5a272c1cf938ad3f428c8deb0 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 12 Sep 2024 09:01:41 +0100 Subject: [PATCH 3/3] chore: simplify --- src/index.ts | 21 ++++--- src/utils.ts | 128 +++++++++++++++++++++++------------------ src/validator.ts | 13 ++++- test/index.spec.ts | 26 ++++----- test/selector.spec.ts | 6 +- test/utils.spec.ts | 24 +++++--- test/validator.spec.ts | 12 ++-- 7 files changed, 132 insertions(+), 98 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7dca7c9..041f9d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { IpnsEntry } from './pb/ipns.js' import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, normalizeValue } from './utils.js' import type { PrivateKey, PublicKey } from '@libp2p/interface' import type { CID } from 'multiformats/cid' +import type { MultihashDigest } from 'multiformats/hashes/interface' const log = logger('ipns') const DEFAULT_TTL_NS = 60 * 60 * 1e+9 // 1 Hour or 3600 Seconds @@ -157,10 +158,10 @@ const defaultCreateOptions: CreateOptions = { * @param {number} lifetime - lifetime of the record (in milliseconds). * @param {CreateOptions} options - additional create options. */ -export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise -export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise -export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise -export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise { +export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise +export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise +export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise +export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise { // Validity in ISOString with nanoseconds precision and validity type EOL const expirationDate = new NanoDate(Date.now() + Number(lifetime)) const validityType = IpnsEntry.ValidityType.EOL @@ -185,10 +186,10 @@ export async function createIPNSRecord (privateKey: PrivateKey, value: CID | Pub * @param {string} expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision. * @param {CreateOptions} options - additional creation options. */ -export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise -export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise -export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateOptions): Promise -export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise { +export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise +export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise +export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options: CreateOptions): Promise +export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise { const expirationDate = NanoDate.fromString(expiration) const validityType = IpnsEntry.ValidityType.EOL const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS) @@ -196,7 +197,7 @@ export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, va return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options) } -const _create = async (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise => { +const _create = async (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise => { seq = BigInt(seq) const isoValidity = uint8ArrayFromString(validity) const normalizedValue = normalizeValue(value) @@ -254,8 +255,6 @@ export { unmarshalIPNSRecord } from './utils.js' export { marshalIPNSRecord } from './utils.js' export { multihashToIPNSRoutingKey } from './utils.js' export { multihashFromIPNSRoutingKey } from './utils.js' -export { publicKeyToIPNSRoutingKey } from './utils.js' -export { publicKeyFromIPNSRoutingKey } from './utils.js' export { extractPublicKeyFromIPNSRecord } from './utils.js' /** diff --git a/src/utils.ts b/src/utils.ts index e7a8424..ddd77ea 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { publicKeyFromMultihash, publicKeyFromProtobuf } from '@libp2p/crypto/keys' +import { publicKeyFromProtobuf } from '@libp2p/crypto/keys' import { InvalidMultihashError } from '@libp2p/interface' import { logger } from '@libp2p/logger' import * as cborg from 'cborg' @@ -12,17 +12,19 @@ import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { InvalidRecordDataError, InvalidValueError, SignatureVerificationError, UnsupportedValidityError } from './errors.js' import { IpnsEntry } from './pb/ipns.js' import type { IPNSRecord, IPNSRecordV2, IPNSRecordData } from './index.js' -import type { PublicKey, Ed25519PublicKey, Secp256k1PublicKey } from '@libp2p/interface' +import type { PublicKey } from '@libp2p/interface' const log = logger('ipns:utils') const IPNS_PREFIX = uint8ArrayFromString('/ipns/') -const LIBP2P_CID_CODEC = 114 +const LIBP2P_CID_CODEC = 0x72 +const IDENTITY_CODEC = 0x0 +const SHA2_256_CODEC = 0x12 /** * Extracts a public key from the passed PeerId, falling back to the pubKey * embedded in the ipns record */ -export const extractPublicKeyFromIPNSRecord = (record: IPNSRecord | IPNSRecordV2): PublicKey | undefined => { +export function extractPublicKeyFromIPNSRecord (record: IPNSRecord | IPNSRecordV2): PublicKey | undefined { let pubKey: PublicKey | undefined if (record.pubKey != null) { @@ -42,7 +44,7 @@ export const extractPublicKeyFromIPNSRecord = (record: IPNSRecord | IPNSRecordV2 /** * Utility for creating the record data for being signed */ -export const ipnsRecordDataForV1Sig = (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array): Uint8Array => { +export function ipnsRecordDataForV1Sig (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array): Uint8Array { const validityTypeBuffer = uint8ArrayFromString(validityType) return uint8ArrayConcat([value, validity, validityTypeBuffer]) @@ -51,13 +53,13 @@ export const ipnsRecordDataForV1Sig = (value: Uint8Array, validityType: IpnsEntr /** * Utility for creating the record data for being signed */ -export const ipnsRecordDataForV2Sig = (data: Uint8Array): Uint8Array => { +export function ipnsRecordDataForV2Sig (data: Uint8Array): Uint8Array { const entryData = uint8ArrayFromString('ipns-signature:') return uint8ArrayConcat([entryData, data]) } -export const marshalIPNSRecord = (obj: IPNSRecord | IPNSRecordV2): Uint8Array => { +export function marshalIPNSRecord (obj: IPNSRecord | IPNSRecordV2): Uint8Array { if ('signatureV1' in obj) { return IpnsEntry.encode({ value: uint8ArrayFromString(obj.value), @@ -100,7 +102,7 @@ export function unmarshalIPNSRecord (buf: Uint8Array): IPNSRecord { } const data = parseCborData(message.data) - const value = normalizeValue(data.Value) + const value = normalizeByteValue(data.Value) const validity = uint8ArrayToString(data.Validity) if (message.value != null && message.signatureV1 != null) { @@ -135,36 +137,24 @@ export function unmarshalIPNSRecord (buf: Uint8Array): IPNSRecord { } } -export const publicKeyToIPNSRoutingKey = (publicKey: PublicKey): Uint8Array => { - return multihashToIPNSRoutingKey(publicKey.toMultihash()) -} - -export const multihashToIPNSRoutingKey = (digest: MultihashDigest<0x00 | 0x12>): Uint8Array => { +export function multihashToIPNSRoutingKey (digest: MultihashDigest<0x00 | 0x12>): Uint8Array { return uint8ArrayConcat([ IPNS_PREFIX, digest.bytes ]) } -export const publicKeyFromIPNSRoutingKey = (key: Uint8Array): Ed25519PublicKey | Secp256k1PublicKey | undefined => { - try { - // @ts-expect-error digest code may not be 0 - return publicKeyFromMultihash(multihashFromIPNSRoutingKey(key)) - } catch {} -} - -export const multihashFromIPNSRoutingKey = (key: Uint8Array): MultihashDigest<0x00 | 0x12> => { +export function multihashFromIPNSRoutingKey (key: Uint8Array): MultihashDigest<0x00 | 0x12> { const digest = Digest.decode(key.slice(IPNS_PREFIX.length)) - if (digest.code !== 0x00 && digest.code !== 0x12) { + if (!isCodec(digest, IDENTITY_CODEC) && !isCodec(digest, SHA2_256_CODEC)) { throw new InvalidMultihashError('Multihash in IPNS key was not identity or sha2-256') } - // @ts-expect-error digest may not have correct code even though we just checked return digest } -export const createCborData = (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array, sequence: bigint, ttl: bigint): Uint8Array => { +export function createCborData (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array, sequence: bigint, ttl: bigint): Uint8Array { let ValidityType if (validityType === IpnsEntry.ValidityType.EOL) { @@ -184,7 +174,7 @@ export const createCborData = (value: Uint8Array, validityType: IpnsEntry.Validi return cborg.encode(data) } -export const parseCborData = (buf: Uint8Array): IPNSRecordData => { +export function parseCborData (buf: Uint8Array): IPNSRecordData { const data = cborg.decode(buf) if (data.ValidityType === 0) { @@ -206,35 +196,40 @@ export const parseCborData = (buf: Uint8Array): IPNSRecordData => { return data } +export function normalizeByteValue (value: Uint8Array): string { + const string = uint8ArrayToString(value).trim() + + // if we have a path, check it is a valid path + if (string.startsWith('/')) { + return string + } + + // try parsing what we have as CID bytes or a CID string + try { + return `/ipfs/${CID.decode(value).toV1().toString()}` + } catch { + // fall through + } + + try { + return `/ipfs/${CID.parse(string).toV1().toString()}` + } catch { + // fall through + } + + throw new InvalidValueError('Value must be a valid content path starting with /') +} + /** * Normalizes the given record value. It ensures it is a PeerID, a CID or a * string starting with '/'. PeerIDs become `/ipns/${cidV1Libp2pKey}`, * CIDs become `/ipfs/${cidAsV1}`. */ -export const normalizeValue = (value?: CID | PublicKey | string | Uint8Array): string => { +export function normalizeValue (value?: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string): string { if (value != null) { - // if we have a PeerId, turn it into an ipns path - if (hasToCID(value)) { - return `/ipns/${value.toCID().toString(base36)}` - } - - // if the value is bytes, stringify it and see if we have a path - if (value instanceof Uint8Array) { - const string = uint8ArrayToString(value) - - if (string.startsWith('/')) { - value = string - } - } - - // if we have a path, check it is a valid path - const string = value.toString().trim() - if (string.startsWith('/') && string.length > 1) { - return string - } + const cid = asCID(value) // if we have a CID, turn it into an ipfs path - const cid = CID.asCID(value) if (cid != null) { // PeerID encoded as a CID if (cid.code === LIBP2P_CID_CODEC) { @@ -244,22 +239,22 @@ export const normalizeValue = (value?: CID | PublicKey | string | Uint8Array): s return `/ipfs/${cid.toV1().toString()}` } - // try parsing what we have as CID bytes or a CID string - try { - if (value instanceof Uint8Array) { - return `/ipfs/${CID.decode(value).toV1().toString()}` - } + if (hasBytes(value)) { + return `/ipns/${base36.encode(value.bytes)}` + } + + // if we have a path, check it is a valid path + const string = value.toString().trim() - return `/ipfs/${CID.parse(string).toV1().toString()}` - } catch { - // fall through + if (string.startsWith('/') && string.length > 1) { + return string } } throw new InvalidValueError('Value must be a valid content path starting with /') } -const validateCborDataMatchesPbData = (entry: IpnsEntry): void => { +function validateCborDataMatchesPbData (entry: IpnsEntry): void { if (entry.data == null) { throw new InvalidRecordDataError('Record data is missing') } @@ -287,6 +282,29 @@ const validateCborDataMatchesPbData = (entry: IpnsEntry): void => { } } +function hasBytes (obj?: any): obj is { bytes: Uint8Array } { + return obj.bytes instanceof Uint8Array +} + function hasToCID (obj?: any): obj is { toCID(): CID } { return typeof obj?.toCID === 'function' } + +function asCID (obj?: any): CID | null { + if (hasToCID(obj)) { + return obj.toCID() + } + + // try parsing as a CID string + try { + return CID.parse(obj) + } catch { + // fall through + } + + return CID.asCID(obj) +} + +export function isCodec (digest: MultihashDigest, codec: T): digest is MultihashDigest { + return digest.code === codec +} diff --git a/src/validator.ts b/src/validator.ts index c4a41ac..2671c39 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -1,9 +1,10 @@ +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.js' import { IpnsEntry } from './pb/ipns.js' -import { extractPublicKeyFromIPNSRecord, ipnsRecordDataForV2Sig, publicKeyFromIPNSRoutingKey, publicKeyToIPNSRoutingKey, unmarshalIPNSRecord } from './utils.js' +import { extractPublicKeyFromIPNSRecord, ipnsRecordDataForV2Sig, isCodec, multihashFromIPNSRoutingKey, multihashToIPNSRoutingKey, unmarshalIPNSRecord } from './utils.js' import type { PublicKey } from '@libp2p/interface' const log = logger('ipns:validator') @@ -58,7 +59,13 @@ export async function ipnsValidator (key: Uint8Array, marshalledData: Uint8Array } // try to extract public key from routing key - const routingPubKey = publicKeyFromIPNSRoutingKey(key) + const routingMultihash = multihashFromIPNSRoutingKey(key) + let routingPubKey: PublicKey | undefined + + // identity hash + if (isCodec(routingMultihash, 0x0)) { + routingPubKey = publicKeyFromMultihash(routingMultihash) + } // extract public key from record const receivedRecord = unmarshalIPNSRecord(marshalledData) @@ -68,7 +75,7 @@ export async function ipnsValidator (key: Uint8Array, marshalledData: Uint8Array throw new InvalidEmbeddedPublicKeyError('Could not extract public key from IPNS record or routing key') } - const routingKey = publicKeyToIPNSRoutingKey(recordPubKey) + const routingKey = multihashToIPNSRoutingKey(recordPubKey.toMultihash()) if (!uint8ArrayEquals(key, routingKey)) { throw new InvalidEmbeddedPublicKeyError('Embedded public key did not match routing key') diff --git a/test/index.spec.ts b/test/index.spec.ts index abb5b22..6b9497c 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -14,7 +14,7 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { InvalidEmbeddedPublicKeyError, InvalidValueError, RecordExpiredError, SignatureVerificationError } from '../src/errors.js' import { createIPNSRecord, createIPNSRecordWithExpiration } from '../src/index.js' import { IpnsEntry } from '../src/pb/ipns.js' -import { extractPublicKeyFromIPNSRecord, parseCborData, createCborData, ipnsRecordDataForV2Sig, marshalIPNSRecord, unmarshalIPNSRecord, publicKeyToIPNSRoutingKey, multihashToIPNSRoutingKey, multihashFromIPNSRoutingKey } from '../src/utils.js' +import { extractPublicKeyFromIPNSRecord, parseCborData, createCborData, ipnsRecordDataForV2Sig, marshalIPNSRecord, unmarshalIPNSRecord, multihashToIPNSRoutingKey, multihashFromIPNSRoutingKey } from '../src/utils.js' import { ipnsValidator } from '../src/validator.js' import { kuboRecord } from './fixtures/records.js' import type { PrivateKey } from '@libp2p/interface' @@ -108,7 +108,7 @@ describe('ipns', function () { const record = await createIPNSRecordWithExpiration(privateKey, contentPath, sequence, expiration) const marshalledRecord = marshalIPNSRecord(record) - await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord) + await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledRecord) const pb = IpnsEntry.decode(marshalledRecord) expect(pb).to.have.property('validity') @@ -122,7 +122,7 @@ describe('ipns', function () { const record = await createIPNSRecordWithExpiration(privateKey, contentPath, sequence, expiration, { v1Compatible: false }) const marshalledRecord = marshalIPNSRecord(record) - await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord) + await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledRecord) const pb = IpnsEntry.decode(marshalIPNSRecord(record)) expect(pb).to.not.have.property('validity') @@ -141,7 +141,7 @@ describe('ipns', function () { }) const marshalledRecord = marshalIPNSRecord(record) - await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord) + await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledRecord) const pb = IpnsEntry.decode(marshalledRecord) const data = parseCborData(pb.data ?? new Uint8Array(0)) @@ -159,7 +159,7 @@ describe('ipns', function () { }) const marshalledRecord = marshalIPNSRecord(record) - await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord) + await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledRecord) const pb = IpnsEntry.decode(marshalledRecord) expect(pb).to.not.have.property('ttl') @@ -173,7 +173,7 @@ describe('ipns', function () { const validity = 1000000 const record = await createIPNSRecord(privateKey, contentPath, sequence, validity) - await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record)) + await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalIPNSRecord(record)) }) it('should create an ipns record (V2) and validate it correctly', async () => { @@ -181,7 +181,7 @@ describe('ipns', function () { const validity = 1000000 const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, { v1Compatible: false }) - await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record)) + await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalIPNSRecord(record)) }) it('should normalize value when creating an ipns record (arbitrary string path)', async () => { @@ -281,7 +281,7 @@ describe('ipns', function () { // confirm a v1 exists expect(pb).to.have.property('signatureV1') - await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), IpnsEntry.encode(pb))).to.eventually.be.rejected() + await expect(ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), IpnsEntry.encode(pb))).to.eventually.be.rejected() .with.property('name', SignatureVerificationError.name) }) @@ -298,7 +298,7 @@ describe('ipns', function () { // confirm a v1 exists expect(pb).to.have.property('signatureV1') - await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), IpnsEntry.encode(pb))).to.eventually.be.rejected() + await expect(ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), IpnsEntry.encode(pb))).to.eventually.be.rejected() .with.property('name', SignatureVerificationError.name) }) @@ -311,7 +311,7 @@ describe('ipns', function () { // corrupt the record by changing the value to random bytes record.value = uint8ArrayToString(randomBytes(46)) - await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record))).to.eventually.be.rejected() + await expect(ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalIPNSRecord(record))).to.eventually.be.rejected() .with.property('name', SignatureVerificationError.name) }) @@ -323,7 +323,7 @@ describe('ipns', function () { await new Promise(resolve => setTimeout(resolve, 1)) - await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record))).to.eventually.be.rejected() + await expect(ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalIPNSRecord(record))).to.eventually.be.rejected() .with.property('name', RecordExpiredError.name) }) @@ -345,7 +345,7 @@ describe('ipns', function () { expect(createdRecord.signatureV2).to.equalBytes(unmarshalledData.signatureV2) expect(createdRecord.data).to.equalBytes(unmarshalledData.data) - await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledData) + await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledData) }) it('should be able to turn routing key back into id', () => { @@ -399,7 +399,7 @@ describe('ipns', function () { delete record.pubKey const marshalledData = marshalIPNSRecord(record) - const key = publicKeyToIPNSRoutingKey(privateKey.publicKey) + const key = multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()) await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() .with.property('name', InvalidEmbeddedPublicKeyError.name) diff --git a/test/selector.spec.ts b/test/selector.spec.ts index e63909d..e263d87 100644 --- a/test/selector.spec.ts +++ b/test/selector.spec.ts @@ -2,7 +2,7 @@ import { generateKeyPair } from '@libp2p/crypto/keys' import { expect } from 'aegir/chai' -import { createIPNSRecord, marshalIPNSRecord, publicKeyToIPNSRoutingKey } from '../src/index.js' +import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey } from '../src/index.js' import { ipnsSelector } from '../src/selector.js' import type { PrivateKey } from '@libp2p/interface' @@ -26,7 +26,7 @@ describe('selector', function () { const marshalledData = marshalIPNSRecord(record) const marshalledNewData = marshalIPNSRecord(newRecord) - const key = publicKeyToIPNSRoutingKey(privateKey.publicKey) + const key = multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()) let valid = ipnsSelector(key, [marshalledNewData, marshalledData]) expect(valid).to.equal(0) // new data is the selected one @@ -45,7 +45,7 @@ describe('selector', function () { const marshalledData = marshalIPNSRecord(record) const marshalledNewData = marshalIPNSRecord(newRecord) - const key = publicKeyToIPNSRoutingKey(privateKey.publicKey) + const key = multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()) let valid = ipnsSelector(key, [marshalledNewData, marshalledData]) expect(valid).to.equal(0) // new data is the selected one diff --git a/test/utils.spec.ts b/test/utils.spec.ts index 86467b1..69efc9a 100644 --- a/test/utils.spec.ts +++ b/test/utils.spec.ts @@ -2,7 +2,7 @@ import { peerIdFromString } from '@libp2p/peer-id' import { expect } from 'aegir/chai' import { CID } from 'multiformats/cid' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import { normalizeValue, peerIdFromRoutingKey, peerIdToRoutingKey } from '../src/utils.js' +import { normalizeValue, multihashFromIPNSRoutingKey, multihashToIPNSRoutingKey, normalizeByteValue } from '../src/utils.js' import type { PeerId } from '@libp2p/interface' describe('utils', () => { @@ -40,8 +40,18 @@ describe('utils', () => { 'string path': { input: '/hello', output: '/hello' - }, + } + } + + Object.entries(cases).forEach(([name, { input, output }]) => { + it(`should normalize a ${name}`, async () => { + expect(normalizeValue(await input)).to.equal(output) + }) + }) + }) + describe('normalizeByteValue', () => { + const cases: Record = { // Uint8Array input 'v0 CID bytes': { input: CID.parse('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq').bytes, @@ -74,8 +84,8 @@ describe('utils', () => { } Object.entries(cases).forEach(([name, { input, output }]) => { - it(`should normalize a ${name}`, async () => { - expect(normalizeValue(await input)).to.equal(output) + it(`should normalize a ${name}`, () => { + expect(normalizeByteValue(input)).to.equal(output) }) }) }) @@ -89,10 +99,10 @@ describe('utils', () => { Object.entries(cases).forEach(([name, input]) => { it(`should round trip a ${name} key`, async () => { - const key = peerIdToRoutingKey(input) - const output = peerIdFromRoutingKey(key) + const key = multihashToIPNSRoutingKey(input.toMultihash()) + const output = multihashFromIPNSRoutingKey(key) - expect(input.equals(output)).to.be.true() + expect(input.toMultihash().bytes).to.equalBytes(output.bytes) }) }) }) diff --git a/test/validator.spec.ts b/test/validator.spec.ts index fe73028..673d443 100644 --- a/test/validator.spec.ts +++ b/test/validator.spec.ts @@ -5,7 +5,7 @@ import { generateKeyPair, publicKeyToProtobuf } from '@libp2p/crypto/keys' import { expect } from 'aegir/chai' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { InvalidEmbeddedPublicKeyError, RecordTooLargeError, SignatureVerificationError } from '../src/errors.js' -import { createIPNSRecord, marshalIPNSRecord, publicKeyToIPNSRoutingKey } from '../src/index.js' +import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey } from '../src/index.js' import { ipnsValidator } from '../src/validator.js' import type { PrivateKey } from '@libp2p/interface' @@ -26,7 +26,7 @@ describe('validator', function () { const validity = 1000000 const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: false }) const marshalledData = marshalIPNSRecord(record) - const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey) + const key = multihashToIPNSRoutingKey(privateKey1.publicKey.toMultihash()) await ipnsValidator(key, marshalledData) }) @@ -36,7 +36,7 @@ describe('validator', function () { const validity = 1000000 const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: true }) const marshalledData = marshalIPNSRecord(record) - const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey) + const key = multihashToIPNSRoutingKey(privateKey1.publicKey.toMultihash()) await ipnsValidator(key, marshalledData) }) @@ -51,7 +51,7 @@ describe('validator', function () { record.value = uint8ArrayToString(randomBytes(record.value?.length ?? 0)) const marshalledData = marshalIPNSRecord(record) - const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey) + const key = multihashToIPNSRoutingKey(privateKey1.publicKey.toMultihash()) await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() .with.property('name', SignatureVerificationError.name) @@ -64,7 +64,7 @@ describe('validator', function () { const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity) const marshalledData = marshalIPNSRecord(record) - const key = publicKeyToIPNSRoutingKey(privateKey2.publicKey) + const key = multihashToIPNSRoutingKey(privateKey2.publicKey.toMultihash()) await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() .with.property('name', InvalidEmbeddedPublicKeyError.name) @@ -78,7 +78,7 @@ describe('validator', function () { record.pubKey = publicKeyToProtobuf(privateKey2.publicKey) const marshalledData = marshalIPNSRecord(record) - const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey) + const key = multihashToIPNSRoutingKey(privateKey1.publicKey.toMultihash()) await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected() .with.property('name', InvalidEmbeddedPublicKeyError.name)