Skip to content

Commit e743632

Browse files
authored
feat: add public key support (#4)
* feat: add public key support
1 parent 981801e commit e743632

File tree

5 files changed

+218
-29
lines changed

5 files changed

+218
-29
lines changed

README.md

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const ipns = require('ipns')
4040

4141
ipns.create(privateKey, value, sequenceNumber, lifetime, (err, entryData) => {
4242
// your code goes here
43-
});
43+
})
4444
```
4545

4646
#### Validate record
@@ -51,23 +51,35 @@ const ipns = require('ipns')
5151
ipns.validate(publicKey, ipnsEntry, (err) => {
5252
// your code goes here
5353
// if no error, the record is valid
54-
});
54+
})
5555
```
5656

5757
#### Embed public key to record
5858

59-
> Not available yet
59+
```js
60+
const ipns = require('ipns')
61+
62+
ipns.embedPublicKey(publicKey, ipnsEntry, (err, ipnsEntryWithEmbedPublicKey) => {
63+
// your code goes here
64+
})
65+
```
6066

6167
#### Extract public key from record
6268

63-
> Not available yet
69+
```js
70+
const ipns = require('ipns')
71+
72+
ipns.extractPublicKey(peerId, ipnsEntry, (err, publicKey) => {
73+
// your code goes here
74+
})
75+
```
6476

6577
#### Datastore key
6678

6779
```js
6880
const ipns = require('ipns')
6981

70-
ipns.getLocalKey(peerId);
82+
ipns.getLocalKey(peerId)
7183
```
7284

7385
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) => {
8597
// ...
8698
const marshalledData = ipns.marshal(entryData)
8799
// ...
88-
});
100+
})
89101
```
90102

91103
Returns the entry data serialized.
@@ -106,7 +118,7 @@ Returns the entry data structure after being serialized.
106118

107119
```js
108120

109-
ipns.create(privateKey, value, sequenceNumber, lifetime, [callback]);
121+
ipns.create(privateKey, value, sequenceNumber, lifetime, [callback])
110122
```
111123

112124
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.
133145

134146
```js
135147

136-
ipns.validate(publicKey, ipnsEntry, [callback]);
148+
ipns.validate(publicKey, ipnsEntry, [callback])
137149
```
138150

139151
Validate an IPNS record previously stored in a protocol buffer.
@@ -147,7 +159,7 @@ Validate an IPNS record previously stored in a protocol buffer.
147159
#### Datastore key
148160

149161
```js
150-
ipns.getDatastoreKey(peerId);
162+
ipns.getDatastoreKey(peerId)
151163
```
152164

153165
Get a key for storing the ipns entry in the datastore.
@@ -174,6 +186,34 @@ Returns the entry data structure after being serialized.
174186

175187
- `storedData` (Buffer): ipns entry record serialized.
176188

189+
#### Embed public key to record
190+
191+
```js
192+
ipns.embedPublicKey(publicKey, ipnsEntry, [callback])
193+
```
194+
195+
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.
196+
197+
- `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.
198+
- `ipnsEntry` (Object): ipns entry record (obtained using the create function).
199+
- `callback` (function): operation result.
200+
201+
`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.
202+
203+
#### Extract public key from record
204+
205+
```js
206+
ipns.extractPublicKey(peerId, ipnsEntry, [callback])
207+
```
208+
209+
Extract a public key from an IPNS entry.
210+
211+
- `peerId` (`PeerId` [Instance](https://github.com/libp2p/js-peer-id)): peer identifier object.
212+
- `ipnsEntry` (Object): ipns entry record (obtained using the create function).
213+
- `callback` (function): operation result.
214+
215+
`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.
216+
177217
## Contribute
178218

179219
Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/js-ipns/issues)!

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@
3838
"debug": "^3.1.0",
3939
"interface-datastore": "^0.4.2",
4040
"left-pad": "^1.3.0",
41+
"libp2p-crypto": "^0.13.0",
42+
"multihashes": "^0.4.14",
4143
"nano-date": "^2.1.0",
44+
"peer-id": "^0.11.0",
4245
"protons": "^1.0.1"
4346
},
4447
"devDependencies": {
@@ -48,9 +51,7 @@
4851
"chai-string": "^1.4.0",
4952
"dirty-chai": "^2.0.1",
5053
"ipfs": "^0.29.3",
51-
"ipfsd-ctl": "^0.36.0",
52-
"libp2p-crypto": "^0.13.0",
53-
"multihashes": "^0.4.13"
54+
"ipfsd-ctl": "^0.36.0"
5455
},
5556
"contributors": [
5657
"Vasco Santos <vasco.santos@ua.pt>"

src/errors.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ exports.ERR_UNRECOGNIZED_VALIDITY = 'ERR_UNRECOGNIZED_VALIDITY'
55
exports.ERR_SIGNATURE_CREATION = 'ERR_SIGNATURE_CREATION'
66
exports.ERR_SIGNATURE_VERIFICATION = 'ERR_SIGNATURE_VERIFICATION'
77
exports.ERR_UNRECOGNIZED_FORMAT = 'ERR_UNRECOGNIZED_FORMAT'
8+
exports.ERR_PEER_ID_FROM_PUBLIC_KEY = 'ERR_PEER_ID_FROM_PUBLIC_KEY'
9+
exports.ERR_PUBLIC_KEY_FROM_ID = 'ERR_PUBLIC_KEY_FROM_ID'
10+
exports.ERR_UNDEFINED_PARAMETER = 'ERR_UNDEFINED_PARAMETER'

src/index.js

Lines changed: 99 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ const base32Encode = require('base32-encode')
44
const Big = require('big.js')
55
const NanoDate = require('nano-date').default
66
const { Key } = require('interface-datastore')
7+
const crypto = require('libp2p-crypto')
8+
const PeerId = require('peer-id')
9+
const multihash = require('multihashes')
710

811
const debug = require('debug')
912
const log = debug('jsipns')
@@ -13,6 +16,7 @@ const ipnsEntryProto = require('./pb/ipns.proto')
1316
const { parseRFC3339 } = require('./utils')
1417
const ERRORS = require('./errors')
1518

19+
const ID_MULTIHASH_CODE = multihash.names.id
1620
/**
1721
* Creates a new ipns entry and signs it with the given private key.
1822
* 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) => {
4448

4549
const entry = {
4650
value: value,
47-
signature: signature, // TODO confirm format compliance with go-ipfs
51+
signature: signature,
4852
validityType: validityType,
4953
validity: isoValidity,
5054
sequence: seq
@@ -68,7 +72,7 @@ const validate = (publicKey, entry, callback) => {
6872
const dataForSignature = ipnsEntryDataForSig(value, validityType, validity)
6973

7074
// Validate Signature
71-
publicKey.verify(dataForSignature, entry.signature, (err, result) => {
75+
publicKey.verify(dataForSignature, entry.signature, (err) => {
7276
if (err) {
7377
log.error('record signature verification failed')
7478
return callback(Object.assign(new Error('record signature verification failed'), { code: ERRORS.ERR_SIGNATURE_VERIFICATION }))
@@ -100,15 +104,56 @@ const validate = (publicKey, entry, callback) => {
100104
}
101105

102106
/**
103-
* Validates the given ipns entry against the given public key.
107+
* Embed the given public key in the given entry. While not strictly required,
108+
* some nodes (eg. DHT servers) may reject IPNS entries that don't embed their
109+
* public keys as they may not be able to validate them efficiently.
110+
* As a consequence of nodes needing to validade a record upon receipt, they need
111+
* the public key associated with it. For olde RSA keys, it is easier if we just
112+
* send this as part of the record itself. For newer ed25519 keys, the public key
113+
* can be embedded in the peerId.
104114
*
105-
* @param {Object} publicKey public key for validating the record.
115+
* @param {Object} publicKey public key to embed.
106116
* @param {Object} entry ipns entry record.
107117
* @param {function(Error)} [callback]
108118
* @return {Void}
109119
*/
110120
const embedPublicKey = (publicKey, entry, callback) => {
111-
callback(new Error('not implemented yet'))
121+
if (!publicKey || !publicKey.bytes || !entry) {
122+
const error = 'one or more of the provided parameters are not defined'
123+
124+
log.error(error)
125+
return callback(Object.assign(new Error(error), { code: ERRORS.ERR_UNDEFINED_PARAMETER }))
126+
}
127+
128+
// Create a peer id from the public key.
129+
PeerId.createFromPubKey(publicKey.bytes, (err, peerId) => {
130+
if (err) {
131+
log.error(err)
132+
return callback(Object.assign(new Error(err), { code: ERRORS.ERR_PEER_ID_FROM_PUBLIC_KEY }))
133+
}
134+
135+
// Try to extract the public key from the ID. If we can, no need to embed it
136+
let extractedPublicKey
137+
try {
138+
extractedPublicKey = extractPublicKeyFromId(peerId)
139+
} catch (err) {
140+
log.error(err)
141+
return callback(Object.assign(new Error(err), { code: ERRORS.ERR_PUBLIC_KEY_FROM_ID }))
142+
}
143+
144+
if (extractedPublicKey) {
145+
return callback(null, null)
146+
}
147+
148+
// If we failed to extract the public key from the peer ID, embed it in the record.
149+
try {
150+
entry.pubKey = crypto.keys.marshalPublicKey(publicKey)
151+
} catch (err) {
152+
log.error(err)
153+
return callback(err)
154+
}
155+
callback(null, entry)
156+
})
112157
}
113158

114159
/**
@@ -120,7 +165,24 @@ const embedPublicKey = (publicKey, entry, callback) => {
120165
* @return {Void}
121166
*/
122167
const extractPublicKey = (peerId, entry, callback) => {
123-
callback(new Error('not implemented yet'))
168+
if (!entry || !peerId) {
169+
const error = 'one or more of the provided parameters are not defined'
170+
171+
log.error(error)
172+
return callback(Object.assign(new Error(error), { code: ERRORS.ERR_UNDEFINED_PARAMETER }))
173+
}
174+
175+
if (entry.pubKey) {
176+
let pubKey
177+
try {
178+
pubKey = crypto.keys.unmarshalPublicKey(entry.pubKey)
179+
} catch (err) {
180+
log.error(err)
181+
return callback(err)
182+
}
183+
return callback(null, pubKey)
184+
}
185+
callback(null, peerId.pubKey)
124186
}
125187

126188
// rawStdEncoding with RFC4648
@@ -139,16 +201,16 @@ const getLocalKey = (key) => new Key(`/ipns/${rawStdEncoding(key)}`)
139201
* Get key for sharing the record in the routing mechanism.
140202
* Format: ${base32(/ipns/<HASH>)}, ${base32(/pk/<HASH>)}
141203
*
142-
* @param {Buffer} key peer identifier object.
204+
* @param {Buffer} pid peer identifier represented by the multihash of the public key as Buffer.
143205
* @returns {Object} containing the `nameKey` and the `ipnsKey`.
144206
*/
145-
const getIdKeys = (key) => {
207+
const getIdKeys = (pid) => {
146208
const pkBuffer = Buffer.from('/pk/')
147209
const ipnsBuffer = Buffer.from('/ipns/')
148210

149211
return {
150-
nameKey: rawStdEncoding(Buffer.concat([pkBuffer, key])),
151-
ipnsKey: rawStdEncoding(Buffer.concat([ipnsBuffer, key]))
212+
pkKey: new Key(rawStdEncoding(Buffer.concat([pkBuffer, pid]))),
213+
ipnsKey: new Key(rawStdEncoding(Buffer.concat([ipnsBuffer, pid])))
152214
}
153215
}
154216

@@ -164,13 +226,35 @@ const sign = (privateKey, value, validityType, validity, callback) => {
164226
})
165227
}
166228

167-
// Create record data for being signed
168-
const ipnsEntryDataForSig = (value, validityType, eol) => {
229+
// Utility for getting the validity type code name of a validity
230+
const getValidityType = (validityType) => {
231+
if (validityType.toString() === '0') {
232+
return 'EOL'
233+
} else {
234+
const error = `unrecognized validity type ${validityType.toString()}`
235+
log.error(error)
236+
throw Object.assign(new Error(error), { code: ERRORS.ERR_UNRECOGNIZED_VALIDITY })
237+
}
238+
}
239+
240+
// Utility for creating the record data for being signed
241+
const ipnsEntryDataForSig = (value, validityType, validity) => {
169242
const valueBuffer = Buffer.from(value)
170-
const validityTypeBuffer = Buffer.from(validityType.toString())
171-
const eolBuffer = Buffer.from(eol)
243+
const validityTypeBuffer = Buffer.from(getValidityType(validityType))
244+
const validityBuffer = Buffer.from(validity)
245+
246+
return Buffer.concat([valueBuffer, validityBuffer, validityTypeBuffer])
247+
}
248+
249+
// Utility for extracting the public key from a peer-id
250+
const extractPublicKeyFromId = (peerId) => {
251+
const decodedId = multihash.decode(peerId.id)
252+
253+
if (decodedId.code !== ID_MULTIHASH_CODE) {
254+
return null
255+
}
172256

173-
return Buffer.concat([valueBuffer, validityTypeBuffer, eolBuffer])
257+
return crypto.keys.unmarshalPublicKey(decodedId.digest)
174258
}
175259

176260
module.exports = {

0 commit comments

Comments
 (0)