diff --git a/README.md b/README.md index f8d61c3..7b66672 100644 --- a/README.md +++ b/README.md @@ -14,19 +14,11 @@ - [Usage](#usage) - [Create record](#create-record) - [Validate record](#validate-record) - - [Embed public key to record](#embed-public-key-to-record) - [Extract public key from record](#extract-public-key-from-record) - [Marshal data with proto buffer](#marshal-data-with-proto-buffer) - [Unmarshal data from proto buffer](#unmarshal-data-from-proto-buffer) - - [Validator](#validator) -- [API](#api) - - [Create record](#create-record-1) - - [Validate record](#validate-record-1) - - [Marshal data with proto buffer](#marshal-data-with-proto-buffer-1) - - [Unmarshal data from proto buffer](#unmarshal-data-from-proto-buffer-1) - - [Extract public key from record](#extract-public-key-from-record-1) - - [Namespace](#namespace) - [API Docs](#api-docs) + - [Namespace](#namespace) - [License](#license) - [Contribute](#contribute) @@ -53,24 +45,26 @@ This module contains all the necessary code for creating, understanding and vali ```js import * as ipns from 'ipns' -const ipnsRecord = await ipns.create(privateKey, value, sequenceNumber, lifetime) +const ipnsRecord = await ipns.createIPNSRecord(privateKey, value, sequenceNumber, lifetime) ``` -### Validate record +### Validate record against public key ```js -import * as ipns from 'ipns' +import { validate } from 'ipns/validator' -await ipns.validate(publicKey, marshalledData) +await validate(publicKey, marshalledRecord) // if no error thrown, the record is valid ``` -### Embed public key to record +### Validate record against routing key + +This is useful when validating IPNS names that use RSA keys, whose public key is embedded in the record (rather than in the routing key as with Ed25519). ```js -import * as ipns from 'ipns' +import { ipnsValidator } from 'ipns/validator' -const ipnsRecordWithEmbeddedPublicKey = await ipns.embedPublicKey(publicKey, ipnsRecord) +await ipnsValidator(routingKey, marshalledRecord) ``` ### Extract public key from record @@ -78,7 +72,7 @@ const ipnsRecordWithEmbeddedPublicKey = await ipns.embedPublicKey(publicKey, ipn ```js import * as ipns from 'ipns' -const publicKey = await ipns.extractPublicKey(peerId, ipnsRecord) +const publicKey = await ipns.extractPublicKeyFromIPNSRecord(peerId, ipnsRecord) ``` ### Marshal data with proto buffer @@ -86,9 +80,9 @@ const publicKey = await ipns.extractPublicKey(peerId, ipnsRecord) ```js import * as ipns from 'ipns' -const ipnsRecord = await ipns.create(privateKey, value, sequenceNumber, lifetime) +const ipnsRecord = await ipns.createIPNSRecord(privateKey, value, sequenceNumber, lifetime) // ... -const marshalledData = ipns.marshal(ipnsRecord) +const marshalledData = ipns.marshalIPNSRecord(ipnsRecord) // ... ``` @@ -99,89 +93,16 @@ Returns the record data serialized. ```js import * as ipns from 'ipns' -const ipnsRecord = ipns.unmarshal(storedData) +const ipnsRecord = ipns.unmarshalIPNSRecord(storedData) ``` Returns the `IPNSRecord` after being deserialized. -### Validator - -```js -import * as ipns from 'ipns' - -const validator = ipns.validator -``` - -Contains an object with `validate (marshalledData, key)` and `select (dataA, dataB)` functions. - -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`. - -## API - -### Create record - -```js - -ipns.create(privateKey, value, sequenceNumber, lifetime, options) -``` - -Create an IPNS record for being stored in a protocol buffer. - -- `privateKey` ([PrivateKey](https://libp2p.github.io/js-libp2p/interfaces/_libp2p_interface.keys.PrivateKey.html)): key to be used for cryptographic operations. -- `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 a `IPNSRecord`. - -### Validate record - -```js -ipns.validate(publicKey, ipnsRecord) -``` - -Validate an IPNS record previously stored in a protocol buffer. - -- `publicKey` ([PublicKey](https://libp2p.github.io/js-libp2p/interfaces/_libp2p_interface.keys.PublicKey.html)): key to be used for cryptographic operations. -- `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(ipnsRecord) -``` - -Returns the serialized IPNS record. -- `ipnsRecord` (`IPNSRecord`): ipns record (obtained using the create function). - -### Unmarshal data from proto buffer - -```js -const data = ipns.unmarshal(storedData) -``` - -Returns a `IPNSRecord` after being serialized. - -- `storedData` (Uint8Array): ipns record serialized. - -### Extract public key from record - -```js -const publicKey = await ipns.extractPublicKey(peerId, ipnsRecord) -``` - -Extract a public key from an IPNS record. +## API Docs -- `peerId` ([PeerId](https://libp2p.github.io/js-libp2p/types/_libp2p_interface.peer_id.PeerId.html)): peer identifier object. -- `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. ### Namespace @@ -199,9 +120,6 @@ ipns.namespaceLength // 6 ``` -## API Docs - -- ## License diff --git a/src/index.ts b/src/index.ts index 041f9d9..da8b321 100644 --- a/src/index.ts +++ b/src/index.ts @@ -146,10 +146,10 @@ const defaultCreateOptions: CreateOptions = { * 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. + * The passed value can be a CID, a PublicKey or an arbitrary string path e.g. `/ipfs/...` or `/ipns/...`. * * * 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}` + * * PublicKeys 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 {PrivateKey} privateKey - the private key for signing the record. @@ -174,10 +174,10 @@ export async function createIPNSRecord (privateKey: PrivateKey, value: CID | Pub * 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. + * The passed value can be a CID, a PublicKey or an arbitrary string path e.g. `/ipfs/...` or `/ipns/...`. * * * 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}` + * * PublicKeys 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 {PrivateKey} privateKey - the private key for signing the record. diff --git a/src/selector.ts b/src/selector.ts index c669a5b..5dcbb11 100644 --- a/src/selector.ts +++ b/src/selector.ts @@ -2,6 +2,17 @@ import NanoDate from 'timestamp-nano' import { IpnsEntry } from './pb/ipns.js' import { unmarshalIPNSRecord } from './utils.js' +/** + * Selects the latest valid IPNS record from an array of marshalled IPNS records. + * + * Records are sorted by: + * 1. Sequence number (higher takes precedence) + * 2. Validity time for EOL records with same sequence number (longer lived record takes precedence) + * + * @param key - The routing key for the IPNS record + * @param data - Array of marshalled IPNS records to select from + * @returns The index of the most valid record from the input array + */ export function ipnsSelector (key: Uint8Array, data: Uint8Array[]): number { const entries = data.map((buf, index) => ({ record: unmarshalIPNSRecord(buf), diff --git a/src/validator.ts b/src/validator.ts index 2671c39..9427120 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -18,11 +18,11 @@ const MAX_RECORD_SIZE = 1024 * 10 * Validates the given IPNS Record against the given public key. We need a "raw" * record in order to be able to access to all of its fields. */ -export const validate = async (publicKey: PublicKey, buf: Uint8Array): Promise => { +export const validate = async (publicKey: PublicKey, marshalledRecord: Uint8Array): Promise => { // unmarshal ensures that (1) SignatureV2 and Data are present, (2) that ValidityType // and Validity are of valid types and have a value, (3) that CBOR data matches protobuf // if it's a V1+V2 record. - const record = unmarshalIPNSRecord(buf) + const record = unmarshalIPNSRecord(marshalledRecord) // Validate Signature V2 let isValid @@ -53,13 +53,21 @@ export const validate = async (publicKey: PublicKey, buf: Uint8Array): Promise { - if (marshalledData.byteLength > MAX_RECORD_SIZE) { +/** + * Validate the given IPNS record against the given routing key. + * + * @see https://specs.ipfs.tech/ipns/ipns-record/#routing-record for the binary format of the routing key + * + * @param routingKey - The routing key in binary format: binary(ascii(IPNS_PREFIX) + multihash(public key)) + * @param marshalledRecord - The marshalled record to validate. + */ +export async function ipnsValidator (routingKey: Uint8Array, marshalledRecord: Uint8Array): Promise { + if (marshalledRecord.byteLength > MAX_RECORD_SIZE) { throw new RecordTooLargeError('The record is too large') } // try to extract public key from routing key - const routingMultihash = multihashFromIPNSRoutingKey(key) + const routingMultihash = multihashFromIPNSRoutingKey(routingKey) let routingPubKey: PublicKey | undefined // identity hash @@ -68,19 +76,19 @@ export async function ipnsValidator (key: Uint8Array, marshalledData: Uint8Array } // extract public key from record - const receivedRecord = unmarshalIPNSRecord(marshalledData) + const receivedRecord = unmarshalIPNSRecord(marshalledRecord) const recordPubKey = extractPublicKeyFromIPNSRecord(receivedRecord) ?? routingPubKey if (recordPubKey == null) { throw new InvalidEmbeddedPublicKeyError('Could not extract public key from IPNS record or routing key') } - const routingKey = multihashToIPNSRoutingKey(recordPubKey.toMultihash()) + const expectedRoutingKey = multihashToIPNSRoutingKey(recordPubKey.toMultihash()) - if (!uint8ArrayEquals(key, routingKey)) { + if (!uint8ArrayEquals(expectedRoutingKey, routingKey)) { throw new InvalidEmbeddedPublicKeyError('Embedded public key did not match routing key') } // Record validation - await validate(recordPubKey, marshalledData) + await validate(recordPubKey, marshalledRecord) } diff --git a/test/utils.spec.ts b/test/utils.spec.ts index 69efc9a..42ee19e 100644 --- a/test/utils.spec.ts +++ b/test/utils.spec.ts @@ -22,6 +22,16 @@ describe('utils', () => { output: '/ipns/k73ap3wtp70r7cd9ofyhwgogv1j96huvtvfnsof5spyfaaopkxmonumi4fckgguqr' }, + // path input + '/ipfs/CID path': { + input: '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq/docs/readme.md', + output: '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq/docs/readme.md' + }, + '/ipns/CID path': { + input: '/ipns/k51qzi5uqu5djni72pr40dt64kxlh0zb8baat8h7dtdvkov66euc2lho0oidr3', + output: '/ipns/k51qzi5uqu5djni72pr40dt64kxlh0zb8baat8h7dtdvkov66euc2lho0oidr3' + }, + // peer id input 'Ed25519 PeerId': { input: peerIdFromString('12D3KooWKBpVwnRACfEsk6QME7dA5CZnFYVHQ7Zc927BEzuUekQe'),