Skip to content

Commit

Permalink
feat: add public key support (#4)
Browse files Browse the repository at this point in the history
* feat: add public key support
  • Loading branch information
vasco-santos authored Aug 9, 2018
1 parent 981801e commit e743632
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 29 deletions.
58 changes: 49 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const ipns = require('ipns')

ipns.create(privateKey, value, sequenceNumber, lifetime, (err, entryData) => {
// your code goes here
});
})
```

#### Validate record
Expand All @@ -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:
Expand All @@ -85,7 +97,7 @@ ipns.create(privateKey, value, sequenceNumber, lifetime, (err, entryData) => {
// ...
const marshalledData = ipns.marshal(entryData)
// ...
});
})
```

Returns the entry data serialized.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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)!
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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 <vasco.santos@ua.pt>"
Expand Down
3 changes: 3 additions & 0 deletions src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
114 changes: 99 additions & 15 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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 }))
Expand Down Expand Up @@ -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)
})
}

/**
Expand All @@ -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
Expand All @@ -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/<HASH>)}, ${base32(/pk/<HASH>)}
*
* @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])))
}
}

Expand All @@ -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 = {
Expand Down
Loading

0 comments on commit e743632

Please sign in to comment.