diff --git a/README.md b/README.md index a85d036..cd7df0e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ const ipns = require('ipns') ipns.create(privateKey, value, sequenceNumber, lifetime, (err, entryData) => { // your code goes here -}); +}) ``` #### Validate record @@ -51,23 +51,35 @@ const ipns = require('ipns') ipns.validate(publicKey, ipnsEntry, (err) => { // your code goes here // if no error, the record is valid -}); +}) ``` #### Embed public key to record -> Not available yet +```js +const ipns = require('ipns') + +ipns.embedPublicKey(publicKey, ipnsEntry, (err, ipnsEntryWithEmbedPublicKey) => { + // your code goes here +}) +``` #### Extract public key from record -> Not available yet +```js +const ipns = require('ipns') + +ipns.extractPublicKey(peerId, ipnsEntry, (err, publicKey) => { + // your code goes here +}) +``` #### Datastore key ```js const ipns = require('ipns') -ipns.getLocalKey(peerId); +ipns.getLocalKey(peerId) ``` Returns a key to be used for storing the ipns entry locally, that is: @@ -85,7 +97,7 @@ ipns.create(privateKey, value, sequenceNumber, lifetime, (err, entryData) => { // ... const marshalledData = ipns.marshal(entryData) // ... -}); +}) ``` Returns the entry data serialized. @@ -106,7 +118,7 @@ Returns the entry data structure after being serialized. ```js -ipns.create(privateKey, value, sequenceNumber, lifetime, [callback]); +ipns.create(privateKey, value, sequenceNumber, lifetime, [callback]) ``` Create an IPNS record for being stored in a protocol buffer. @@ -133,7 +145,7 @@ Create an IPNS record for being stored in a protocol buffer. ```js -ipns.validate(publicKey, ipnsEntry, [callback]); +ipns.validate(publicKey, ipnsEntry, [callback]) ``` Validate an IPNS record previously stored in a protocol buffer. @@ -147,7 +159,7 @@ Validate an IPNS record previously stored in a protocol buffer. #### Datastore key ```js -ipns.getDatastoreKey(peerId); +ipns.getDatastoreKey(peerId) ``` Get a key for storing the ipns entry in the datastore. @@ -174,6 +186,34 @@ Returns the entry data structure after being serialized. - `storedData` (Buffer): ipns entry record serialized. +#### Embed public key to record + +```js +ipns.embedPublicKey(publicKey, ipnsEntry, [callback]) +``` + +Embed a public key in an IPNS entry. If it is possible to extract the public key from the `peer-id`, there is no need to embed. + +- `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). +- `callback` (function): operation result. + +`callback` must follow `function (err, resultEntry) {}` signature, where `err` is an error if the operation was not successful. This way, if no error, the operation was successful. If the `resultEntry` is also null, the `peer-id` allows to extract the public key from the `peer-id` and there is no need in extracting it. + +#### Extract public key from record + +```js +ipns.extractPublicKey(peerId, ipnsEntry, [callback]) +``` + +Extract a public key from an IPNS entry. + +- `peerId` (`PeerId` [Instance](https://github.com/libp2p/js-peer-id)): peer identifier object. +- `ipnsEntry` (Object): ipns entry record (obtained using the create function). +- `callback` (function): operation result. + +`callback` must follow `function (err, publicKey) {}` signature, where `err` is an error if the operation was not successful. This way, if no error, the validation was successful. The public key (`PubKey` [RSA Instance](https://github.com/libp2p/js-libp2p-crypto/blob/master/src/keys/rsa-class.js)): may be used for cryptographic operations. + ## Contribute Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/js-ipns/issues)! diff --git a/package.json b/package.json index 3a6d9eb..60d6320 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,10 @@ "debug": "^3.1.0", "interface-datastore": "^0.4.2", "left-pad": "^1.3.0", + "libp2p-crypto": "^0.13.0", + "multihashes": "^0.4.14", "nano-date": "^2.1.0", + "peer-id": "^0.11.0", "protons": "^1.0.1" }, "devDependencies": { @@ -48,9 +51,7 @@ "chai-string": "^1.4.0", "dirty-chai": "^2.0.1", "ipfs": "^0.29.3", - "ipfsd-ctl": "^0.36.0", - "libp2p-crypto": "^0.13.0", - "multihashes": "^0.4.13" + "ipfsd-ctl": "^0.36.0" }, "contributors": [ "Vasco Santos " diff --git a/src/errors.js b/src/errors.js index ac8e3b1..3b6853c 100644 --- a/src/errors.js +++ b/src/errors.js @@ -5,3 +5,6 @@ exports.ERR_UNRECOGNIZED_VALIDITY = 'ERR_UNRECOGNIZED_VALIDITY' exports.ERR_SIGNATURE_CREATION = 'ERR_SIGNATURE_CREATION' exports.ERR_SIGNATURE_VERIFICATION = 'ERR_SIGNATURE_VERIFICATION' exports.ERR_UNRECOGNIZED_FORMAT = 'ERR_UNRECOGNIZED_FORMAT' +exports.ERR_PEER_ID_FROM_PUBLIC_KEY = 'ERR_PEER_ID_FROM_PUBLIC_KEY' +exports.ERR_PUBLIC_KEY_FROM_ID = 'ERR_PUBLIC_KEY_FROM_ID' +exports.ERR_UNDEFINED_PARAMETER = 'ERR_UNDEFINED_PARAMETER' diff --git a/src/index.js b/src/index.js index 8f4558c..419ef12 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,9 @@ const base32Encode = require('base32-encode') const Big = require('big.js') const NanoDate = require('nano-date').default const { Key } = require('interface-datastore') +const crypto = require('libp2p-crypto') +const PeerId = require('peer-id') +const multihash = require('multihashes') const debug = require('debug') const log = debug('jsipns') @@ -13,6 +16,7 @@ const ipnsEntryProto = require('./pb/ipns.proto') const { parseRFC3339 } = require('./utils') const ERRORS = require('./errors') +const ID_MULTIHASH_CODE = multihash.names.id /** * 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. @@ -44,7 +48,7 @@ const create = (privateKey, value, seq, lifetime, callback) => { const entry = { value: value, - signature: signature, // TODO confirm format compliance with go-ipfs + signature: signature, validityType: validityType, validity: isoValidity, sequence: seq @@ -68,7 +72,7 @@ const validate = (publicKey, entry, callback) => { const dataForSignature = ipnsEntryDataForSig(value, validityType, validity) // Validate Signature - publicKey.verify(dataForSignature, entry.signature, (err, result) => { + publicKey.verify(dataForSignature, entry.signature, (err) => { if (err) { log.error('record signature verification failed') return callback(Object.assign(new Error('record signature verification failed'), { code: ERRORS.ERR_SIGNATURE_VERIFICATION })) @@ -100,15 +104,56 @@ const validate = (publicKey, entry, callback) => { } /** - * Validates the given ipns entry against the given public key. + * Embed the given public key in the given entry. While not strictly required, + * some nodes (eg. DHT servers) may reject IPNS entries that don't embed their + * public keys as they may not be able to validate them efficiently. + * As a consequence of nodes needing to validade a record upon receipt, they need + * the public key associated with it. For olde RSA keys, it is easier if we just + * send this as part of the record itself. For newer ed25519 keys, the public key + * can be embedded in the peerId. * - * @param {Object} publicKey public key for validating the record. + * @param {Object} publicKey public key to embed. * @param {Object} entry ipns entry record. * @param {function(Error)} [callback] * @return {Void} */ const embedPublicKey = (publicKey, entry, callback) => { - callback(new Error('not implemented yet')) + if (!publicKey || !publicKey.bytes || !entry) { + const error = 'one or more of the provided parameters are not defined' + + log.error(error) + return callback(Object.assign(new Error(error), { code: ERRORS.ERR_UNDEFINED_PARAMETER })) + } + + // Create a peer id from the public key. + PeerId.createFromPubKey(publicKey.bytes, (err, peerId) => { + if (err) { + log.error(err) + return callback(Object.assign(new Error(err), { code: ERRORS.ERR_PEER_ID_FROM_PUBLIC_KEY })) + } + + // Try to extract the public key from the ID. If we can, no need to embed it + let extractedPublicKey + try { + extractedPublicKey = extractPublicKeyFromId(peerId) + } catch (err) { + log.error(err) + return callback(Object.assign(new Error(err), { code: ERRORS.ERR_PUBLIC_KEY_FROM_ID })) + } + + if (extractedPublicKey) { + return callback(null, null) + } + + // If we failed to extract the public key from the peer ID, embed it in the record. + try { + entry.pubKey = crypto.keys.marshalPublicKey(publicKey) + } catch (err) { + log.error(err) + return callback(err) + } + callback(null, entry) + }) } /** @@ -120,7 +165,24 @@ const embedPublicKey = (publicKey, entry, callback) => { * @return {Void} */ const extractPublicKey = (peerId, entry, callback) => { - callback(new Error('not implemented yet')) + if (!entry || !peerId) { + const error = 'one or more of the provided parameters are not defined' + + log.error(error) + return callback(Object.assign(new Error(error), { code: ERRORS.ERR_UNDEFINED_PARAMETER })) + } + + if (entry.pubKey) { + let pubKey + try { + pubKey = crypto.keys.unmarshalPublicKey(entry.pubKey) + } catch (err) { + log.error(err) + return callback(err) + } + return callback(null, pubKey) + } + callback(null, peerId.pubKey) } // rawStdEncoding with RFC4648 @@ -139,16 +201,16 @@ const getLocalKey = (key) => new Key(`/ipns/${rawStdEncoding(key)}`) * Get key for sharing the record in the routing mechanism. * Format: ${base32(/ipns/)}, ${base32(/pk/)} * - * @param {Buffer} key peer identifier object. + * @param {Buffer} pid peer identifier represented by the multihash of the public key as Buffer. * @returns {Object} containing the `nameKey` and the `ipnsKey`. */ -const getIdKeys = (key) => { +const getIdKeys = (pid) => { const pkBuffer = Buffer.from('/pk/') const ipnsBuffer = Buffer.from('/ipns/') return { - nameKey: rawStdEncoding(Buffer.concat([pkBuffer, key])), - ipnsKey: rawStdEncoding(Buffer.concat([ipnsBuffer, key])) + pkKey: new Key(rawStdEncoding(Buffer.concat([pkBuffer, pid]))), + ipnsKey: new Key(rawStdEncoding(Buffer.concat([ipnsBuffer, pid]))) } } @@ -164,13 +226,35 @@ const sign = (privateKey, value, validityType, validity, callback) => { }) } -// Create record data for being signed -const ipnsEntryDataForSig = (value, validityType, eol) => { +// Utility for getting the validity type code name of a validity +const getValidityType = (validityType) => { + if (validityType.toString() === '0') { + return 'EOL' + } else { + const error = `unrecognized validity type ${validityType.toString()}` + log.error(error) + throw Object.assign(new Error(error), { code: ERRORS.ERR_UNRECOGNIZED_VALIDITY }) + } +} + +// Utility for creating the record data for being signed +const ipnsEntryDataForSig = (value, validityType, validity) => { const valueBuffer = Buffer.from(value) - const validityTypeBuffer = Buffer.from(validityType.toString()) - const eolBuffer = Buffer.from(eol) + const validityTypeBuffer = Buffer.from(getValidityType(validityType)) + const validityBuffer = Buffer.from(validity) + + return Buffer.concat([valueBuffer, validityBuffer, validityTypeBuffer]) +} + +// Utility for extracting the public key from a peer-id +const extractPublicKeyFromId = (peerId) => { + const decodedId = multihash.decode(peerId.id) + + if (decodedId.code !== ID_MULTIHASH_CODE) { + return null + } - return Buffer.concat([valueBuffer, validityTypeBuffer, eolBuffer]) + return crypto.keys.unmarshalPublicKey(decodedId.digest) } module.exports = { diff --git a/test/index.spec.js b/test/index.spec.js index eb7ef25..761df81 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -146,9 +146,70 @@ describe('ipns', function () { const idKeys = ipns.getIdKeys(fromB58String(ipfsId.id)) expect(idKeys).to.exist() - expect(idKeys).to.have.a.property('nameKey') + expect(idKeys).to.have.a.property('pkKey') expect(idKeys).to.have.a.property('ipnsKey') - expect(idKeys.nameKey).to.not.startsWith('/pk/') + expect(idKeys.pkKey).to.not.startsWith('/pk/') expect(idKeys.ipnsKey).to.not.startsWith('/ipns/') }) + + it('should be able to embed a public key in an ipns record', (done) => { + const sequence = 0 + const validity = 1000000 + + ipns.create(rsa, cid, sequence, validity, (err, entry) => { + expect(err).to.not.exist() + + ipns.embedPublicKey(rsa.public, entry, (err, entry) => { + expect(err).to.not.exist() + expect(entry).to.deep.include({ + pubKey: rsa.public.bytes + }) + done() + }) + }) + }) + + // It should have a public key embeded for newer ed25519 keys + // https://github.com/ipfs/go-ipns/blob/d51115b4b14ed7fcca5472aadff0fee6772aca8c/ipns.go#L81 + // https://github.com/ipfs/go-ipns/blob/d51115b4b14ed7fcca5472aadff0fee6772aca8c/ipns_test.go + // https://github.com/libp2p/go-libp2p-peer/blob/7f219a1e70011a258c5d3e502aef6896c60d03ce/peer.go#L80 + // IDFromEd25519PublicKey is not currently implement on js-libp2p-peer + // https://github.com/libp2p/go-libp2p-peer/pull/30 + it.skip('should be able to extract a public key directly from the peer', (done) => { + const sequence = 0 + const validity = 1000000 + + crypto.keys.generateKeyPair('ed25519', 2048, (err, ed25519) => { + expect(err).to.not.exist() + + ipns.create(ed25519, cid, sequence, validity, (err, entry) => { + expect(err).to.not.exist() + + ipns.embedPublicKey(ed25519.public, entry, (err, entry) => { + expect(err).to.not.exist() + expect(entry).to.not.exist() // Should be null + done() + }) + }) + }) + }) + + it('should be able to export a previously embed public key from an ipns record', (done) => { + const sequence = 0 + const validity = 1000000 + + ipns.create(rsa, cid, sequence, validity, (err, entry) => { + expect(err).to.not.exist() + + ipns.embedPublicKey(rsa.public, entry, (err, entry) => { + expect(err).to.not.exist() + + ipns.extractPublicKey(ipfsId, entry, (err, publicKey) => { + expect(err).to.not.exist() + expect(publicKey.bytes).to.equalBytes(rsa.public.bytes) + done() + }) + }) + }) + }) })