diff --git a/packages/compliance-tests/package.json b/packages/compliance-tests/package.json index 7d244f722..107b3834d 100644 --- a/packages/compliance-tests/package.json +++ b/packages/compliance-tests/package.json @@ -43,8 +43,10 @@ "it-goodbye": "^3.0.0", "it-pair": "^1.0.0", "it-pipe": "^1.1.0", + "libp2p-crypto": "^0.19.5", "libp2p-interfaces": "^1.2.0", "multiaddr": "^10.0.0", + "multiformats": "^9.4.9", "p-defer": "^3.0.0", "p-limit": "^3.1.0", "p-wait-for": "^3.2.0", diff --git a/packages/compliance-tests/src/peer-id/index.js b/packages/compliance-tests/src/peer-id/index.js new file mode 100644 index 000000000..fa20d01ed --- /dev/null +++ b/packages/compliance-tests/src/peer-id/index.js @@ -0,0 +1,411 @@ +// @ts-nocheck interface tests +/* eslint-env mocha */ +'use strict' + +const { expect } = require('aegir/utils/chai') +const crypto = require('libp2p-crypto') +const { CID } = require('multiformats/cid') +const Digest = require('multiformats/hashes/digest') +const { base16 } = require('multiformats/bases/base16') +const { base36 } = require('multiformats/bases/base36') +const { base58btc } = require('multiformats/bases/base58') +const { identity } = require('multiformats/hashes/identity') +const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') +const { toString: uint8ArrayToString } = require('uint8arrays/to-string') + +const DAG_PB_CODE = 0x70 +const LIBP2P_KEY_CODE = 0x72 +const RAW_CODE = 0x55 + +const testOpts = { + bits: 512 +} + +const goId = { + id: 'QmRLoXS3E73psYaUsma1VSbboTa2J8Z9kso1tpiGLk9WQ4', + privKey: 'CAASpwkwggSjAgEAAoIBAQDWBEbO8kc6a5kEks09CKPQargY3p0DCmCczoCT52/RYFqxvH9dI+s+u4ZAvF9aLWOBvFomL7jHZODPxKDrbiNCmyEbViNgZYK+PNbwh0V3ZGbB27X3q8yZtLvYA8dhcNkz/2SHBarSoC4QLA5MXUuSWtVaYMY3MzMnzBF57Jc9Ase7NvHOIUI90M7aN5izP7hxPXpZ+shiN+TyjM8mFxYONG7ZSsY3IxUhtrU5MRzFX+tp1o/gb/aa51mHf7AL3N02j5ABiYbCK97Rbwr03hsBcwgMxoDPJmP3WZ+D5yyPcOIIF1Vd7+4/f7FQJnIw3xr9/jvaFbPyDCVbBOhr9oyxAgMBAAECggEALlrgx2Q8v0+c5hux7p1XdgYXd/OHyKfPw0cLHH4NfylCm6q7X34vLvhJHO5wLMUV/3y/ffPqLu4Pr5DkVfoWExAsvJIMuY1jIzdkStbR2glaJHUlVc7VUxmNcj1nSxi5QwT3TjORC2v8bi5Mroeqnbmk6p15cW1akC0oP+NZ4rG48+WFHRqsBaBusdSOVfA+IiZUqSd1ILysJ1w7aVN3EC7jLjDG43i+P/2BcEHy8TVClGOknJL341bHe3UPdEpmeu6k6aHGlDI4blUMXahCIUh0IdZuj+Vi/TxQME9+3bKIOjQb8RCNm3U3j/uz5gs9SyTjBuYIib9Scj/jDbLh0QKBgQDfLr3go3Q/AR0jb12QjGALJz1lc9ZRX2RQJkqqmYkZwOlHHyl+YJgqOZiO80fUkN0sJ29CmKecXU4gXuHir913Fdceei1ScBSsvZpWtBLhEZXKrRJYq8U0atKUFQADDMGutyB/uGCNeNwR6VcJezHPICvHxQfmWlWHA5VIOEtRPQKBgQD1fID76SkIpF/EaJMnN2alXWWnzKhUBUPGpQtbpwgSfaCBiZ4vr3NQwKBntOOB5QwHmifNZMoqaFQLzC4B/uyTNUcQMQQ6arYav7WQXqXTmW6poTsjUSuSOPx1swsHlYX09SmUwWDfd94XF9UOU0KUfA2/c85ixzNlV5ejkFA4hQKBgEvP3uQN4hD82d8Nl2TgqkdfnvV1cdnWY4buWvK0kOPUqelk5n1tZoMBaZc1gLLuOpMjGiIvJNByyXUpheWxA7POEXLi4b5dIEjFZ0YIiVk21gEw5UiFoMl7d+ihcY2Xqbslrb507SdhZLAY6V3pITRQo06K2XIgQWlJiE4uATepAoGBALZ2vEiBnYZW5vfN4tKbUyhGq3B1pggNgbr8odyV4mIcDlk6OOGov0WeZ5ut0AyUesSLyFnaOIoc0ZuTP/8rxBwG1bMrO8FP39sx83pDX25P9PkQZixyALjGsp+pXOFeOhtAvo9azO5M4j638Bydtjc3neBX62dwOLtyx7tDYN0hAoGAVLmr3w7XMVHTfEuCSzKHyRrOaN2PAuSX31QAji1PwlwVKMylVrb8rRvBOpTicA/wXPX9Q5O/yjegqhqLT/LXAm9ziFzy5b9/9SzXPukKebXXbvc0FOmcsrcxtijlPyUzf9fKM1ShiwqqsgM9eNyZ9GWUJw2GFATCWW7pl7rtnWk=' +} + +const testId = { + id: '122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a9', + privKey: 'CAASpgkwggSiAgEAAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAECggEAZtju/bcKvKFPz0mkHiaJcpycy9STKphorpCT83srBVQi59CdFU6Mj+aL/xt0kCPMVigJw8P3/YCEJ9J+rS8BsoWE+xWUEsJvtXoT7vzPHaAtM3ci1HZd302Mz1+GgS8Epdx+7F5p80XAFLDUnELzOzKftvWGZmWfSeDnslwVONkL/1VAzwKy7Ce6hk4SxRE7l2NE2OklSHOzCGU1f78ZzVYKSnS5Ag9YrGjOAmTOXDbKNKN/qIorAQ1bovzGoCwx3iGIatQKFOxyVCyO1PsJYT7JO+kZbhBWRRE+L7l+ppPER9bdLFxs1t5CrKc078h+wuUr05S1P1JjXk68pk3+kQKBgQDeK8AR11373Mzib6uzpjGzgNRMzdYNuExWjxyxAzz53NAR7zrPHvXvfIqjDScLJ4NcRO2TddhXAfZoOPVH5k4PJHKLBPKuXZpWlookCAyENY7+Pd55S8r+a+MusrMagYNljb5WbVTgN8cgdpim9lbbIFlpN6SZaVjLQL3J8TWH6wKBgQDSChzItkqWX11CNstJ9zJyUE20I7LrpyBJNgG1gtvz3ZMUQCn3PxxHtQzN9n1P0mSSYs+jBKPuoSyYLt1wwe10/lpgL4rkKWU3/m1Myt0tveJ9WcqHh6tzcAbb/fXpUFT/o4SWDimWkPkuCb+8j//2yiXk0a/T2f36zKMuZvujqQKBgC6B7BAQDG2H2B/ijofp12ejJU36nL98gAZyqOfpLJ+FeMz4TlBDQ+phIMhnHXA5UkdDapQ+zA3SrFk+6yGk9Vw4Hf46B+82SvOrSbmnMa+PYqKYIvUzR4gg34rL/7AhwnbEyD5hXq4dHwMNsIDq+l2elPjwm/U9V0gdAl2+r50HAoGALtsKqMvhv8HucAMBPrLikhXP/8um8mMKFMrzfqZ+otxfHzlhI0L08Bo3jQrb0Z7ByNY6M8epOmbCKADsbWcVre/AAY0ZkuSZK/CaOXNX/AhMKmKJh8qAOPRY02LIJRBCpfS4czEdnfUhYV/TYiFNnKRj57PPYZdTzUsxa/yVTmECgYBr7slQEjb5Onn5mZnGDh+72BxLNdgwBkhO0OCdpdISqk0F0Pxby22DFOKXZEpiyI9XYP1C8wPiJsShGm2yEwBPWXnrrZNWczaVuCbXHrZkWQogBDG3HGXNdU4MAWCyiYlyinIBpPpoAJZSzpGLmWbMWh28+RJS6AQX6KHrK1o2uw==', + pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2SKo/HMFZeBml1AF3XijzrxrfQXdJzjePBZAbdxqKR1Mc6juRHXij6HXYPjlAk01BhF1S3Ll4Lwi0cAHhggf457sMg55UWyeGKeUv0ucgvCpBwlR5cQ020i0MgzjPWOLWq1rtvSbNcAi2ZEVn6+Q2EcHo3wUvWRtLeKz+DZSZfw2PEDC+DGPJPl7f8g7zl56YymmmzH9liZLNrzg/qidokUv5u1pdGrcpLuPNeTODk0cqKB+OUbuKj9GShYECCEjaybJDl9276oalL9ghBtSeEv20kugatTvYy590wFlJkkvyl+nPxIH0EEYMKK9XRWlu9XYnoSfboiwcv8M3SlsjAgMBAAE=', + marshaled: '0a22122019318b6e5e0cf93a2314bf01269a2cc23cd3dcd452d742cdb9379d8646f6e4a912ab02080012a60230820122300d06092a864886f70d01010105000382010f003082010a0282010100b648aa3f1cc1597819a5d401775e28f3af1adf417749ce378f05901b771a8a47531cea3b911d78a3e875d83e3940934d41845d52dcb9782f08b47001e18207f8e7bb0c839e545b278629e52fd2e720bc2a41c25479710d36d22d0c8338cf58e2d6ab5aedbd26cd7008b6644567ebe43611c1e8df052f591b4b78acfe0d94997f0d8f1030be0c63c93e5edff20ef3979e98ca69a6cc7f658992cdaf383faa2768914bf9bb5a5d1ab7292ee3cd79338393472a281f8e51bb8a8fd1928581020848dac9b24397ddbbea86a52fd82106d49e12fdb492e81ab53bd8cb9f74c05949924bf297e9cfc481f410460c28af5745696ef57627a127dba22c1cbfc3374a5b2302030100011aab09080012a609308204a20201000282010100b648aa3f1cc1597819a5d401775e28f3af1adf417749ce378f05901b771a8a47531cea3b911d78a3e875d83e3940934d41845d52dcb9782f08b47001e18207f8e7bb0c839e545b278629e52fd2e720bc2a41c25479710d36d22d0c8338cf58e2d6ab5aedbd26cd7008b6644567ebe43611c1e8df052f591b4b78acfe0d94997f0d8f1030be0c63c93e5edff20ef3979e98ca69a6cc7f658992cdaf383faa2768914bf9bb5a5d1ab7292ee3cd79338393472a281f8e51bb8a8fd1928581020848dac9b24397ddbbea86a52fd82106d49e12fdb492e81ab53bd8cb9f74c05949924bf297e9cfc481f410460c28af5745696ef57627a127dba22c1cbfc3374a5b2302030100010282010066d8eefdb70abca14fcf49a41e2689729c9ccbd4932a9868ae9093f37b2b055422e7d09d154e8c8fe68bff1b749023cc562809c3c3f7fd808427d27ead2f01b28584fb159412c26fb57a13eefccf1da02d337722d4765ddf4d8ccf5f86812f04a5dc7eec5e69f345c014b0d49c42f33b329fb6f58666659f49e0e7b25c1538d90bff5540cf02b2ec27ba864e12c5113b976344d8e9254873b30865357fbf19cd560a4a74b9020f58ac68ce0264ce5c36ca34a37fa88a2b010d5ba2fcc6a02c31de21886ad40a14ec72542c8ed4fb09613ec93be9196e105645113e2fb97ea693c447d6dd2c5c6cd6de42aca734efc87ec2e52bd394b53f52635e4ebca64dfe9102818100de2bc011d75dfbdccce26fabb3a631b380d44ccdd60db84c568f1cb1033cf9dcd011ef3acf1ef5ef7c8aa30d270b27835c44ed9375d85701f66838f547e64e0f24728b04f2ae5d9a56968a24080c84358efe3dde794bcafe6be32eb2b31a8183658dbe566d54e037c7207698a6f656db20596937a4996958cb40bdc9f13587eb02818100d20a1cc8b64a965f5d4236cb49f73272504db423b2eba720493601b582dbf3dd93144029f73f1c47b50ccdf67d4fd2649262cfa304a3eea12c982edd70c1ed74fe5a602f8ae4296537fe6d4ccadd2dbde27d59ca8787ab737006dbfdf5e95054ffa384960e299690f92e09bfbc8ffff6ca25e4d1afd3d9fdfacca32e66fba3a90281802e81ec10100c6d87d81fe28e87e9d767a3254dfa9cbf7c800672a8e7e92c9f8578ccf84e504343ea6120c8671d70395247436a943ecc0dd2ac593eeb21a4f55c381dfe3a07ef364af3ab49b9a731af8f62a29822f533478820df8acbffb021c276c4c83e615eae1d1f030db080eafa5d9e94f8f09bf53d57481d025dbeaf9d070281802edb0aa8cbe1bfc1ee7003013eb2e29215cfffcba6f2630a14caf37ea67ea2dc5f1f39612342f4f01a378d0adbd19ec1c8d63a33c7a93a66c22800ec6d6715adefc0018d1992e4992bf09a397357fc084c2a628987ca8038f458d362c8251042a5f4b873311d9df521615fd362214d9ca463e7b3cf619753cd4b316bfc954e610281806beec9501236f93a79f99999c60e1fbbd81c4b35d83006484ed0e09da5d212aa4d05d0fc5bcb6d8314e297644a62c88f5760fd42f303e226c4a11a6db213004f5979ebad9356733695b826d71eb664590a200431b71c65cd754e0c0160b28989728a7201a4fa68009652ce918b9966cc5a1dbcf91252e80417e8a1eb2b5a36bb' +} + +const testIdHex = testId.id +const testIdBytes = base16.decode(`f${testId.id}`) +const testIdDigest = Digest.decode(testIdBytes) +const testIdB58String = base58btc.encode(testIdBytes).substring(1) +const testIdB36String = base36.encode(testIdBytes) +const testIdCID = CID.createV1(LIBP2P_KEY_CODE, testIdDigest) +const testIdCIDString = testIdCID.toString() + +module.exports = (common) => { + describe('interface-peer-id compliance tests', () => { + /** @type {import('libp2p-interfaces/src/peer-id/types').PeerIdFactory} */ + let factory + + beforeEach(async () => { + factory = await common.setup() + }) + + afterEach(() => { + common.teardown && common.teardown() + }) + + it('create a new id', async () => { + const id = await factory.create(testOpts) + expect(id.toB58String().length).to.equal(46) + }) + + it('can be created for a Secp256k1 key', async () => { + const id = await factory.create({ keyType: 'secp256k1', bits: 256 }) + const expB58 = base58btc.encode((await identity.digest(id.pubKey.bytes)).bytes).slice(1) + expect(id.toB58String()).to.equal(expB58) + }) + + it('can get the public key from a Secp256k1 key', async () => { + const original = await factory.create({ keyType: 'secp256k1', bits: 256 }) + const newId = factory.createFromB58String(original.toB58String()) + expect(original.pubKey.bytes).to.eql(newId.pubKey.bytes) + }) + + it('isPeerId', async () => { + const id = await factory.create(testOpts) + expect(factory.isPeerId(id)).to.equal(true) + expect(factory.isPeerId('aaa')).to.equal(false) + expect(factory.isPeerId(uint8ArrayFromString('batatas'))).to.equal(false) + }) + + it('throws on changing the id', async () => { + const id = await factory.create(testOpts) + expect(id.toB58String().length).to.equal(46) + expect(() => { + // @ts-ignore + id.id = uint8ArrayFromString('hello') + }).to.throw(/immutable/) + }) + + it('recreate from Hex string', () => { + const id = factory.createFromHexString(testIdHex) + expect(testIdBytes).to.deep.equal(id.toBytes()) + }) + + it('recreate from a Uint8Array', () => { + const id = factory.createFromBytes(testIdBytes) + expect(testId.id).to.equal(id.toHexString()) + expect(testIdBytes).to.deep.equal(id.toBytes()) + }) + + it('recreate from a B58 String', () => { + const id = factory.createFromB58String(testIdB58String) + expect(testIdB58String).to.equal(id.toB58String()) + expect(testIdBytes).to.deep.equal(id.toBytes()) + }) + + it('recreate from CID object', () => { + const id = factory.createFromCID(testIdCID) + expect(testIdCIDString).to.equal(id.toString()) + expect(testIdBytes).to.deep.equal(id.toBytes()) + }) + + it('recreate from Base58 String (CIDv0)', () => { + const id = factory.createFromCID(CID.parse(testIdB58String)) + expect(testIdCIDString).to.equal(id.toString()) + expect(testIdBytes).to.deep.equal(id.toBytes()) + }) + + it('recreate from Base36 String', () => { + const id = factory.parse(testIdB36String) + expect(testIdCIDString).to.equal(id.toString()) + expect(testIdBytes).to.deep.equal(id.toBytes()) + }) + + it('recreate from CIDv1 Base32 (libp2p-key multicodec)', () => { + const cid = CID.createV1(LIBP2P_KEY_CODE, testIdDigest) + const id = factory.createFromCID(cid) + expect(cid.toString()).to.equal(id.toString()) + expect(testIdBytes).to.deep.equal(id.toBytes()) + }) + + it('recreate from CIDv1 Base32 (dag-pb multicodec)', () => { + const cid = CID.createV1(DAG_PB_CODE, testIdDigest) + const id = factory.createFromCID(cid) + // toString should return CID with multicodec set to libp2p-key + expect(CID.parse(id.toString()).code).to.equal(LIBP2P_KEY_CODE) + expect(testIdBytes).to.deep.equal(id.toBytes()) + }) + + it('recreate from CID Uint8Array', () => { + const id = factory.createFromBytes(testIdCID.bytes) + expect(testIdCIDString).to.equal(id.toString()) + expect(testIdBytes).to.deep.equal(id.toBytes()) + }) + + it('throws on invalid CID multicodec', () => { + // only libp2p and dag-pb are supported + const invalidCID = CID.createV1(RAW_CODE, testIdDigest) + expect(() => { + factory.createFromCID(invalidCID) + }).to.throw(/invalid/i) + }) + + it('throws on invalid multihash value', () => { + // using function code 0x50 that does not represent valid hash function + // https://github.com/multiformats/js-multihash/blob/b85999d5768bf06f1b0f16b926ef2cb6d9c14265/src/constants.js#L345 + const invalidMultihash = uint8ArrayToString(Uint8Array.from([0x50, 0x1, 0x0]), 'base58btc') + expect(() => { + factory.createFromB58String(invalidMultihash) + }).to.throw(/invalid/i) + }) + + it('throws on invalid CID object', () => { + const invalidCID = {} + expect(() => { + // @ts-expect-error invalid cid is invalid type + factory.createFromCID(invalidCID) + }).to.throw(/invalid/i) + }) + + it('recreate from a Public Key', async () => { + const id = await factory.createFromPubKey(testId.pubKey) + expect(testIdB58String).to.equal(id.toB58String()) + expect(testIdBytes).to.deep.equal(id.toBytes()) + }) + + it('recreate from a Private Key', async () => { + const id = await factory.createFromPrivKey(testId.privKey) + expect(testIdB58String).to.equal(id.toB58String()) + const encoded = uint8ArrayFromString(testId.privKey, 'base64pad') + const id2 = await factory.createFromPrivKey(encoded) + expect(testIdB58String).to.equal(id2.toB58String()) + expect(id.marshalPubKey()).to.deep.equal(id2.marshalPubKey()) + }) + + it('recreate from Protobuf', async () => { + const id = await factory.createFromProtobuf(testId.marshaled) + expect(testIdB58String).to.equal(id.toB58String()) + const encoded = uint8ArrayFromString(testId.privKey, 'base64pad') + const id2 = await factory.createFromPrivKey(encoded) + expect(testIdB58String).to.equal(id2.toB58String()) + expect(id.marshalPubKey()).to.deep.equal(id2.marshalPubKey()) + expect(uint8ArrayToString(id.marshal(), 'base16')).to.deep.equal(testId.marshaled) + }) + + it('recreate from embedded ed25519 key', async () => { + const key = '12D3KooWRm8J3iL796zPFi2EtGGtUJn58AG67gcqzMFHZnnsTzqD' + const id = await factory.parse(key) + expect(id.toB58String()).to.equal(key) + const expB58 = base58btc.encode((await identity.digest(id.pubKey.bytes)).bytes).slice(1) + expect(id.toB58String()).to.equal(expB58) + }) + + it('recreate from embedded secp256k1 key', async () => { + const key = '16Uiu2HAm5qw8UyXP2RLxQUx5KvtSN8DsTKz8quRGqGNC3SYiaB8E' + const id = await factory.parse(key) + expect(id.toB58String()).to.equal(key) + const expB58 = base58btc.encode((await identity.digest(id.pubKey.bytes)).bytes).slice(1) + expect(id.toB58String()).to.equal(expB58) + }) + + it('recreate from string key', async () => { + const key = 'QmRsooYQasV5f5r834NSpdUtmejdQcpxXkK6qsozZWEihC' + const id = await factory.parse(key) + expect(id.toB58String()).to.equal(key) + }) + + it('can be created from a Secp256k1 public key', async () => { + const privKey = await crypto.keys.generateKeyPair('secp256k1', 256) + const id = await factory.createFromPubKey(privKey.public.bytes) + const expB58 = base58btc.encode((await identity.digest(id.pubKey.bytes)).bytes).slice(1) + expect(id.toB58String()).to.equal(expB58) + }) + + it('can be created from a Secp256k1 private key', async () => { + const privKey = await crypto.keys.generateKeyPair('secp256k1', 256) + const id = await factory.createFromPrivKey(privKey.bytes) + const expB58 = base58btc.encode((await identity.digest(id.pubKey.bytes)).bytes).slice(1) + expect(id.toB58String()).to.equal(expB58) + }) + + it('Compare generated ID with one created from PubKey', async () => { + const id1 = await factory.create(testOpts) + const id2 = await factory.createFromPubKey(id1.marshalPubKey()) + expect(id1.id).to.be.eql(id2.id) + }) + + it('Works with default options', async function () { + this.timeout(10000) + const id = await factory.create() + expect(id.toB58String().length).to.equal(46) + }) + + it('Non-default # of bits', async function () { + this.timeout(1000 * 60) + const shortId = await factory.create(testOpts) + const longId = await factory.create({ bits: 1024 }) + expect(shortId.privKey.bytes.length).is.below(longId.privKey.bytes.length) + }) + + it('Pretty printing', async () => { + const id1 = await factory.create(testOpts) + const json = id1.toJSON() + const id2 = await factory.createFromPrivKey(json.privKey || 'invalid, should not happen') + expect(id1.toPrint()).to.be.eql(id2.toPrint()) + expect(id1.toPrint()).to.equal('') + }) + + it('toBytes', () => { + const id = factory.createFromHexString(testIdHex) + expect(uint8ArrayToString(id.toBytes(), 'base16')).to.equal(uint8ArrayToString(testIdBytes, 'base16')) + }) + + it('isEqual', async () => { + const ids = await Promise.all([ + factory.create(testOpts), + factory.create(testOpts) + ]) + + expect(ids[0].isEqual(ids[0])).to.equal(true) + expect(ids[0].isEqual(ids[1])).to.equal(false) + expect(ids[0].isEqual(ids[0].id)).to.equal(true) + expect(ids[0].isEqual(ids[1].id)).to.equal(false) + }) + + it('equals', async () => { + const ids = await Promise.all([ + factory.create(testOpts), + factory.create(testOpts) + ]) + + expect(ids[0].equals(ids[0])).to.equal(true) + expect(ids[0].equals(ids[1])).to.equal(false) + expect(ids[0].equals(ids[0].id)).to.equal(true) + expect(ids[0].equals(ids[1].id)).to.equal(false) + }) + + describe('hasInlinePublicKey', () => { + it('returns true if uses a key type with inline public key', async () => { + const peerId = await factory.create({ keyType: 'secp256k1' }) + expect(peerId.hasInlinePublicKey()).to.equal(true) + }) + + it('returns false if uses a key type with no inline public key', async () => { + const peerId = await factory.create({ keyType: 'RSA' }) + expect(peerId.hasInlinePublicKey()).to.equal(false) + }) + }) + + describe('fromJSON', () => { + it('full node', async () => { + const id = await factory.create(testOpts) + const other = await factory.createFromJSON(id.toJSON()) + expect(id.toB58String()).to.equal(other.toB58String()) + expect(id.privKey.bytes).to.eql(other.privKey.bytes) + expect(id.pubKey.bytes).to.eql(other.pubKey.bytes) + }) + + it('only id', async () => { + const key = await crypto.keys.generateKeyPair('RSA', 1024) + const digest = await key.public.hash() + const id = factory.createFromBytes(digest) + expect(id.privKey).to.not.exist() + expect(id.pubKey).to.not.exist() + const other = await factory.createFromJSON(id.toJSON()) + expect(id.toB58String()).to.equal(other.toB58String()) + }) + + it('go interop', async () => { + const id = await factory.createFromJSON(goId) + const digest = await id.privKey.public.hash() + expect(base58btc.encode(digest).slice(1)).to.eql(goId.id) + }) + }) + + it('set privKey (valid)', async () => { + const peerId = await factory.create(testOpts) + // @ts-ignore + peerId.privKey = peerId._privKey + expect(peerId.isValid()).to.equal(true) + }) + + it('set pubKey (valid)', async () => { + const peerId = await factory.create(testOpts) + // @ts-ignore + peerId.pubKey = peerId._pubKey + expect(peerId.isValid()).to.equal(true) + }) + + it('set privKey (invalid)', async () => { + const peerId = await factory.create(testOpts) + // @ts-ignore + peerId.privKey = uint8ArrayFromString('bufff') + expect(peerId.isValid()).to.equal(false) + }) + + it('set pubKey (invalid)', async () => { + const peerId = await factory.create(testOpts) + // @ts-ignore + peerId.pubKey = uint8ArrayFromString('bufff') + expect(peerId.isValid()).to.equal(false) + }) + + it('keys are equal after one is stringified', async () => { + const peerId = await factory.create(testOpts) + const peerId1 = factory.createFromB58String(peerId.toB58String()) + const peerId2 = factory.createFromB58String(peerId.toB58String()) + + expect(peerId1).to.deep.equal(peerId2) + + peerId1.toString() + + expect(peerId1).to.deep.equal(peerId2) + }) + + describe('throws on inconsistent data', () => { + let k1 + let k2 + let k3 + + before(async () => { + const keys = await Promise.all([ + crypto.keys.generateKeyPair('RSA', 512), + crypto.keys.generateKeyPair('RSA', 512), + crypto.keys.generateKeyPair('RSA', 512) + ]) + + k1 = keys[0] + k2 = keys[1] + k3 = keys[2] + }) + + it('missmatch private - public key', async () => { + const digest = await k1.public.hash() + expect(() => { + factory.createFromJSON({ + id: digest, + pubKey: k1, + privKey: k2.public + }) // eslint-disable-line no-new + }).to.throw(/inconsistent arguments/) + }) + + it('missmatch id - private - public key', async () => { + const digest = await k1.public.hash() + expect(() => { + factory.createFromJSON({ + id: digest, + pubKey: k1, + privKey: k3.public + }) // eslint-disable-line no-new + }).to.throw(/inconsistent arguments/) + }) + + it('invalid id', () => { + // @ts-expect-error incorrect constructor arg type + expect(() => factory.createFromJSON('hello world')).to.throw(/invalid id/) + }) + }) + }) +} diff --git a/packages/interfaces/package.json b/packages/interfaces/package.json index 3a357e28a..71d62aebe 100644 --- a/packages/interfaces/package.json +++ b/packages/interfaces/package.json @@ -59,6 +59,7 @@ "dependencies": { "abort-controller": "^3.0.0", "abortable-iterator": "^3.0.0", + "cids": "^1.1.9", "debug": "^4.3.1", "err-code": "^3.0.1", "it-length-prefixed": "^5.0.2", diff --git a/packages/interfaces/src/crypto/types.d.ts b/packages/interfaces/src/crypto/types.d.ts index c094edfd8..0815d0d04 100644 --- a/packages/interfaces/src/crypto/types.d.ts +++ b/packages/interfaces/src/crypto/types.d.ts @@ -1,4 +1,4 @@ -import PeerId from 'peer-id' +import { PeerId } from '../peer-id/types' import { MultiaddrConnection } from '../transport/types' /** @@ -22,35 +22,3 @@ export type SecureOutbound = { remoteEarlyData: Buffer; remotePeer: PeerId; } - -export interface PublicKey { - readonly bytes: Uint8Array - verify: (data: Uint8Array, sig: Uint8Array) => Promise - marshal: () => Uint8Array - equals: (key: PublicKey) => boolean - hash: () => Promise -} - -/** - * Generic private key interface - */ -export interface PrivateKey { - readonly public: PublicKey - readonly bytes: Uint8Array - sign: (data: Uint8Array) => Promise - marshal: () => Uint8Array - equals: (key: PrivateKey) => boolean - hash: () => Promise - /** - * Gets the ID of the key. - * - * The key id is the base58 encoding of the SHA-256 multihash of its public key. - * The public key is a protobuf encoding containing a type and the DER encoding - * of the PKCS SubjectPublicKeyInfo. - */ - id: () => Promise - /** - * Exports the password protected key in the format specified. - */ - export: (password: string, format?: 'pkcs-8' | string) => Promise -} diff --git a/packages/interfaces/src/keys/README.md b/packages/interfaces/src/keys/README.md new file mode 100644 index 000000000..c158729d7 --- /dev/null +++ b/packages/interfaces/src/keys/README.md @@ -0,0 +1,25 @@ +# interface-keys + +> Interfaces for libp2p keys + +## Table of Contents +- [Using the Test Suite](#using-the-test-suite) + +## Using the Test Suite + +You can also check out the [internal test suite](../../test/crypto/compliance.spec.js) to see the setup in action. + +```js +const tests = require('libp2p-interfaces-compliance-tests/src/keys') +const yourKeys = require('./your-keys') + +tests({ + setup () { + // Set up your keys if needed, then return it + return yourKeys + }, + teardown () { + // Clean up your keys if needed + } +}) +``` diff --git a/packages/interfaces/src/keys/types.d.ts b/packages/interfaces/src/keys/types.d.ts new file mode 100644 index 000000000..fbb9a3039 --- /dev/null +++ b/packages/interfaces/src/keys/types.d.ts @@ -0,0 +1,34 @@ + +export interface PublicKey { + readonly bytes: Uint8Array + verify: (data: Uint8Array, sig: Uint8Array) => Promise + marshal: () => Uint8Array + equals: (key: PublicKey) => boolean + hash: () => Promise +} + +/** + * Generic private key interface + */ +export interface PrivateKey { + readonly public: PublicKey + readonly bytes: Uint8Array + sign: (data: Uint8Array) => Promise + marshal: () => Uint8Array + equals: (key: PrivateKey) => boolean + hash: () => Promise + /** + * Gets the ID of the key. + * + * The key id is the base58 encoding of the SHA-256 multihash of its public key. + * The public key is a protobuf encoding containing a type and the DER encoding + * of the PKCS SubjectPublicKeyInfo. + */ + id: () => Promise + /** + * Exports the password protected key in the format specified. + */ + export: (password: string, format?: 'pkcs-8' | string) => Promise +} + +export type KeyType = 'Ed25519' | 'RSA' | 'secp256k1' diff --git a/packages/interfaces/src/peer-id/README.md b/packages/interfaces/src/peer-id/README.md new file mode 100644 index 000000000..f7a342387 --- /dev/null +++ b/packages/interfaces/src/peer-id/README.md @@ -0,0 +1,44 @@ +interface-peer-id +======================== + +> A test suite and interface you can use to implement a PeerId module for libp2p. + +The primary goal of this module is to enable developers to implement PeerId modules. This module and test suite was heavily inspired by earlier implementation of [PeerId](https://github.com/libp2p/js-peer-id). + +Publishing a test suite as a module lets multiple modules all ensure compatibility since they use the same test suite. + +The API is presented with both Node.js and Go primitives, however, there is not actual limitations for it to be extended for any other language, pushing forward the cross compatibility and interop through different stacks. + +## Modules that implement the interface + +- [JavaScript libp2p-peer-id](https://github.com/libp2p/js-peer-id) + +Send a PR to add a new one if you happen to find or write one. + +## Badge + +Include this badge in your readme if you make a new module that uses interface-peer-id API. + +![](/img/badge.png) + +## Usage + +### Node.js + +Install `libp2p-interfaces-compliance-tests` as one of the development dependencies of your project and as a test file. Then, using `mocha` (for JavaScript) or a test runner with compatible API, do: + +```js +const tests = require('libp2p-interfaces-compliance-tests/src/peer-id') + +describe('your peer id', () => { + // use all of the test suits + tests({ + setup () { + return YourPeerIdFactory + }, + teardown () { + // Clean up any resources created by setup() + } + }) +}) +``` diff --git a/packages/interfaces/src/peer-id/types.d.ts b/packages/interfaces/src/peer-id/types.d.ts new file mode 100644 index 000000000..d469c0eb4 --- /dev/null +++ b/packages/interfaces/src/peer-id/types.d.ts @@ -0,0 +1,137 @@ +import type { CID } from 'multiformats/cid' +import type { PublicKey, PrivateKey, KeyType } from '../keys/types' + +interface PeerIdJSON { + readonly id: string; + readonly pubKey?: string; + readonly privKey?: string; +} + +interface CreateOptions { + bits?: number; + keyType?: KeyType; +} + +export interface PeerId { + readonly id: Uint8Array; + privKey: PrivateKey | undefined; + pubKey: PublicKey | undefined; + + /** + * Return the protobuf version of the public key, matching go ipfs formatting + */ + marshalPubKey ():Uint8Array | undefined; + + /** + * Return the protobuf version of the private key, matching go ipfs formatting + */ + marshalPrivKey (): Uint8Array | undefined; + + /** + * Return the protobuf version of the peer-id + */ + marshal (excludePriv?: boolean): Uint8Array; + + /** + * String representation + */ + toPrint (): string; + + /** + * The jsonified version of the key, matching the formatting of go-ipfs for its config file + */ + toJSON (): PeerIdJSON; + + /** + * Encode to hex. + */ + toHexString ():string; + + /** + * Return raw id bytes + */ + toBytes () : Uint8Array; + + /** + * Encode to base58 string. + */ + toB58String (): string; + + /** + * Self-describing String representation + * in default format from RFC 0001: https://github.com/libp2p/specs/pull/209 + */ + toString ():string; + + /** + * Checks the equality of `this` peer against a given PeerId. + */ + equals (id: Uint8Array|PeerId): boolean | never; + + /** + * Check if this PeerId instance is valid (privKey -> pubKey -> Id) + */ + isValid (): boolean; + + /** + * Check if the PeerId has an inline public key. + */ + hasInlinePublicKey (): boolean; +} + +export interface PeerIdFactory { + /** + * Create a new PeerId. + **/ + create (args: CreateOptions): Promise; + + /** + * Create PeerId from raw bytes. + */ + createFromBytes (buf: Uint8Array): PeerId; + + /** + * Create PeerId from base58-encoded string. + */ + createFromB58String (str: string): PeerId; + + /** + * Create PeerId from hex string. + */ + createFromHexString (str: string): PeerId; + + /** + * Create PeerId from CID. + */ + createFromCID (cid: CID | Uint8Array | string): PeerId + + /** + * Create PeerId from public key. + */ + createFromPubKey (key: Uint8Array | string): Promise; + + /** + * Create PeerId from private key. + */ + createFromPrivKey (key: Uint8Array | string): Promise; + + /** + * Create PeerId from PeerId JSON formatted object. + */ + createFromJSON (obj: PeerIdJSON): Promise; + + /** + * Create PeerId from Protobuf bytes. + */ + createFromProtobuf (buf: Uint8Array | string): Promise; + + /** + * Parse PeerId from string, maybe base58btc encoded without multibase prefix + */ + parse (str: string): PeerId + + /** + * Checks if a value is an instance of PeerId. + */ + isPeerId (peerId:unknown): boolean; +} diff --git a/packages/interfaces/src/value-store/types.d.ts b/packages/interfaces/src/value-store/types.d.ts index e3aa37aff..ed89ac6a1 100644 --- a/packages/interfaces/src/value-store/types.d.ts +++ b/packages/interfaces/src/value-store/types.d.ts @@ -1,4 +1,4 @@ -import type PeerId from 'peer-id' +import type { PeerId } from '../peer-id/types' export interface GetValueResult { from: PeerId,