diff --git a/README.md b/README.md index e6cecef..b546d87 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![codecov](https://img.shields.io/codecov/c/github/ipfs/js-ipns.svg?style=flat-square)](https://codecov.io/gh/ipfs/js-ipns) [![CI](https://img.shields.io/github/actions/workflow/status/ipfs/js-ipns/js-test-and-release.yml?branch=master\&style=flat-square)](https://github.com/ipfs/js-ipns/actions/workflows/js-test-and-release.yml?query=branch%3Amaster) -> ipns record definitions +> IPNS Record definitions ## Table of contents @@ -54,7 +54,7 @@ This module contains all the necessary code for creating, understanding and vali ```js import * as ipns from 'ipns' -const entryData = await ipns.create(privateKey, value, sequenceNumber, lifetime) +const ipnsRecord = await ipns.create(privateKey, value, sequenceNumber, lifetime) ``` ### Validate record @@ -62,7 +62,7 @@ const entryData = await ipns.create(privateKey, value, sequenceNumber, lifetime) ```js import * as ipns from 'ipns' -await ipns.validate(publicKey, ipnsEntry) +await ipns.validate(publicKey, marshalledData) // if no error thrown, the record is valid ``` @@ -71,7 +71,7 @@ await ipns.validate(publicKey, ipnsEntry) ```js import * as ipns from 'ipns' -const ipnsEntryWithEmbedPublicKey = await ipns.embedPublicKey(publicKey, ipnsEntry) +const ipnsRecordWithEmbeddedPublicKey = await ipns.embedPublicKey(publicKey, ipnsRecord) ``` ### Extract public key from record @@ -79,7 +79,7 @@ const ipnsEntryWithEmbedPublicKey = await ipns.embedPublicKey(publicKey, ipnsEnt ```js import * as ipns from 'ipns' -const publicKey = await ipns.extractPublicKey(peerId, ipnsEntry) +const publicKey = await ipns.extractPublicKey(peerId, ipnsRecord) ``` ### Datastore key @@ -90,7 +90,7 @@ import * as ipns from 'ipns' ipns.getLocalKey(peerId) ``` -Returns a key to be used for storing the ipns entry locally, that is: +Returns a key to be used for storing the IPNS record locally, that is: ``` /ipns/${base32()} @@ -101,23 +101,23 @@ Returns a key to be used for storing the ipns entry locally, that is: ```js import * as ipns from 'ipns' -const entryData = await ipns.create(privateKey, value, sequenceNumber, lifetime) +const ipnsRecord = await ipns.create(privateKey, value, sequenceNumber, lifetime) // ... -const marshalledData = ipns.marshal(entryData) +const marshalledData = ipns.marshal(ipnsRecord) // ... ``` -Returns the entry data serialized. +Returns the record data serialized. ### Unmarshal data from proto buffer ```js import * as ipns from 'ipns' -const data = ipns.unmarshal(storedData) +const ipnsRecord = ipns.unmarshal(storedData) ``` -Returns the entry data structure after being serialized. +Returns the `IPNSRecord` after being deserialized. ### Validator @@ -131,7 +131,7 @@ Contains an object with `validate (marshalledData, key)` and `select (dataA, dat The `validate` async function aims to verify if an IPNS record is valid. First the record is unmarshalled, then the public key is obtained and finally the record is validated (`signatureV2` of CBOR `data` is verified). -The `select` function is responsible for deciding which ipns record is the best (newer) between two records. Both records are unmarshalled and their sequence numbers are compared. If the first record provided is the newer, the operation result will be `0`, otherwise the operation result will be `1`. +The `select` function is responsible for deciding which IPNS record is the best (newer) between two records. Both records are unmarshalled and their sequence numbers are compared. If the first record provided is the newer, the operation result will be `0`, otherwise the operation result will be `1`. ## API @@ -139,52 +139,41 @@ The `select` function is responsible for deciding which ipns record is the best ```js -ipns.create(privateKey, value, sequenceNumber, lifetime) +ipns.create(privateKey, value, sequenceNumber, lifetime, options) ``` Create an IPNS record for being stored in a protocol buffer. - `privateKey` (`PrivKey` [RSA Instance](https://github.com/libp2p/js-libp2p-crypto/blob/master/src/keys/rsa-class.js)): key to be used for cryptographic operations. -- `value` (Uint8Array): ipfs path of the object to be published. +- `value` (string): IPFS path of the object to be published. - `sequenceNumber` (Number): number representing the current version of the record. - `lifetime` (Number): lifetime of the record (in milliseconds). +- `options` (CreateOptions): additional creation options. -Returns a `Promise` that resolves to an object with the entry's properties eg: - -```js -{ - value: Uint8Array, - signature: Uint8Array, // V1 (legacy, ignored) - validityType: 0, - validity: Uint8Array, - sequence: 2, - signatureV2: Uint8Array, // V2 signature of data field - data: Uint8Array // DAG-CBOR that was signed -} -``` +Returns a `Promise` that resolves to an object with a `IPNSRecord`. ### Validate record ```js -ipns.validate(publicKey, ipnsEntry) +ipns.validate(publicKey, ipnsRecord) ``` Validate an IPNS record previously stored in a protocol buffer. - `publicKey` (`PubKey` [RSA Instance](https://github.com/libp2p/js-libp2p-crypto/blob/master/src/keys/rsa-class.js)): key to be used for cryptographic operations. -- `ipnsEntry` (Object): ipns entry record (obtained using the create function). +- `ipnsRecord` (`IPNSRecord`): IPNS record (obtained using the create function). Returns a `Promise`, which may be rejected if the validation was not successful. ### Marshal data with proto buffer ```js -const marshalledData = ipns.marshal(entryData) +const marshalledData = ipns.marshal(ipnsRecord) ``` -Returns the entry data serialized. +Returns the serialized IPNS record. -- `entryData` (Object): ipns entry record (obtained using the create function). +- `ipnsRecord` (`IPNSRecord`): ipns record (obtained using the create function). ### Unmarshal data from proto buffer @@ -192,20 +181,20 @@ Returns the entry data serialized. const data = ipns.unmarshal(storedData) ``` -Returns the entry data structure after being serialized. +Returns a `IPNSRecord` after being serialized. -- `storedData` (Uint8Array): ipns entry record serialized. +- `storedData` (Uint8Array): ipns record serialized. ### Extract public key from record ```js -const publicKey = await ipns.extractPublicKey(peerId, ipnsEntry) +const publicKey = await ipns.extractPublicKey(peerId, ipnsRecord) ``` -Extract a public key from an IPNS entry. +Extract a public key from an IPNS record. - `peerId` (`PeerId` [Instance](https://github.com/libp2p/js-libp2p-peer-id/tree/master/packages/libp2p-peer-id)): peer identifier object. -- `ipnsEntry` (Object): ipns entry record (obtained using the create function). +- `ipnsRecord` (`IPNSRecord`): ipns record (obtained using the create function). Returns a `Promise` which resolves to public key ([`PublicKey`](https://github.com/libp2p/js-libp2p-interfaces/blob/master/packages/interface-keys/src/index.ts) ): may be used for cryptographic operations. diff --git a/package.json b/package.json index 9a57e07..78633fc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ipns", "version": "6.0.7", - "description": "ipns record definitions", + "description": "IPNS record definitions", "author": "Vasco Santos ", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/ipfs/js-ipns#readme", diff --git a/src/errors.ts b/src/errors.ts index 29dd232..969b5eb 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -7,6 +7,7 @@ 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' diff --git a/src/index.ts b/src/index.ts index 85912a4..25ba91c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,9 +10,10 @@ import { equals as uint8ArrayEquals } from 'uint8arrays/equals' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import * as ERRORS from './errors.js' import { IpnsEntry } from './pb/ipns.js' -import { createCborData, ipnsEntryDataForV1Sig, ipnsEntryDataForV2Sig } from './utils.js' +import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, normalizeValue } from './utils.js' import type { PrivateKey } from '@libp2p/interface/keys' import type { PeerId } from '@libp2p/interface/peer-id' +import type { CID } from 'multiformats/cid' const log = logger('ipns') const ID_MULTIHASH_CODE = identity.code @@ -20,19 +21,98 @@ const ID_MULTIHASH_CODE = identity.code export const namespace = '/ipns/' export const namespaceLength = namespace.length -export interface IPNSEntry { - value: Uint8Array - signature: Uint8Array // signature of the record - validityType: IpnsEntry.ValidityType // Type of validation being used - validity: Uint8Array // expiration datetime for the record in RFC3339 format - sequence: bigint // number representing the version of the record - ttl?: bigint // ttl in nanoseconds - pubKey?: Uint8Array // the public portion of the key that signed this record (only present if it was not embedded in the IPNS key) - signatureV2?: Uint8Array // the v2 signature of the record - data?: Uint8Array // extensible data +export interface IPNSRecordV1V2 { + /** + * value of the record + */ + value: string + + /** + * signature of the record + */ + signatureV1: Uint8Array + + /** + * Type of validation being used + */ + validityType: IpnsEntry.ValidityType + + /** + * expiration datetime for the record in RFC3339 format + */ + validity: NanoDate + + /** + * number representing the version of the record + */ + sequence: bigint + + /** + * ttl in nanoseconds + */ + ttl?: bigint + + /** + * the public portion of the key that signed this record (only present if it was not embedded in the IPNS key) + */ + pubKey?: Uint8Array + + /** + * the v2 signature of the record + */ + signatureV2: Uint8Array + + /** + * extensible data + */ + data: Uint8Array } -export interface IPNSEntryData { +export interface IPNSRecordV2 { + /** + * value of the record + */ + value: string + + /** + * the v2 signature of the record + */ + signatureV2: Uint8Array + + /** + * Type of validation being used + */ + validityType: IpnsEntry.ValidityType + + /** + * expiration datetime for the record in RFC3339 format + */ + validity: NanoDate + + /** + * number representing the version of the record + */ + sequence: bigint + + /** + * ttl in nanoseconds + */ + ttl?: bigint + + /** + * the public portion of the key that signed this record (only present if it was not embedded in the IPNS key) + */ + pubKey?: Uint8Array + + /** + * extensible data + */ + data: Uint8Array +} + +export type IPNSRecord = IPNSRecordV1V2 | IPNSRecordV2 + +export interface IPNSRecordData { Value: Uint8Array Validity: Uint8Array ValidityType: IpnsEntry.ValidityType @@ -47,69 +127,94 @@ export interface IDKeys { ipnsKey: Key } +export interface CreateOptions { + v1Compatible?: boolean +} + +export interface CreateV2OrV1Options { + v1Compatible: true +} + +export interface CreateV2Options { + v1Compatible: false +} + +const defaultCreateOptions: CreateOptions = { + v1Compatible: true +} + /** - * Creates a new ipns entry and signs it with the given private key. - * The ipns entry validity should follow the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision. + * Creates a new IPNS record and signs it with the given private key. + * The IPNS Record validity should follow the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision. * Note: This function does not embed the public key. If you want to do that, use `EmbedPublicKey`. * + * The passed value can be a CID, a PeerID or an arbitrary string path. + * + * * CIDs will be converted to v1 and stored in the record as a string similar to: `/ipfs/${cid}` + * * 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 {Uint8Array} value - value to be stored in the record. + * @param {CID | PeerId | 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 const create = async (peerId: PeerId, value: Uint8Array, seq: number | bigint, lifetime: number): Promise => { +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 = 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 [ms, ns] = lifetime.toString().split('.') const lifetimeNs = (BigInt(ms) * BigInt(100000)) + BigInt(ns ?? '0') - return _create(peerId, value, seq, validityType, expirationDate, lifetimeNs) + return _create(peerId, value, seq, validityType, expirationDate, lifetimeNs, options) } /** * Same as create(), but instead of generating a new Date, it receives the intended expiration time * WARNING: nano precision is not standard, make sure the value in seconds is 9 orders of magnitude lesser than the one provided. * + * The passed value can be a CID, a PeerID or an arbitrary string path. + * + * * CIDs will be converted to v1 and stored in the record as a string similar to: `/ipfs/${cid}` + * * 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 {Uint8Array} value - value to be stored in the record. + * @param {CID | PeerId | 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 const createWithExpiration = async (peerId: PeerId, value: Uint8Array, seq: number | bigint, expiration: string): Promise => { +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 = defaultCreateOptions): Promise { const expirationDate = NanoDate.fromString(expiration) const validityType = IpnsEntry.ValidityType.EOL const ttlMs = expirationDate.toDate().getTime() - Date.now() const ttlNs = (BigInt(ttlMs) * BigInt(100000)) + BigInt(expirationDate.getNano()) - return _create(peerId, value, seq, validityType, expirationDate, ttlNs) + return _create(peerId, value, seq, validityType, expirationDate, ttlNs, options) } -const _create = async (peerId: PeerId, value: Uint8Array, seq: number | bigint, validityType: IpnsEntry.ValidityType, expirationDate: NanoDate, ttl: bigint): Promise => { +const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, expirationDate: NanoDate, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise => { seq = BigInt(seq) const isoValidity = uint8ArrayFromString(expirationDate.toString()) + 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 signatureV1 = await signLegacyV1(privateKey, value, validityType, isoValidity) - const data = createCborData(value, isoValidity, validityType, seq, ttl) - const sigData = ipnsEntryDataForV2Sig(data) + const data = createCborData(encodedValue, isoValidity, validityType, seq, ttl) + const sigData = ipnsRecordDataForV2Sig(data) const signatureV2 = await privateKey.sign(sigData) - - const entry: IPNSEntry = { - value, - signature: signatureV1, - validityType, - validity: isoValidity, - sequence: seq, - ttl, - signatureV2, - data - } + let pubKey: Uint8Array | undefined // if we cannot derive the public key from the PeerId (e.g. RSA PeerIDs), // we have to embed it in the IPNS record @@ -117,12 +222,46 @@ const _create = async (peerId: PeerId, value: Uint8Array, seq: number | bigint, const digest = Digest.decode(peerId.toBytes()) if (digest.code !== ID_MULTIHASH_CODE || !uint8ArrayEquals(peerId.publicKey, digest.digest)) { - entry.pubKey = peerId.publicKey + pubKey = peerId.publicKey } } - log('ipns entry for %b created', value) - return entry + if (options.v1Compatible === true) { + const signatureV1 = await signLegacyV1(privateKey, encodedValue, validityType, isoValidity) + + const record: IPNSRecord = { + value: normalizedValue, + signatureV1, + validity: expirationDate, + validityType, + sequence: seq, + ttl, + signatureV2, + data + } + + if (pubKey != null) { + record.pubKey = pubKey + } + + return record + } else { + const record: IPNSRecordV2 = { + value: normalizedValue, + validity: expirationDate, + validityType, + sequence: seq, + ttl, + signatureV2, + data + } + + if (pubKey != null) { + record.pubKey = pubKey + } + + return record + } } /** @@ -149,7 +288,7 @@ export { extractPublicKey } from './utils.js' */ const signLegacyV1 = async (privateKey: PrivateKey, value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array): Promise => { try { - const dataForSignature = ipnsEntryDataForV1Sig(value, validityType, validity) + const dataForSignature = ipnsRecordDataForV1Sig(value, validityType, validity) return await privateKey.sign(dataForSignature) } catch (error: any) { diff --git a/src/pb/ipns.proto b/src/pb/ipns.proto index 276e88d..87a7ac3 100644 --- a/src/pb/ipns.proto +++ b/src/pb/ipns.proto @@ -1,39 +1,39 @@ -// https://github.com/ipfs/go-ipns/blob/master/pb/ipns.proto +// https://github.com/ipfs/boxo/blob/main/ipns/pb/record.proto syntax = "proto3"; message IpnsEntry { - enum ValidityType { - EOL = 0; // setting an EOL says "this record is valid until..." + enum ValidityType { + // setting an EOL says "this record is valid until..." + EOL = 0; } - // value to be stored in the record - optional bytes value = 1; + // legacy V1 copy of data[Value] + optional bytes value = 1; - // signature of the record - optional bytes signature = 2; + // legacy V1 field, verify 'signatureV2' instead + optional bytes signatureV1 = 2; - // Type of validation being used + // legacy V1 copies of data[ValidityType] and data[Validity] optional ValidityType validityType = 3; - - // expiration datetime for the record in RFC3339 format optional bytes validity = 4; - // number representing the version of the record + // legacy V1 copy of data[Sequence] optional uint64 sequence = 5; - // ttl in nanoseconds + // legacy V1 copy copy of data[TTL] optional uint64 ttl = 6; - // in order for nodes to properly validate a record upon receipt, they need the public - // key associated with it. For old RSA keys, its easiest if we just send this as part of - // the record itself. For newer ed25519 keys, the public key can be embedded in the - // peerID, making this field unnecessary. + // Optional Public Key to be used for signature verification. + // Used for big keys such as old RSA keys. Including the public key as part of + // the record itself makes it verifiable in offline mode, without any additional lookup. + // For newer Ed25519 keys, the public key is small enough that it can be embedded in the + // IPNS Name itself, making this field unnecessary. optional bytes pubKey = 7; - // the v2 signature of the record + // (mandatory V2) signature of the IPNS record optional bytes signatureV2 = 8; - // extensible data + // (mandatory V2) extensible record data in DAG-CBOR format optional bytes data = 9; } diff --git a/src/pb/ipns.ts b/src/pb/ipns.ts index 867ea37..fb18014 100644 --- a/src/pb/ipns.ts +++ b/src/pb/ipns.ts @@ -10,7 +10,7 @@ import type { Uint8ArrayList } from 'uint8arraylist' export interface IpnsEntry { value?: Uint8Array - signature?: Uint8Array + signatureV1?: Uint8Array validityType?: IpnsEntry.ValidityType validity?: Uint8Array sequence?: bigint @@ -49,9 +49,9 @@ export namespace IpnsEntry { w.bytes(obj.value) } - if (obj.signature != null) { + if (obj.signatureV1 != null) { w.uint32(18) - w.bytes(obj.signature) + w.bytes(obj.signatureV1) } if (obj.validityType != null) { @@ -105,7 +105,7 @@ export namespace IpnsEntry { obj.value = reader.bytes() break case 2: - obj.signature = reader.bytes() + obj.signatureV1 = reader.bytes() break case 3: obj.validityType = IpnsEntry.ValidityType.codec().decode(reader) diff --git a/src/selector.ts b/src/selector.ts index 49e2683..e0f2a07 100644 --- a/src/selector.ts +++ b/src/selector.ts @@ -1,23 +1,18 @@ -import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { IpnsEntry } from './pb/ipns.js' -import { parseRFC3339 } from './utils.js' +import { unmarshal } from './utils.js' export function ipnsSelector (key: Uint8Array, data: Uint8Array[]): number { const entries = data.map((buf, index) => ({ - entry: IpnsEntry.decode(buf), + record: unmarshal(buf), index })) entries.sort((a, b) => { - // having a newer signature version is better than an older signature version - if (a.entry.signatureV2 != null && b.entry.signatureV2 == null) { - return -1 - } else if (a.entry.signatureV2 == null && b.entry.signatureV2 != null) { - return 1 - } + // Before we'd sort based on the signature version. Unmarshal now fails if + // a record does not have SignatureV2, so that is no longer needed. V1-only + // records haven't been issues in a long time. - const aSeq = a.entry.sequence ?? 0n - const bSeq = b.entry.sequence ?? 0n + const aSeq = a.record.sequence + const bSeq = b.record.sequence // choose later sequence number if (aSeq > bSeq) { @@ -26,18 +21,15 @@ export function ipnsSelector (key: Uint8Array, data: Uint8Array[]): number { return 1 } - const aValidty = a.entry.validity ?? new Uint8Array(0) - const bValidty = b.entry.validity ?? new Uint8Array(0) - // choose longer lived record if sequence numbers the same - const entryAValidityDate = parseRFC3339(uint8ArrayToString(aValidty)) - const entryBValidityDate = parseRFC3339(uint8ArrayToString(bValidty)) + const recordAValidityDate = a.record.validity.toDate() + const recordBValidityDate = b.record.validity.toDate() - if (entryAValidityDate.getTime() > entryBValidityDate.getTime()) { + if (recordAValidityDate.getTime() > recordBValidityDate.getTime()) { return -1 } - if (entryAValidityDate.getTime() < entryBValidityDate.getTime()) { + if (recordAValidityDate.getTime() < recordBValidityDate.getTime()) { return 1 } diff --git a/src/utils.ts b/src/utils.ts index 5deaa0b..9c3bff6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,18 +1,24 @@ import { unmarshalPublicKey } from '@libp2p/crypto/keys' +import { isPeerId, type PeerId } from '@libp2p/interface/peer-id' 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 NanoDate from 'timestamp-nano' 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 { IpnsEntry } from './pb/ipns.js' -import type { IPNSEntry, IPNSEntryData } from './index.js' +import type { IPNSRecord, IPNSRecordV2, IPNSRecordData } from './index.js' import type { PublicKey } from '@libp2p/interface/keys' -import type { PeerId } from '@libp2p/interface/peer-id' const log = logger('ipns:utils') const IPNS_PREFIX = uint8ArrayFromString('/ipns/') +const LIBP2P_CID_CODEC = 114 /** * Convert a JavaScript date into an `RFC3339Nano` formatted @@ -56,7 +62,7 @@ export function parseRFC3339 (time: string): Date { const hour = parseInt(m[4], 10) const minute = parseInt(m[5], 10) const second = parseInt(m[6], 10) - const millisecond = parseInt(m[7].slice(0, -6), 10) + const millisecond = parseInt(m[7].padEnd(6, '0').slice(0, 3), 10) return new Date(Date.UTC(year, month, date, hour, minute, second, millisecond)) } @@ -65,8 +71,8 @@ export function parseRFC3339 (time: string): Date { * Extracts a public key from the passed PeerId, falling * back to the pubKey embedded in the ipns record */ -export const extractPublicKey = async (peerId: PeerId, entry: IpnsEntry): Promise => { - if (entry == null || peerId == null) { +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) @@ -75,15 +81,15 @@ export const extractPublicKey = async (peerId: PeerId, entry: IpnsEntry): Promis let pubKey: PublicKey | undefined - if (entry.pubKey != null) { + if (record.pubKey != null) { try { - pubKey = unmarshalPublicKey(entry.pubKey) + pubKey = unmarshalPublicKey(record.pubKey) } catch (err) { log.error(err) throw err } - const otherId = await peerIdFromKeys(entry.pubKey) + 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) @@ -102,7 +108,7 @@ export const extractPublicKey = async (peerId: PeerId, entry: IpnsEntry): Promis /** * Utility for creating the record data for being signed */ -export const ipnsEntryDataForV1Sig = (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array): Uint8Array => { +export const ipnsRecordDataForV1Sig = (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array): Uint8Array => { const validityTypeBuffer = uint8ArrayFromString(validityType) return uint8ArrayConcat([value, validity, validityTypeBuffer]) @@ -111,17 +117,35 @@ export const ipnsEntryDataForV1Sig = (value: Uint8Array, validityType: IpnsEntry /** * Utility for creating the record data for being signed */ -export const ipnsEntryDataForV2Sig = (data: Uint8Array): Uint8Array => { +export const ipnsRecordDataForV2Sig = (data: Uint8Array): Uint8Array => { const entryData = uint8ArrayFromString('ipns-signature:') return uint8ArrayConcat([entryData, data]) } -export const marshal = (obj: IPNSEntry): Uint8Array => { - return IpnsEntry.encode(obj) +export const marshal = (obj: IPNSRecord | IPNSRecordV2): Uint8Array => { + if ('signatureV1' in obj) { + return IpnsEntry.encode({ + value: uint8ArrayFromString(obj.value), + signatureV1: obj.signatureV1, + validityType: obj.validityType, + validity: uint8ArrayFromString(obj.validity.toString()), + sequence: obj.sequence, + ttl: obj.ttl, + pubKey: obj.pubKey, + signatureV2: obj.signatureV2, + data: obj.data + }) + } else { + return IpnsEntry.encode({ + pubKey: obj.pubKey, + signatureV2: obj.signatureV2, + data: obj.data + }) + } } -export const unmarshal = (buf: Uint8Array): IPNSEntry => { +export function unmarshal (buf: Uint8Array): IPNSRecord { const message = IpnsEntry.decode(buf) // protobufjs returns bigints as numbers @@ -134,16 +158,53 @@ export const unmarshal = (buf: Uint8Array): IPNSEntry => { message.ttl = BigInt(message.ttl) } - return { - value: message.value ?? new Uint8Array(0), - signature: message.signature ?? new Uint8Array(0), - validityType: message.validityType ?? IpnsEntry.ValidityType.EOL, - validity: message.validity ?? new Uint8Array(0), - sequence: message.sequence ?? 0n, - pubKey: message.pubKey, - ttl: message.ttl ?? undefined, - signatureV2: message.signatureV2, - data: message.data + // Check if we have the data field. If we don't, we fail. We've been producing + // 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) + } + + const data = parseCborData(message.data) + const value = normalizeValue(data.Value) + + let validity + try { + validity = NanoDate.fromDate(parseRFC3339(uint8ArrayToString(data.Validity))) + } catch (e) { + log.error('unrecognized validity format (not an rfc3339 format)') + throw errCode(new Error('unrecognized validity format (not an rfc3339 format)'), ERRORS.ERR_UNRECOGNIZED_FORMAT) + } + + if (message.value != null && message.signatureV1 != null) { + // V1+V2 + validateCborDataMatchesPbData(message) + + return { + value, + validityType: IpnsEntry.ValidityType.EOL, + validity, + sequence: data.Sequence, + ttl: data.TTL, + pubKey: message.pubKey, + signatureV1: message.signatureV1, + signatureV2: message.signatureV2, + data: message.data + } + } else if (message.signatureV2 != null) { + // V2-only + return { + value, + validityType: IpnsEntry.ValidityType.EOL, + validity, + sequence: data.Sequence, + ttl: data.TTL, + pubKey: message.pubKey, + signatureV2: message.signatureV2, + data: message.data + } + } else { + throw new Error('invalid record: does not include signatureV1 or signatureV2') } } @@ -178,7 +239,7 @@ export const createCborData = (value: Uint8Array, validity: Uint8Array, validity return cborg.encode(data) } -export const parseCborData = (buf: Uint8Array): IPNSEntryData => { +export const parseCborData = (buf: Uint8Array): IPNSRecordData => { const data = cborg.decode(buf) if (data.ValidityType === 0) { @@ -199,3 +260,84 @@ export const parseCborData = (buf: Uint8Array): IPNSEntryData => { return data } + +/** + * 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 | PeerId | string | Uint8Array): string => { + if (value != null) { + // if we have a PeerId, turn it into an ipns path + if (isPeerId(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 + } + + // 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) { + return `/ipns/${cid.toString(base36)}` + } + + 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()}` + } + + return `/ipfs/${CID.parse(string).toV1().toString()}` + } catch { + // fall through + } + } + + throw errCode(new Error('Value must be a valid content path starting with /'), ERRORS.ERR_INVALID_VALUE) +} + +const validateCborDataMatchesPbData = (entry: IpnsEntry): void => { + if (entry.data == null) { + throw errCode(new Error('Record data is missing'), ERRORS.ERR_INVALID_RECORD_DATA) + } + + 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) + } + + 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) + } + + if (data.ValidityType !== entry.validityType) { + throw errCode(new Error('Field "validityType" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) + } + + if (data.Sequence !== entry.sequence) { + throw errCode(new Error('Field "sequence" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) + } + + if (data.TTL !== entry.ttl) { + throw errCode(new Error('Field "ttl" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) + } +} diff --git a/src/validator.ts b/src/validator.ts index 7732a0d..3518e47 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -1,11 +1,8 @@ import { logger } from '@libp2p/logger' import errCode from 'err-code' -import { equals as uint8ArrayEquals } from 'uint8arrays/equals' -import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import * as ERRORS from './errors.js' import { IpnsEntry } from './pb/ipns.js' -import { parseRFC3339, extractPublicKey, ipnsEntryDataForV2Sig, unmarshal, peerIdFromRoutingKey, parseCborData } from './utils.js' -import type { IPNSEntry } from './index.js' +import { extractPublicKey, ipnsRecordDataForV2Sig, unmarshal, peerIdFromRoutingKey } from './utils.js' import type { PublicKey } from '@libp2p/interface/keys' const log = logger('ipns:validator') @@ -16,28 +13,20 @@ const log = logger('ipns:validator') const MAX_RECORD_SIZE = 1024 * 10 /** - * Validates the given ipns entry against the given public key + * Validates the given IPNS Record against the given public key. We need a "raw" + * record in order to be able to access to all of its fields. */ -export const validate = async (publicKey: PublicKey, entry: IPNSEntry): Promise => { - const { value, validityType, validity } = entry +export const validate = async (publicKey: PublicKey, buf: Uint8Array): Promise => { + // unmarshal ensures that (1) SignatureV2 and Data are present, (2) that ValidityType + // and Validity are of valid types and have a value, (3) that CBOR data matches protobuf + // if it's a V1+V2 record. + const record = unmarshal(buf) - let dataForSignature: Uint8Array - let signature: Uint8Array - - // Check v2 signature if it's available, otherwise use the v1 signature - if ((entry.signatureV2 != null) && (entry.data != null)) { - signature = entry.signatureV2 - dataForSignature = ipnsEntryDataForV2Sig(entry.data) - - validateCborDataMatchesPbData(entry) - } else { - throw errCode(new Error('missing data or signatureV2'), ERRORS.ERR_SIGNATURE_VERIFICATION) - } - - // Validate Signature + // Validate Signature V2 let isValid try { - isValid = await publicKey.verify(dataForSignature, signature) + const dataForSignature = ipnsRecordDataForV2Sig(record.data) + isValid = await publicKey.verify(dataForSignature, record.signatureV2) } catch (err) { isValid = false } @@ -47,54 +36,17 @@ export const validate = async (publicKey: PublicKey, entry: IPNSEntry): Promise< } // Validate according to the validity type - if (validity != null && validityType === IpnsEntry.ValidityType.EOL) { - let validityDate - - try { - validityDate = parseRFC3339(uint8ArrayToString(validity)) - } catch (e) { - log.error('unrecognized validity format (not an rfc3339 format)') - throw errCode(new Error('unrecognized validity format (not an rfc3339 format)'), ERRORS.ERR_UNRECOGNIZED_FORMAT) - } - - if (validityDate.getTime() < Date.now()) { + if (record.validityType === IpnsEntry.ValidityType.EOL) { + if (record.validity.toDate().getTime() < Date.now()) { log.error('record has expired') throw errCode(new Error('record has expired'), ERRORS.ERR_IPNS_EXPIRED_RECORD) } - } else if (validityType != null) { + } else if (record.validityType != null) { log.error('unrecognized validity type') throw errCode(new Error('unrecognized validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY) } - log('ipns entry for %b is valid', value) -} - -const validateCborDataMatchesPbData = (entry: IPNSEntry): void => { - if (entry.data == null) { - throw errCode(new Error('Record data is missing'), ERRORS.ERR_INVALID_RECORD_DATA) - } - - const data = parseCborData(entry.data) - - if (!uint8ArrayEquals(data.Value, entry.value)) { - throw errCode(new Error('Field "value" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) - } - - if (!uint8ArrayEquals(data.Validity, entry.validity)) { - throw errCode(new Error('Field "validity" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) - } - - if (data.ValidityType !== entry.validityType) { - throw errCode(new Error('Field "validityType" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) - } - - if (data.Sequence !== entry.sequence) { - throw errCode(new Error('Field "sequence" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) - } - - if (data.TTL !== entry.ttl) { - throw errCode(new Error('Field "ttl" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) - } + log('ipns record for %b is valid', record.value) } export async function ipnsValidator (key: Uint8Array, marshalledData: Uint8Array): Promise { @@ -103,11 +55,11 @@ export async function ipnsValidator (key: Uint8Array, marshalledData: Uint8Array } const peerId = peerIdFromRoutingKey(key) - const receivedEntry = unmarshal(marshalledData) + const receivedRecord = unmarshal(marshalledData) // extract public key - const pubKey = await extractPublicKey(peerId, receivedEntry) + const pubKey = await extractPublicKey(peerId, receivedRecord) // Record validation - await validate(pubKey, receivedEntry) + await validate(pubKey, marshalledData) } diff --git a/test/conformance.spec.ts b/test/conformance.spec.ts new file mode 100644 index 0000000..83f7c59 --- /dev/null +++ b/test/conformance.spec.ts @@ -0,0 +1,68 @@ +/* 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 { 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) + }) + + 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 cid = CID.parse('k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w', base36) + const peerId = peerIdFromCID(cid) + const publicKey = unmarshalPublicKey(peerId.publicKey ?? new Uint8Array()) + await validate(publicKey, buf) + + expect(record.value).to.equal('/ipfs/bafkqaddwgevxmmraojswg33smq') + }) + + 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) + }) + + 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) + }) + + 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) + + 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 cid = CID.parse('k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f', base36) + const peerId = peerIdFromCID(cid) + const publicKey = unmarshalPublicKey(peerId.publicKey ?? new Uint8Array()) + await validate(publicKey, buf) + + expect(record.value).to.equal('/ipfs/bafkqadtwgiww63tmpeqhezldn5zgi') + }) +}) diff --git a/test/fixtures/k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record b/test/fixtures/k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record new file mode 100644 index 0000000..0f278c8 Binary files /dev/null and b/test/fixtures/k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record differ diff --git a/test/fixtures/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record b/test/fixtures/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record new file mode 100644 index 0000000..cb914e9 Binary files /dev/null and b/test/fixtures/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record differ diff --git a/test/fixtures/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record b/test/fixtures/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record new file mode 100644 index 0000000..ddce738 Binary files /dev/null and b/test/fixtures/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record differ diff --git a/test/fixtures/k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record b/test/fixtures/k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record new file mode 100644 index 0000000..6f44adc Binary files /dev/null and b/test/fixtures/k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record differ diff --git a/test/fixtures/k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw_v1-v2-broken-v1-value.ipns-record b/test/fixtures/k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw_v1-v2-broken-v1-value.ipns-record new file mode 100644 index 0000000..9b4953e Binary files /dev/null and b/test/fixtures/k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw_v1-v2-broken-v1-value.ipns-record differ diff --git a/test/fixtures/k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record b/test/fixtures/k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record new file mode 100644 index 0000000..c6e2a0f Binary files /dev/null and b/test/fixtures/k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record differ diff --git a/test/index.spec.ts b/test/index.spec.ts index d9f9f32..867a35d 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,22 +1,27 @@ /* eslint-env mocha */ import { randomBytes } from '@libp2p/crypto' -import { generateKeyPair } from '@libp2p/crypto/keys' +import { generateKeyPair, unmarshalPrivateKey } from '@libp2p/crypto/keys' import { peerIdFromKeys, peerIdFromString } from '@libp2p/peer-id' import { createEd25519PeerId } from '@libp2p/peer-id-factory' 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 { 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 { unmarshal, marshal, extractPublicKey, peerIdToRoutingKey } from '../src/utils.js' +import { IpnsEntry } from '../src/pb/ipns.js' +import { extractPublicKey, peerIdToRoutingKey, parseCborData, createCborData, ipnsRecordDataForV2Sig } from '../src/utils.js' import { ipnsValidator } from '../src/validator.js' import type { PeerId } from '@libp2p/interface/peer-id' describe('ipns', function () { this.timeout(20 * 1000) - const cid = uint8ArrayFromString('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq') + const contentPath = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' let peerId: PeerId before(async () => { @@ -24,125 +29,272 @@ describe('ipns', function () { peerId = await peerIdFromKeys(rsa.public.bytes, rsa.bytes) }) - it('should create an ipns record correctly', async () => { + it('should create an ipns record (V1+V2) correctly', async () => { const sequence = 0 const validity = 1000000 - const entry = await ipns.create(peerId, cid, sequence, validity) - expect(entry).to.deep.include({ - value: cid, - sequence: BigInt(sequence) - }) - expect(entry).to.have.property('validity') - expect(entry).to.have.property('signature') - expect(entry).to.have.property('validityType') - expect(entry).to.have.property('signatureV2') - expect(entry).to.have.property('data') + const record = await ipns.create(peerId, contentPath, sequence, validity) + + expect(record.value).to.equal(contentPath) + expect(record.validityType).to.equal(IpnsEntry.ValidityType.EOL) + expect(record.validity).to.exist() + expect(record.sequence).to.equal(BigInt(0)) + expect(record.ttl).to.equal(BigInt(validity * 100000)) + expect(record.signatureV1).to.exist() + expect(record.signatureV2).to.exist() + expect(record.data).to.exist() + + // Protobuf must have all fields! + const pb = IpnsEntry.decode(ipns.marshal(record)) + expect(pb.value).to.equalBytes(uint8ArrayFromString(contentPath)) + expect(pb.validityType).to.equal(IpnsEntry.ValidityType.EOL) + expect(pb.validity).to.exist() + expect(pb.sequence).to.equal(BigInt(sequence)) + expect(pb.ttl).to.equal(BigInt(validity * 100000)) + expect(pb.signatureV1).to.exist() + expect(pb.signatureV2).to.exist() + expect(pb.data).to.exist() + + // Protobuf.Data must have all fields and match! + const data = parseCborData(pb.data ?? new Uint8Array(0)) + expect(data.Value).to.equalBytes(pb.value) + expect(data.ValidityType).to.equal(pb.validityType) + expect(data.Validity).to.equalBytes(pb.validity) + expect(data.Sequence).to.equal(pb.sequence) + expect(data.TTL).to.equal(pb.ttl) + }) + + it('should create an ipns record (V2) correctly', async () => { + const sequence = 0 + const validity = 1000000 + + const record = await ipns.create(peerId, contentPath, sequence, validity, { v1Compatible: false }) + + expect(record.value).to.equal(contentPath) + expect(record.validityType).to.equal(IpnsEntry.ValidityType.EOL) + expect(record.validity).to.exist() + expect(record.sequence).to.equal(BigInt(0)) + expect(record.ttl).to.equal(BigInt(validity * 100000)) + expect(record.signatureV2).to.exist() + expect(record).to.not.have.property('signatureV1') + expect(record.data).to.exist() + + // PB must only have signature and data. + const pb = IpnsEntry.decode(ipns.marshal(record)) + expect(pb.value).to.not.exist() + expect(pb.validityType).to.not.exist() + expect(pb.validity).to.not.exist() + expect(pb.sequence).to.not.exist() + expect(pb.ttl).to.not.exist() + expect(pb.signatureV1).to.not.exist() + expect(pb.signatureV2).to.exist() + expect(pb.data).to.exist() + + // Protobuf.Data must have all fields and match! + const data = parseCborData(pb.data ?? new Uint8Array(0)) + expect(data.Value).to.equalBytes(uint8ArrayFromString(contentPath)) + expect(data.ValidityType).to.equal(IpnsEntry.ValidityType.EOL) + expect(data.Validity).to.exist() + expect(data.Sequence).to.equal(BigInt(sequence)) + expect(data.TTL).to.equal(BigInt(validity * 100000)) }) - it('should be able to create a record with a fixed expiration', async () => { + it('should be able to create a record (V1+V2) with a fixed expiration', async () => { const sequence = 0 - // 2033-05-18T03:33:20.000000000Z const expiration = '2033-05-18T03:33:20.000000000Z' - const entry = await ipns.createWithExpiration(peerId, cid, sequence, expiration) + const record = await ipns.createWithExpiration(peerId, contentPath, sequence, expiration) + const marshalledRecord = ipns.marshal(record) + + await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord) + + const pb = IpnsEntry.decode(marshalledRecord) + expect(pb).to.have.property('validity') + expect(pb.validity).to.equalBytes(uint8ArrayFromString(expiration)) + }) + + it('should be able to create a record (V2) with a fixed expiration', async () => { + 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) + + await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord) + + const pb = IpnsEntry.decode(ipns.marshal(record)) + expect(pb).to.not.have.property('validity') - await ipnsValidator(peerIdToRoutingKey(peerId), marshal(entry)) - expect(entry).to.have.property('validity') - expect(entry.validity).to.equalBytes(uint8ArrayFromString('2033-05-18T03:33:20.000000000Z')) + const data = parseCborData(pb.data ?? new Uint8Array(0)) + expect(data.Validity).to.equalBytes(uint8ArrayFromString(expiration)) }) - it('should create an ipns record and validate it correctly', async () => { + it('should create an ipns record (V1+V2) and validate it correctly', async () => { const sequence = 0 const validity = 1000000 - const entry = await ipns.create(peerId, cid, sequence, validity) - await ipnsValidator(peerIdToRoutingKey(peerId), marshal(entry)) + const record = await ipns.create(peerId, contentPath, sequence, validity) + await ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(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)) + }) + + 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) + 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) + 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) + 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) + 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) + 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 pb = IpnsEntry.decode(ipns.marshal(record)) + pb.data = createCborData(uint8ArrayFromString(inputValue), pb.validity ?? new Uint8Array(0), pb.validityType ?? '', pb.sequence ?? 0n, pb.ttl ?? 0n) + pb.value = uint8ArrayFromString(inputValue) + + const modifiedRecord = ipns.unmarshal(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 pb = IpnsEntry.decode(ipns.marshal(record)) + pb.data = createCborData(uint8ArrayFromString(inputValue), pb.validity ?? new Uint8Array(0), pb.validityType ?? '', pb.sequence ?? 0n, pb.ttl ?? 0n) + pb.value = uint8ArrayFromString(inputValue) + + const modifiedRecord = ipns.unmarshal(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) + }) + + 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) }) it('should fail to validate a v1 (deprecated legacy) message', async () => { const sequence = 0 const validity = 1000000 - const entry = await ipns.create(peerId, cid, sequence, validity) + const record = await ipns.create(peerId, contentPath, sequence, validity) + const pb = IpnsEntry.decode(ipns.marshal(record)) // remove the extra fields added for v2 sigs - delete entry.data - delete entry.signatureV2 + delete pb.data + delete pb.signatureV2 // confirm a v1 exists - expect(entry).to.have.property('signature') + expect(pb).to.have.property('signatureV1') - await expect(ipnsValidator(peerIdToRoutingKey(peerId), marshal(entry))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) + await expect(ipnsValidator(peerIdToRoutingKey(peerId), IpnsEntry.encode(pb))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) }) it('should fail to validate a v2 without v2 signature (ignore v1)', async () => { const sequence = 0 const validity = 1000000 - const entry = await ipns.create(peerId, cid, sequence, validity) + const record = await ipns.create(peerId, contentPath, sequence, validity) + const pb = IpnsEntry.decode(ipns.marshal(record)) // remove v2 sig - delete entry.signatureV2 + delete pb.signatureV2 // confirm a v1 exists - expect(entry).to.have.property('signature') + expect(pb).to.have.property('signatureV1') - await expect(ipnsValidator(peerIdToRoutingKey(peerId), marshal(entry))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) + await expect(ipnsValidator(peerIdToRoutingKey(peerId), IpnsEntry.encode(pb))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) }) it('should fail to validate a bad record', async () => { const sequence = 0 const validity = 1000000 - const entry = await ipns.create(peerId, cid, sequence, validity) + const record = await ipns.create(peerId, contentPath, sequence, validity) // corrupt the record by changing the value to random bytes - entry.value = randomBytes(46) + record.value = uint8ArrayToString(randomBytes(46)) - await expect(ipnsValidator(peerIdToRoutingKey(peerId), marshal(entry))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) + await expect(ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) }) 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 entry = await ipns.create(peerId, cid, sequence, validity) + const record = await ipns.create(peerId, contentPath, sequence, validity) await new Promise(resolve => setTimeout(resolve, 1)) - await expect(ipnsValidator(peerIdToRoutingKey(peerId), marshal(entry))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_IPNS_EXPIRED_RECORD) + await expect(ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_IPNS_EXPIRED_RECORD) }) it('should create an ipns record, marshal and unmarshal it, as well as validate it correctly', async () => { const sequence = 0 const validity = 1000000 - const entryDataCreated = await ipns.create(peerId, cid, sequence, validity) + const createdRecord = await ipns.create(peerId, contentPath, sequence, validity) - const marshalledData = marshal(entryDataCreated) - const unmarshalledData = unmarshal(marshalledData) + const marshalledData = ipns.marshal(createdRecord) + const unmarshalledData = ipns.unmarshal(marshalledData) - expect(entryDataCreated.value).to.equalBytes(unmarshalledData.value) - expect(entryDataCreated.validity).to.equalBytes(unmarshalledData.validity) - expect(entryDataCreated.validityType).to.equal(unmarshalledData.validityType) - expect(entryDataCreated.signature).to.equalBytes(unmarshalledData.signature) - expect(entryDataCreated.sequence).to.equal(unmarshalledData.sequence) - expect(entryDataCreated.ttl).to.equal(unmarshalledData.ttl) + expect(createdRecord.value).to.equal(unmarshalledData.value) + expect(createdRecord.validity.toString()).to.equal(unmarshalledData.validity.toString()) + expect(createdRecord.validityType).to.equal(unmarshalledData.validityType) + expect(createdRecord.signatureV1).to.equalBytes('signatureV1' in unmarshalledData ? unmarshalledData.signatureV1 : new Uint8Array(0)) + expect(createdRecord.sequence).to.equal(unmarshalledData.sequence) + expect(createdRecord.ttl).to.equal(unmarshalledData.ttl) + expect(createdRecord.signatureV2).to.equalBytes(unmarshalledData.signatureV2) + expect(createdRecord.data).to.equalBytes(unmarshalledData.data) - if (unmarshalledData.signatureV2 == null) { - throw new Error('No v2 sig found') - } - - expect(entryDataCreated.signatureV2).to.equalBytes(unmarshalledData.signatureV2) - - if (unmarshalledData.data == null) { - throw new Error('No v2 data found') - } - - expect(entryDataCreated.data).to.equalBytes(unmarshalledData.data) - - await ipnsValidator(peerIdToRoutingKey(peerId), marshal(unmarshalledData)) + await ipnsValidator(peerIdToRoutingKey(peerId), marshalledData) }) it('should get datastore key correctly', () => { @@ -170,11 +322,11 @@ describe('ipns', function () { const sequence = 0 const validity = 1000000 - const entry = await ipns.create(peerId, cid, sequence, validity) + const record = await ipns.create(peerId, contentPath, sequence, validity) + expect(record.pubKey).to.equalBytes(peerId.publicKey) - expect(entry).to.deep.include({ - pubKey: peerId.publicKey - }) + const pb = IpnsEntry.decode(ipns.marshal(record)) + expect(pb.pubKey).to.equalBytes(peerId.publicKey) }) // It should have a public key embedded for newer ed25519 keys @@ -188,19 +340,19 @@ describe('ipns', function () { const validity = 1000000 const ed25519 = await createEd25519PeerId() - const entry = await ipns.create(ed25519, cid, sequence, validity) + const record = await ipns.create(ed25519, contentPath, sequence, validity) - expect(entry).to.not.have.property('pubKey') // ed25519 keys should not be embedded + expect(record).to.not.have.property('pubKey') // ed25519 keys should not be embedded }) it('validator with no valid public key should error', async () => { const sequence = 0 const validity = 1000000 - const entry = await ipns.create(peerId, cid, sequence, validity) - delete entry.pubKey + const record = await ipns.create(peerId, contentPath, sequence, validity) + delete record.pubKey - const marshalledData = marshal(entry) + const marshalledData = ipns.marshal(record) const key = peerIdToRoutingKey(peerId) await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected().with.property('code', ERRORS.ERR_UNDEFINED_PARAMETER) @@ -210,11 +362,39 @@ describe('ipns', function () { const sequence = 0 const validity = 1000000 - const entry = await ipns.create(peerId, cid, sequence, validity) + const record = await ipns.create(peerId, contentPath, sequence, validity) - const publicKey = await extractPublicKey(peerId, entry) + const publicKey = await extractPublicKey(peerId, record) expect(publicKey).to.deep.include({ bytes: peerId.publicKey }) }) + + it('should unmarshal a record with raw CID bytes', async () => { + // we may encounter these in the wild due to older versions of this module + // 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, { + v1Compatible: false + }) + + // we will store the raw bytes from this CID + const cid = CID.parse('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') + + // override data with raw CID bytes + const data = cbor.decode(input.data) + data.Value = cid.bytes + 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) + + expect(record).to.have.property('value', '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') + }) }) diff --git a/test/selector.spec.ts b/test/selector.spec.ts index 4122bb8..249e59b 100644 --- a/test/selector.spec.ts +++ b/test/selector.spec.ts @@ -3,7 +3,6 @@ import { generateKeyPair } from '@libp2p/crypto/keys' import { peerIdFromKeys } from '@libp2p/peer-id' import { expect } from 'aegir/chai' -import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import * as ipns from '../src/index.js' import { ipnsSelector } from '../src/selector.js' import { marshal, peerIdToRoutingKey } from '../src/utils.js' @@ -12,7 +11,7 @@ import type { PeerId } from '@libp2p/interface/peer-id' describe('selector', function () { this.timeout(20 * 1000) - const cid = uint8ArrayFromString('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq') + const contentPath = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' let peerId: PeerId before(async () => { @@ -24,11 +23,11 @@ describe('selector', function () { const sequence = 0 const lifetime = 1000000 - const entry = await ipns.create(peerId, cid, sequence, lifetime) - const newEntry = await ipns.create(peerId, cid, (sequence + 1), lifetime) + const record = await ipns.create(peerId, contentPath, sequence, lifetime) + const newRecord = await ipns.create(peerId, contentPath, (sequence + 1), lifetime) - const marshalledData = marshal(entry) - const marshalledNewData = marshal(newEntry) + const marshalledData = marshal(record) + const marshalledNewData = marshal(newRecord) const key = peerIdToRoutingKey(peerId) @@ -43,11 +42,11 @@ describe('selector', function () { const sequence = 0 const lifetime = 1000000 - const entry = await ipns.create(peerId, cid, sequence, lifetime) - const newEntry = await ipns.create(peerId, cid, sequence, (lifetime + 1)) + const record = await ipns.create(peerId, contentPath, sequence, lifetime) + const newRecord = await ipns.create(peerId, contentPath, sequence, (lifetime + 1)) - const marshalledData = marshal(entry) - const marshalledNewData = marshal(newEntry) + const marshalledData = marshal(record) + const marshalledNewData = marshal(newRecord) const key = peerIdToRoutingKey(peerId) @@ -57,25 +56,4 @@ describe('selector', function () { valid = ipnsSelector(key, [marshalledData, marshalledNewData]) expect(valid).to.equal(1) // new data is the selected one }) - - it('should use validator.select to select an older record with a v2 sig when the newer record only uses v1', async () => { - const sequence = 0 - const lifetime = 1000000 - - const entry = await ipns.create(peerId, cid, sequence, lifetime) - - const newEntry = await ipns.create(peerId, cid, sequence + 1, lifetime) - delete newEntry.signatureV2 - - const marshalledData = marshal(entry) - const marshalledNewData = marshal(newEntry) - - const key = peerIdToRoutingKey(peerId) - - let valid = ipnsSelector(key, [marshalledNewData, marshalledData]) - expect(valid).to.equal(1) // old data is the selected one - - valid = ipnsSelector(key, [marshalledData, marshalledNewData]) - expect(valid).to.equal(0) // old data is the selected one - }) }) diff --git a/test/validator.spec.ts b/test/validator.spec.ts index e274b32..dbacced 100644 --- a/test/validator.spec.ts +++ b/test/validator.spec.ts @@ -7,6 +7,7 @@ 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' @@ -16,7 +17,7 @@ import type { PeerId } from '@libp2p/interface/peer-id' describe('validator', function () { this.timeout(20 * 1000) - const cid = uint8ArrayFromString('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq') + const contentPath = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' let peerId1: PeerId let peerId2: PeerId @@ -28,12 +29,25 @@ describe('validator', function () { peerId2 = await peerIdFromKeys(rsa2.public.bytes, rsa2.bytes) }) - it('should validate a record', async () => { + it('should validate a (V2) record', async () => { const sequence = 0 const validity = 1000000 - const entry = await ipns.create(peerId1, cid, sequence, validity) - const marshalledData = marshal(entry) + 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]) + + await ipnsValidator(key, marshalledData) + }) + + 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]) @@ -45,11 +59,11 @@ describe('validator', function () { const sequence = 0 const validity = 1000000 - const entry = await ipns.create(peerId1, cid, sequence, validity) + const record = await ipns.create(peerId1, contentPath, sequence, validity) // corrupt the record by changing the value to random bytes - entry.value = randomBytes(entry.value.length) - const marshalledData = marshal(entry) + record.value = uint8ArrayToString(randomBytes(record.value?.length ?? 0)) + const marshalledData = marshal(record) const key = peerIdToRoutingKey(peerId1) @@ -60,8 +74,8 @@ describe('validator', function () { const sequence = 0 const validity = 1000000 - const entry = await ipns.create(peerId1, cid, sequence, validity) - const marshalledData = marshal(entry) + const record = await ipns.create(peerId1, contentPath, sequence, validity) + const marshalledData = marshal(record) const key = peerIdToRoutingKey(peerId2) @@ -72,9 +86,9 @@ describe('validator', function () { const sequence = 0 const validity = 1000000 - const entry = await ipns.create(peerId1, cid, sequence, validity) - entry.pubKey = peerId2.publicKey - const marshalledData = marshal(entry) + const record = await ipns.create(peerId1, contentPath, sequence, validity) + record.pubKey = peerId2.publicKey + const marshalledData = marshal(record) const key = peerIdToRoutingKey(peerId1)