feat(keys): add Ed25519 support for signing & verification

Closes #43
This commit is contained in:
Yusef Napora 2016-12-23 08:52:40 -05:00 committed by Friedel Ziegelmayer
parent e92bab1736
commit c45bdf602e
9 changed files with 411 additions and 8 deletions

View File

@ -42,6 +42,7 @@
"pem-jwk": "^1.5.1", "pem-jwk": "^1.5.1",
"protocol-buffers": "^3.2.1", "protocol-buffers": "^3.2.1",
"rsa-pem-to-jwk": "^1.1.3", "rsa-pem-to-jwk": "^1.1.3",
"tweetnacl": "^0.14.5",
"webcrypto-shim": "github:dignifiedquire/webcrypto-shim#master" "webcrypto-shim": "github:dignifiedquire/webcrypto-shim#master"
}, },
"devDependencies": { "devDependencies": {
@ -75,4 +76,4 @@
"Richard Littauer <richard.littauer@gmail.com>", "Richard Littauer <richard.littauer@gmail.com>",
"nikuda <nikuda@gmail.com>" "nikuda <nikuda@gmail.com>"
] ]
} }

View File

@ -5,3 +5,4 @@ exports.hmac = require('./crypto/hmac')
exports.ecdh = require('./crypto/ecdh') exports.ecdh = require('./crypto/ecdh')
exports.aes = require('./crypto/aes') exports.aes = require('./crypto/aes')
exports.rsa = require('./crypto/rsa') exports.rsa = require('./crypto/rsa')
exports.ed25519 = require('./crypto/ed25519')

View File

@ -2,6 +2,7 @@
module.exports = `enum KeyType { module.exports = `enum KeyType {
RSA = 0; RSA = 0;
Ed25519 = 1;
} }
message PublicKey { message PublicKey {

34
src/crypto/ed25519.js Normal file
View File

@ -0,0 +1,34 @@
'use strict'
const nacl = require('tweetnacl')
const setImmediate = require('async/setImmediate')
exports.publicKeyLength = nacl.sign.publicKeyLength
exports.privateKeyLength = nacl.sign.secretKeyLength
exports.generateKey = function (callback) {
const done = (err, res) => setImmediate(() => {
callback(err, res)
})
let keys
try {
keys = nacl.sign.keyPair()
} catch (err) {
done(err)
return
}
done(null, keys)
}
exports.hashAndSign = function (key, msg, callback) {
setImmediate(() => {
callback(null, Buffer.from(nacl.sign.detached(msg, key)))
})
}
exports.hashAndVerify = function (key, sig, msg, callback) {
setImmediate(() => {
callback(null, nacl.sign.detached.verify(msg, sig, key))
})
}

View File

@ -9,6 +9,8 @@ exports.aes = c.aes
exports.webcrypto = c.webcrypto exports.webcrypto = c.webcrypto
const keys = exports.keys = require('./keys') const keys = exports.keys = require('./keys')
const KEY_TYPES = ['rsa', 'ed25519']
exports.keyStretcher = require('./key-stretcher') exports.keyStretcher = require('./key-stretcher')
exports.generateEphemeralKeyPair = require('./ephemeral-keys') exports.generateEphemeralKeyPair = require('./ephemeral-keys')
@ -30,6 +32,8 @@ exports.unmarshalPublicKey = (buf) => {
switch (decoded.Type) { switch (decoded.Type) {
case pbm.KeyType.RSA: case pbm.KeyType.RSA:
return keys.rsa.unmarshalRsaPublicKey(decoded.Data) return keys.rsa.unmarshalRsaPublicKey(decoded.Data)
case pbm.KeyType.Ed25519:
return keys.ed25519.unmarshalEd25519PublicKey(decoded.Data)
default: default:
throw new Error('invalid or unsupported key type') throw new Error('invalid or unsupported key type')
} }
@ -38,9 +42,7 @@ exports.unmarshalPublicKey = (buf) => {
// Converts a public key object into a protobuf serialized public key // Converts a public key object into a protobuf serialized public key
exports.marshalPublicKey = (key, type) => { exports.marshalPublicKey = (key, type) => {
type = (type || 'rsa').toLowerCase() type = (type || 'rsa').toLowerCase()
if (KEY_TYPES.indexOf(type) < 0) {
// for now only rsa is supported
if (type !== 'rsa') {
throw new Error('invalid or unsupported key type') throw new Error('invalid or unsupported key type')
} }
@ -55,6 +57,8 @@ exports.unmarshalPrivateKey = (buf, callback) => {
switch (decoded.Type) { switch (decoded.Type) {
case pbm.KeyType.RSA: case pbm.KeyType.RSA:
return keys.rsa.unmarshalRsaPrivateKey(decoded.Data, callback) return keys.rsa.unmarshalRsaPrivateKey(decoded.Data, callback)
case pbm.KeyType.Ed25519:
return keys.ed25519.unmarshalEd25519PrivateKey(decoded.Data, callback)
default: default:
callback(new Error('invalid or unsupported key type')) callback(new Error('invalid or unsupported key type'))
} }
@ -63,9 +67,7 @@ exports.unmarshalPrivateKey = (buf, callback) => {
// Converts a private key object into a protobuf serialized private key // Converts a private key object into a protobuf serialized private key
exports.marshalPrivateKey = (key, type) => { exports.marshalPrivateKey = (key, type) => {
type = (type || 'rsa').toLowerCase() type = (type || 'rsa').toLowerCase()
if (KEY_TYPES.indexOf(type) < 0) {
// for now only rsa is supported
if (type !== 'rsa') {
throw new Error('invalid or unsupported key type') throw new Error('invalid or unsupported key type')
} }

141
src/keys/ed25519.js Normal file
View File

@ -0,0 +1,141 @@
'use strict'
const multihashing = require('multihashing-async')
const protobuf = require('protocol-buffers')
const crypto = require('../crypto').ed25519
const pbm = protobuf(require('../crypto.proto'))
class Ed25519PublicKey {
constructor (key) {
this._key = ensureKey(key, crypto.publicKeyLength)
}
verify (data, sig, callback) {
ensure(callback)
crypto.hashAndVerify(this._key, sig, data, callback)
}
marshal () {
return Buffer.from(this._key)
}
get bytes () {
return pbm.PublicKey.encode({
Type: pbm.KeyType.Ed25519,
Data: this.marshal()
})
}
equals (key) {
return this.bytes.equals(key.bytes)
}
hash (callback) {
ensure(callback)
multihashing(this.bytes, 'sha2-256', callback)
}
}
class Ed25519PrivateKey {
// key - 64 byte Uint8Array or Buffer containing private key
// publicKey - 32 byte Uint8Array or Buffer containing public key
constructor (key, publicKey) {
this._key = ensureKey(key, crypto.privateKeyLength)
this._publicKey = ensureKey(publicKey, crypto.publicKeyLength)
}
sign (message, callback) {
ensure(callback)
crypto.hashAndSign(this._key, message, callback)
}
get public () {
if (!this._publicKey) {
throw new Error('public key not provided')
}
return new Ed25519PublicKey(this._publicKey)
}
marshal () {
return Buffer.concat([Buffer.from(this._key), Buffer.from(this._publicKey)])
}
get bytes () {
return pbm.PrivateKey.encode({
Type: pbm.KeyType.Ed25519,
Data: this.marshal()
})
}
equals (key) {
return this.bytes.equals(key.bytes)
}
hash (callback) {
ensure(callback)
multihashing(this.bytes, 'sha2-256', callback)
}
}
function unmarshalEd25519PrivateKey (bytes, callback) {
try {
bytes = ensureKey(bytes, crypto.privateKeyLength + crypto.publicKeyLength)
} catch (err) {
return callback(err)
}
const privateKeyBytes = bytes.slice(0, crypto.privateKeyLength)
const publicKeyBytes = bytes.slice(crypto.privateKeyLength, bytes.length)
callback(null, new Ed25519PrivateKey(privateKeyBytes, publicKeyBytes))
}
function unmarshalEd25519PublicKey (bytes) {
bytes = ensureKey(bytes, crypto.publicKeyLength)
return new Ed25519PublicKey(bytes)
}
function generateKeyPair (_bits, cb) {
if (cb === undefined && typeof _bits === 'function') {
cb = _bits
}
crypto.generateKey((err, keys) => {
if (err) {
return cb(err)
}
let privkey
try {
privkey = new Ed25519PrivateKey(keys.secretKey, keys.publicKey)
} catch (err) {
cb(err)
return
}
cb(null, privkey)
})
}
function ensure (cb) {
if (typeof cb !== 'function') {
throw new Error('callback is required')
}
}
function ensureKey (key, length) {
if (Buffer.isBuffer(key)) {
key = new Uint8Array(key)
}
if (!(key instanceof Uint8Array) || key.length !== length) {
throw new Error('Key must be a Uint8Array or Buffer of length ' + length)
}
return key
}
module.exports = {
Ed25519PublicKey,
Ed25519PrivateKey,
unmarshalEd25519PrivateKey,
unmarshalEd25519PublicKey,
generateKeyPair
}

View File

@ -1,5 +1,6 @@
'use strict' 'use strict'
module.exports = { module.exports = {
rsa: require('./rsa') rsa: require('./rsa'),
ed25519: require('./ed25519')
} }

194
test/ed25519.spec.js Normal file
View File

@ -0,0 +1,194 @@
/* eslint-env mocha */
'use strict'
const expect = require('chai').expect
const crypto = require('../src')
const ed25519 = crypto.keys.ed25519
const fixtures = require('./fixtures/go-key-ed25519')
describe('ed25519', () => {
let key
before((done) => {
crypto.generateKeyPair('Ed25519', 512, (err, _key) => {
if (err) return done(err)
key = _key
done()
})
})
it('generates a valid key', (done) => {
expect(
key
).to.be.an.instanceof(
ed25519.Ed25519PrivateKey
)
key.hash((err, digest) => {
if (err) {
return done(err)
}
expect(digest).to.have.length(34)
done()
})
})
it('signs', (done) => {
const text = crypto.randomBytes(512)
key.sign(text, (err, sig) => {
if (err) {
return done(err)
}
key.public.verify(text, sig, (err, res) => {
if (err) {
return done(err)
}
expect(res).to.be.eql(true)
done()
})
})
})
it('encoding', (done) => {
const keyMarshal = key.marshal()
ed25519.unmarshalEd25519PrivateKey(keyMarshal, (err, key2) => {
if (err) {
return done(err)
}
const keyMarshal2 = key2.marshal()
expect(
keyMarshal
).to.be.eql(
keyMarshal2
)
const pk = key.public
const pkMarshal = pk.marshal()
const pk2 = ed25519.unmarshalEd25519PublicKey(pkMarshal)
const pkMarshal2 = pk2.marshal()
expect(
pkMarshal
).to.be.eql(
pkMarshal2
)
done()
})
})
describe('key equals', () => {
it('equals itself', () => {
expect(
key.equals(key)
).to.be.eql(
true
)
expect(
key.public.equals(key.public)
).to.be.eql(
true
)
})
it('not equals other key', (done) => {
crypto.generateKeyPair('Ed25519', 512, (err, key2) => {
if (err) return done(err)
expect(
key.equals(key2)
).to.be.eql(
false
)
expect(
key2.equals(key)
).to.be.eql(
false
)
expect(
key.public.equals(key2.public)
).to.be.eql(
false
)
expect(
key2.public.equals(key.public)
).to.be.eql(
false
)
done()
})
})
})
it('sign and verify', (done) => {
const data = new Buffer('hello world')
key.sign(data, (err, sig) => {
if (err) {
return done(err)
}
key.public.verify(data, sig, (err, valid) => {
if (err) {
return done(err)
}
expect(valid).to.be.eql(true)
done()
})
})
})
it('fails to verify for different data', (done) => {
const data = new Buffer('hello world')
key.sign(data, (err, sig) => {
if (err) {
return done(err)
}
key.public.verify(new Buffer('hello'), sig, (err, valid) => {
if (err) {
return done(err)
}
expect(valid).to.be.eql(false)
done()
})
})
})
describe('go interop', () => {
let privateKey
before((done) => {
crypto.unmarshalPrivateKey(fixtures.verify.privateKey, (err, key) => {
expect(err).to.not.exist
privateKey = key
done()
})
})
it('verifies with data from go', (done) => {
const key = crypto.unmarshalPublicKey(fixtures.verify.publicKey)
key.verify(fixtures.verify.data, fixtures.verify.signature, (err, ok) => {
if (err) throw err
expect(err).to.not.exist
expect(ok).to.be.eql(true)
done()
})
})
it('generates the same signature as go', (done) => {
privateKey.sign(fixtures.verify.data, (err, sig) => {
expect(err).to.not.exist
expect(sig).to.deep.equal(fixtures.verify.signature)
done()
})
})
})
})

28
test/fixtures/go-key-ed25519.js vendored Normal file
View File

@ -0,0 +1,28 @@
'use strict'
module.exports = {
// These were generated in a gore (https://github.com/motemen/gore) repl session:
//
// :import github.com/libp2p/go-libp2p-crypto
// :import crypto/rand
// priv, pub, err := crypto.GenerateEd25519Key(rand.Reader)
// pubkeyBytes, err := pub.Bytes()
// privkeyBytes, err := priv.Bytes()
// data := []byte("hello! and welcome to some awesome crypto primitives")
// sig, err := priv.Sign(data)
//
// :import io/ioutil
// ioutil.WriteFile("/tmp/pubkey_go.bin", pubkeyBytes, 0644)
// // etc..
//
// Then loaded into a node repl and dumped to arrays with:
//
// var pubkey = Array.from(fs.readFileSync('/tmp/pubkey_go.bin'))
// console.log(JSON.stringify(pubkey))
verify: {
privateKey: Buffer.from([8, 1, 18, 96, 201, 208, 1, 110, 176, 16, 230, 37, 66, 184, 149, 252, 78, 56, 206, 136, 2, 38, 118, 152, 226, 197, 117, 200, 54, 189, 156, 218, 184, 7, 118, 57, 233, 49, 221, 97, 164, 158, 241, 129, 73, 166, 225, 255, 193, 118, 22, 84, 55, 15, 249, 168, 225, 180, 198, 191, 14, 75, 187, 243, 150, 91, 232, 37, 233, 49, 221, 97, 164, 158, 241, 129, 73, 166, 225, 255, 193, 118, 22, 84, 55, 15, 249, 168, 225, 180, 198, 191, 14, 75, 187, 243, 150, 91, 232, 37]),
publicKey: Buffer.from([8, 1, 18, 32, 233, 49, 221, 97, 164, 158, 241, 129, 73, 166, 225, 255, 193, 118, 22, 84, 55, 15, 249, 168, 225, 180, 198, 191, 14, 75, 187, 243, 150, 91, 232, 37]),
data: Buffer.from([104, 101, 108, 108, 111, 33, 32, 97, 110, 100, 32, 119, 101, 108, 99, 111, 109, 101, 32, 116, 111, 32, 115, 111, 109, 101, 32, 97, 119, 101, 115, 111, 109, 101, 32, 99, 114, 121, 112, 116, 111, 32, 112, 114, 105, 109, 105, 116, 105, 118, 101, 115]),
signature: Buffer.from([7, 230, 175, 164, 228, 58, 78, 208, 62, 243, 73, 142, 83, 195, 176, 217, 166, 62, 41, 165, 168, 164, 75, 179, 163, 86, 102, 32, 18, 84, 150, 237, 39, 207, 213, 20, 134, 237, 50, 41, 176, 183, 229, 133, 38, 255, 42, 228, 68, 186, 100, 14, 175, 156, 243, 118, 125, 125, 120, 212, 124, 103, 252, 12])
}
}