Merge pull request #173 from ChainSafe/cayman/secp-migration

Integrate libp2p-crypto-secp256k1
This commit is contained in:
Jacob Heun 2020-04-07 15:55:14 +02:00 committed by GitHub
commit ccda21fe91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 475 additions and 62 deletions

View File

@ -171,9 +171,7 @@ main()
The [`generateKeyPair`](#generatekeypairtype-bits), [`marshalPublicKey`](#marshalpublickeykey-type), and [`marshalPrivateKey`](#marshalprivatekeykey-type) functions accept a string `type` argument. The [`generateKeyPair`](#generatekeypairtype-bits), [`marshalPublicKey`](#marshalpublickeykey-type), and [`marshalPrivateKey`](#marshalprivatekeykey-type) functions accept a string `type` argument.
Currently the `'RSA'` and `'ed25519'` types are supported, although ed25519 keys support only signing and verification of messages. For encryption / decryption support, RSA keys should be used. Currently the `'RSA'`, `'ed25519'`, and `secp256k1` types are supported, although ed25519 and secp256k1 keys support only signing and verification of messages. For encryption / decryption support, RSA keys should be used.
Installing the [libp2p-crypto-secp256k1](https://github.com/libp2p/js-libp2p-crypto-secp256k1) module adds support for the `'secp256k1'` type, which supports ECDSA signatures using the secp256k1 elliptic curve popularized by Bitcoin. This module is not installed by default, and should be explicitly depended on if your project requires secp256k1 support.
### `crypto.keys.generateKeyPair(type, bits)` ### `crypto.keys.generateKeyPair(type, bits)`
@ -232,7 +230,7 @@ Resolves to an object of the form:
### `crypto.keys.marshalPublicKey(key, [type])` ### `crypto.keys.marshalPublicKey(key, [type])`
- `key: keys.rsa.RsaPublicKey | keys.ed25519.Ed25519PublicKey | require('libp2p-crypto-secp256k1').Secp256k1PublicKey` - `key: keys.rsa.RsaPublicKey | keys.ed25519.Ed25519PublicKey | keys.secp256k1.Secp256k1PublicKey`
- `type: String`, see [Supported Key Types](#supported-key-types) above. Defaults to 'rsa'. - `type: String`, see [Supported Key Types](#supported-key-types) above. Defaults to 'rsa'.
Returns `Buffer` Returns `Buffer`
@ -249,7 +247,7 @@ Converts a protobuf serialized public key into its representative object.
### `crypto.keys.marshalPrivateKey(key, [type])` ### `crypto.keys.marshalPrivateKey(key, [type])`
- `key: keys.rsa.RsaPrivateKey | keys.ed25519.Ed25519PrivateKey | require('libp2p-crypto-secp256k1').Secp256k1PrivateKey` - `key: keys.rsa.RsaPrivateKey | keys.ed25519.Ed25519PrivateKey | keys.secp256k1.Secp256k1PrivateKey`
- `type: String`, see [Supported Key Types](#supported-key-types) above. - `type: String`, see [Supported Key Types](#supported-key-types) above.
Returns `Buffer` Returns `Buffer`

View File

@ -33,20 +33,22 @@
"IPFS", "IPFS",
"libp2p", "libp2p",
"crypto", "crypto",
"rsa" "rsa",
"secp256k1"
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"buffer": "^5.5.0", "buffer": "^5.5.0",
"err-code": "^2.0.0", "err-code": "^2.0.0",
"is-typedarray": "^1.0.0",
"iso-random-stream": "^1.1.0", "iso-random-stream": "^1.1.0",
"keypair": "^1.0.1", "keypair": "^1.0.1",
"libp2p-crypto-secp256k1": "^0.4.2",
"multibase": "^0.7.0", "multibase": "^0.7.0",
"multihashing-async": "^0.8.1", "multihashing-async": "^0.8.1",
"node-forge": "~0.9.1", "node-forge": "~0.9.1",
"pem-jwk": "^2.0.0", "pem-jwk": "^2.0.0",
"protons": "^1.0.1", "protons": "^1.0.1",
"secp256k1": "^4.0.0",
"ursa-optional": "~0.10.1" "ursa-optional": "~0.10.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -13,7 +13,7 @@ exports = module.exports
const supportedKeys = { const supportedKeys = {
rsa: require('./rsa-class'), rsa: require('./rsa-class'),
ed25519: require('./ed25519-class'), ed25519: require('./ed25519-class'),
secp256k1: require('libp2p-crypto-secp256k1')(keysPBM, require('../random-bytes')) secp256k1: require('./secp256k1-class')(keysPBM, require('../random-bytes'))
} }
exports.supportedKeys = supportedKeys exports.supportedKeys = supportedKeys

110
src/keys/secp256k1-class.js Normal file
View File

@ -0,0 +1,110 @@
'use strict'
const multibase = require('multibase')
const sha = require('multihashing-async/src/sha')
module.exports = (keysProtobuf, randomBytes, crypto) => {
crypto = crypto || require('./secp256k1')(randomBytes)
class Secp256k1PublicKey {
constructor (key) {
crypto.validatePublicKey(key)
this._key = key
}
verify (data, sig) {
return crypto.hashAndVerify(this._key, sig, data)
}
marshal () {
return crypto.compressPublicKey(this._key)
}
get bytes () {
return keysProtobuf.PublicKey.encode({
Type: keysProtobuf.KeyType.Secp256k1,
Data: this.marshal()
})
}
equals (key) {
return this.bytes.equals(key.bytes)
}
hash () {
return sha.multihashing(this.bytes, 'sha2-256')
}
}
class Secp256k1PrivateKey {
constructor (key, publicKey) {
this._key = key
this._publicKey = publicKey || crypto.computePublicKey(key)
crypto.validatePrivateKey(this._key)
crypto.validatePublicKey(this._publicKey)
}
sign (message) {
return crypto.hashAndSign(this._key, message)
}
get public () {
return new Secp256k1PublicKey(this._publicKey)
}
marshal () {
return this._key
}
get bytes () {
return keysProtobuf.PrivateKey.encode({
Type: keysProtobuf.KeyType.Secp256k1,
Data: this.marshal()
})
}
equals (key) {
return this.bytes.equals(key.bytes)
}
hash () {
return sha.multihashing(this.bytes, 'sha2-256')
}
/**
* 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.
*
* @param {function(Error, id)} callback
* @returns {undefined}
*/
async id () {
const hash = await this.public.hash()
return multibase.encode('base58btc', hash).toString().slice(1)
}
}
function unmarshalSecp256k1PrivateKey (bytes) {
return new Secp256k1PrivateKey(bytes)
}
function unmarshalSecp256k1PublicKey (bytes) {
return new Secp256k1PublicKey(bytes)
}
async function generateKeyPair () {
const privateKeyBytes = await crypto.generateKey()
return new Secp256k1PrivateKey(privateKeyBytes)
}
return {
Secp256k1PublicKey,
Secp256k1PrivateKey,
unmarshalSecp256k1PrivateKey,
unmarshalSecp256k1PublicKey,
generateKeyPair
}
}

86
src/keys/secp256k1.js Normal file
View File

@ -0,0 +1,86 @@
'use strict'
const { Buffer } = require('buffer')
var isTypedArray = require('is-typedarray').strict
const secp256k1 = require('secp256k1')
const sha = require('multihashing-async/src/sha')
const HASH_ALGORITHM = 'sha2-256'
function typedArrayTobuffer (arr) {
if (isTypedArray(arr)) {
// To avoid a copy, use the typed array's underlying ArrayBuffer to back new Buffer
var buf = Buffer.from(arr.buffer)
if (arr.byteLength !== arr.buffer.byteLength) {
// Respect the "view", i.e. byteOffset and byteLength, without doing a copy
buf = buf.slice(arr.byteOffset, arr.byteOffset + arr.byteLength)
}
return buf
} else {
// Pass through all other types to `Buffer.from`
return Buffer.from(arr)
}
}
module.exports = (randomBytes) => {
const privateKeyLength = 32
function generateKey () {
let privateKey
do {
privateKey = randomBytes(32)
} while (!secp256k1.privateKeyVerify(privateKey))
return privateKey
}
async function hashAndSign (key, msg) {
const digest = await sha.digest(msg, HASH_ALGORITHM)
const sig = secp256k1.ecdsaSign(digest, key)
return typedArrayTobuffer(secp256k1.signatureExport(sig.signature))
}
async function hashAndVerify (key, sig, msg) {
const digest = await sha.digest(msg, HASH_ALGORITHM)
sig = typedArrayTobuffer(secp256k1.signatureImport(sig))
return secp256k1.ecdsaVerify(sig, digest, key)
}
function compressPublicKey (key) {
if (!secp256k1.publicKeyVerify(key)) {
throw new Error('Invalid public key')
}
return typedArrayTobuffer(secp256k1.publicKeyConvert(key, true))
}
function decompressPublicKey (key) {
return typedArrayTobuffer(secp256k1.publicKeyConvert(key, false))
}
function validatePrivateKey (key) {
if (!secp256k1.privateKeyVerify(key)) {
throw new Error('Invalid private key')
}
}
function validatePublicKey (key) {
if (!secp256k1.publicKeyVerify(key)) {
throw new Error('Invalid public key')
}
}
function computePublicKey (privateKey) {
validatePrivateKey(privateKey)
return typedArrayTobuffer(secp256k1.publicKeyCreate(privateKey))
}
return {
generateKey,
privateKeyLength,
hashAndSign,
hashAndVerify,
compressPublicKey,
decompressPublicKey,
validatePrivateKey,
validatePublicKey,
computePublicKey
}
}

31
test/fixtures/go-key-secp256k1.js vendored Normal file
View File

@ -0,0 +1,31 @@
'use strict'
const { Buffer } = require('buffer')
// The keypair and signature below were generated in a gore repl session (https://github.com/motemen/gore)
// using the secp256k1 fork of go-libp2p-crypto by github user @vyzo
//
// gore> :import github.com/vyzo/go-libp2p-crypto
// gore> :import crypto/rand
// gore> :import io/ioutil
// gore> priv, pub, err := crypto.GenerateKeyPairWithReader(crypto.Secp256k1, 256, rand.Reader)
// gore> privBytes, err := priv.Bytes()
// gore> pubBytes, err := pub.Bytes()
// gore> msg := []byte("hello! and welcome to some awesome crypto primitives")
// gore> sig, err := priv.Sign(msg)
// gore> ioutil.WriteFile("/tmp/secp-go-priv.bin", privBytes, 0644)
// gore> ioutil.WriteFile("/tmp/secp-go-pub.bin", pubBytes, 0644)
// gore> ioutil.WriteFile("/tmp/secp-go-sig.bin", sig, 0644)
//
// The generated files were then read in a node repl with e.g.:
// > fs.readFileSync('/tmp/secp-go-pub.bin').toString('hex')
// '08021221029c0ce5d53646ed47112560297a3e59b78b8cbd4bae37c7a0c236eeb91d0fbeaf'
//
// and the results copy/pasted in here
module.exports = {
privateKey: Buffer.from('08021220358f15db8c2014d570e8e3a622454e2273975a3cca443ec0c45375b13d381d18', 'hex'),
publicKey: Buffer.from('08021221029c0ce5d53646ed47112560297a3e59b78b8cbd4bae37c7a0c236eeb91d0fbeaf', 'hex'),
message: Buffer.from('hello! and welcome to some awesome crypto primitives', 'utf-8'),
signature: Buffer.from('304402200e4c629e9f5d99439115e60989cd40087f6978c36078b0b50cf3d30af5c38d4102204110342c8e7f0809897c1c7a66e49e1c6b7cb0a6ed6993640ec2fe742c1899a9', 'hex')
}

View File

@ -1,81 +1,267 @@
/* eslint-env mocha */ /* eslint-env mocha */
'use strict' 'use strict'
const { Buffer } = require('buffer')
const chai = require('chai') const chai = require('chai')
const dirtyChai = require('dirty-chai') const dirtyChai = require('dirty-chai')
const expect = chai.expect const expect = chai.expect
chai.use(dirtyChai) chai.use(dirtyChai)
const sinon = require('sinon')
const fixtures = require('../fixtures/secp256k1')
const crypto = require('../../src') const crypto = require('../../src')
const secp256k1 = crypto.keys.supportedKeys.secp256k1
const keysPBM = crypto.keys.keysPBM
const randomBytes = crypto.randomBytes
const secp256k1Crypto = require('../../src/keys/secp256k1')(randomBytes)
describe('without libp2p-crypto-secp256k1 module present', () => { describe('secp256k1 keys', () => {
before(() => { let key
const empty = null
sinon.replace(crypto.keys.supportedKeys, 'secp256k1', empty) before(async () => {
key = await secp256k1.generateKeyPair()
}) })
after(() => {
sinon.restore()
})
it('fails to generate a secp256k1 key', async () => {
try {
await crypto.keys.generateKeyPair('secp256k1', 256)
} catch (err) {
return // expected
}
throw new Error('Expected error to be thrown')
})
it('fails to unmarshal a secp256k1 private key', async () => {
try {
await crypto.keys.unmarshalPrivateKey(fixtures.pbmPrivateKey)
} catch (err) {
return // expected
}
throw new Error('Expected error to be thrown')
})
it('fails to unmarshal a secp256k1 public key', () => {
expect(() => {
crypto.keys.unmarshalPublicKey(fixtures.pbmPublicKey)
}).to.throw(Error)
})
})
describe('with libp2p-crypto-secp256k1 module present', () => {
it('generates a valid key', async () => { it('generates a valid key', async () => {
const key = await crypto.keys.generateKeyPair('secp256k1', 256) expect(key).to.be.an.instanceof(secp256k1.Secp256k1PrivateKey)
expect(key).to.exist() expect(key.public).to.be.an.instanceof(secp256k1.Secp256k1PublicKey)
const digest = await key.hash()
expect(digest).to.have.length(34)
const publicDigest = await key.public.hash()
expect(publicDigest).to.have.length(34)
}) })
it('protobuf encoding', async () => { it('optionally accepts a `bits` argument when generating a key', async () => {
const key = await crypto.keys.generateKeyPair('secp256k1', 256) const _key = await secp256k1.generateKeyPair(256)
expect(key).to.exist() expect(_key).to.be.an.instanceof(secp256k1.Secp256k1PrivateKey)
})
const keyMarshal = crypto.keys.marshalPrivateKey(key) it('signs', async () => {
const key2 = await crypto.keys.unmarshalPrivateKey(keyMarshal) const text = randomBytes(512)
const keyMarshal2 = crypto.keys.marshalPrivateKey(key2) const sig = await key.sign(text)
const res = await key.public.verify(text, sig)
expect(res).to.equal(true)
})
it('encoding', async () => {
const keyMarshal = key.marshal()
const key2 = await secp256k1.unmarshalSecp256k1PrivateKey(keyMarshal)
const keyMarshal2 = key2.marshal()
expect(keyMarshal).to.eql(keyMarshal2) expect(keyMarshal).to.eql(keyMarshal2)
const pk = key.public const pk = key.public
const pkMarshal = crypto.keys.marshalPublicKey(pk) const pkMarshal = pk.marshal()
const pk2 = crypto.keys.unmarshalPublicKey(pkMarshal) const pk2 = secp256k1.unmarshalSecp256k1PublicKey(pkMarshal)
const pkMarshal2 = crypto.keys.marshalPublicKey(pk2) const pkMarshal2 = pk2.marshal()
expect(pkMarshal).to.eql(pkMarshal2) expect(pkMarshal).to.eql(pkMarshal2)
}) })
it('unmarshals a secp256k1 private key', async () => { it('key id', async () => {
const key = await crypto.keys.unmarshalPrivateKey(fixtures.pbmPrivateKey) const id = await key.id()
expect(key).to.exist() expect(id).to.exist()
expect(id).to.be.a('string')
}) })
it('unmarshals a secp256k1 public key', () => { describe('key equals', () => {
const key = crypto.keys.unmarshalPublicKey(fixtures.pbmPublicKey) it('equals itself', () => {
expect(key).to.exist() expect(key.equals(key)).to.eql(true)
expect(key.public.equals(key.public)).to.eql(true)
})
it('not equals other key', async () => {
const key2 = await secp256k1.generateKeyPair(256)
expect(key.equals(key2)).to.eql(false)
expect(key2.equals(key)).to.eql(false)
expect(key.public.equals(key2.public)).to.eql(false)
expect(key2.public.equals(key.public)).to.eql(false)
})
})
it('sign and verify', async () => {
const data = Buffer.from('hello world')
const sig = await key.sign(data)
const valid = await key.public.verify(data, sig)
expect(valid).to.eql(true)
})
it('fails to verify for different data', async () => {
const data = Buffer.from('hello world')
const sig = await key.sign(data)
const valid = await key.public.verify(Buffer.from('hello'), sig)
expect(valid).to.eql(false)
})
})
describe('key generation error', () => {
let generateKey
let secp256k1
before(() => {
generateKey = secp256k1Crypto.generateKey
secp256k1 = require('../../src/keys/secp256k1-class')(keysPBM, randomBytes, secp256k1Crypto)
secp256k1Crypto.generateKey = () => { throw new Error('Error generating key') }
})
after(() => {
secp256k1Crypto.generateKey = generateKey
})
it('returns an error if key generation fails', async () => {
try {
await secp256k1.generateKeyPair()
} catch (err) {
return expect(err.message).to.equal('Error generating key')
}
throw new Error('Expected error to be thrown')
})
})
describe('handles generation of invalid key', () => {
let generateKey
let secp256k1
before(() => {
generateKey = secp256k1Crypto.generateKey
secp256k1 = require('../../src/keys/secp256k1-class')(keysPBM, randomBytes, secp256k1Crypto)
secp256k1Crypto.generateKey = () => Buffer.from('not a real key')
})
after(() => {
secp256k1Crypto.generateKey = generateKey
})
it('returns an error if key generator returns an invalid key', async () => {
try {
await secp256k1.generateKeyPair()
} catch (err) {
return expect(err.message).to.equal('Expected private key to be an Uint8Array with length 32')
}
throw new Error('Expected error to be thrown')
})
})
describe('crypto functions', () => {
let privKey
let pubKey
before(async () => {
privKey = await secp256k1Crypto.generateKey()
pubKey = secp256k1Crypto.computePublicKey(privKey)
})
it('generates valid keys', () => {
expect(() => {
secp256k1Crypto.validatePrivateKey(privKey)
secp256k1Crypto.validatePublicKey(pubKey)
}).to.not.throw()
})
it('does not validate an invalid key', () => {
expect(() => secp256k1Crypto.validatePublicKey(Buffer.from('42'))).to.throw()
expect(() => secp256k1Crypto.validatePrivateKey(Buffer.from('42'))).to.throw()
})
it('validates a correct signature', async () => {
const sig = await secp256k1Crypto.hashAndSign(privKey, Buffer.from('hello'))
const valid = await secp256k1Crypto.hashAndVerify(pubKey, sig, Buffer.from('hello'))
expect(valid).to.equal(true)
})
it('errors if given a null buffer to sign', async () => {
try {
await secp256k1Crypto.hashAndSign(privKey, null)
} catch (err) {
return // expected
}
throw new Error('Expected error to be thrown')
})
it('errors when signing with an invalid key', async () => {
try {
await secp256k1Crypto.hashAndSign(Buffer.from('42'), Buffer.from('Hello'))
} catch (err) {
return expect(err.message).to.equal('Expected private key to be an Uint8Array with length 32')
}
throw new Error('Expected error to be thrown')
})
it('errors if given a null buffer to validate', async () => {
const sig = await secp256k1Crypto.hashAndSign(privKey, Buffer.from('hello'))
try {
await secp256k1Crypto.hashAndVerify(privKey, sig, null)
} catch (err) {
return // expected
}
throw new Error('Expected error to be thrown')
})
it('errors when validating a message with an invalid signature', async () => {
try {
await secp256k1Crypto.hashAndVerify(pubKey, Buffer.from('invalid-sig'), Buffer.from('hello'))
} catch (err) {
return expect(err.message).to.equal('Signature could not be parsed')
}
throw new Error('Expected error to be thrown')
})
it('errors when signing with an invalid key', async () => {
try {
await secp256k1Crypto.hashAndSign(Buffer.from('42'), Buffer.from('Hello'))
} catch (err) {
return expect(err.message).to.equal('Expected private key to be an Uint8Array with length 32')
}
throw new Error('Expected error to be thrown')
})
it('throws when compressing an invalid public key', () => {
expect(() => secp256k1Crypto.compressPublicKey(Buffer.from('42'))).to.throw()
})
it('throws when decompressing an invalid public key', () => {
expect(() => secp256k1Crypto.decompressPublicKey(Buffer.from('42'))).to.throw()
})
it('compresses/decompresses a valid public key', () => {
const decompressed = secp256k1Crypto.decompressPublicKey(pubKey)
expect(decompressed).to.exist()
expect(decompressed.length).to.be.eql(65)
const recompressed = secp256k1Crypto.compressPublicKey(decompressed)
expect(recompressed).to.eql(pubKey)
})
})
describe('go interop', () => {
const fixtures = require('../fixtures/go-key-secp256k1')
it('loads a private key marshaled by go-libp2p-crypto', async () => {
// we need to first extract the key data from the protobuf, which is
// normally handled by js-libp2p-crypto
const decoded = keysPBM.PrivateKey.decode(fixtures.privateKey)
expect(decoded.Type).to.eql(keysPBM.KeyType.Secp256k1)
const key = await secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data)
expect(key).to.be.an.instanceof(secp256k1.Secp256k1PrivateKey)
expect(key.bytes).to.eql(fixtures.privateKey)
})
it('loads a public key marshaled by go-libp2p-crypto', () => {
const decoded = keysPBM.PublicKey.decode(fixtures.publicKey)
expect(decoded.Type).to.be.eql(keysPBM.KeyType.Secp256k1)
const key = secp256k1.unmarshalSecp256k1PublicKey(decoded.Data)
expect(key).to.be.an.instanceof(secp256k1.Secp256k1PublicKey)
expect(key.bytes).to.eql(fixtures.publicKey)
})
it('generates the same signature as go-libp2p-crypto', async () => {
const decoded = keysPBM.PrivateKey.decode(fixtures.privateKey)
expect(decoded.Type).to.eql(keysPBM.KeyType.Secp256k1)
const key = await secp256k1.unmarshalSecp256k1PrivateKey(decoded.Data)
const sig = await key.sign(fixtures.message)
expect(sig).to.eql(fixtures.signature)
}) })
}) })