Skip to content

Commit

Permalink
feat: add secp256k1 EC Key curve and ES256K
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Jul 8, 2019
1 parent 2619091 commit 211d7af
Show file tree
Hide file tree
Showing 26 changed files with 613 additions and 145 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ calculation loop when the private key's private exponent was outright
invalid or tampered with.

The new methods still allow to import private RSA keys with these
optimization key parameters missing but its disabled by default and one
optimization key parameters missing but it is disabled by default and one
should choose to enable it when working with keys from trusted sources

It is recommended not to use @panva/jose versions with this feature in
Expand Down
6 changes: 6 additions & 0 deletions P-256K/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// rename 'secp256k1' to 'P-256K'

const { rename } = require('../lib/jwk/key/secp256k1_crv')
rename('P-256K')

module.exports = require('../lib')
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The following specifications are implemented by @panva/jose
- JSON Web Key Thumbprint - [RFC7638][spec-thumbprint]
- JWS Unencoded Payload Option - [RFC7797][spec-b64]
- CFRG Elliptic Curve Signatures (EdDSA) - [RFC8037][spec-okp]
- secp256k1 curve EC Key support - [JOSE Registrations for WebAuthn Algorithms][draft-secp256k1]

The test suite utilizes examples defined in [RFC7520][spec-cookbook] to confirm its JOSE
implementation is correct.
Expand Down Expand Up @@ -47,7 +48,7 @@ Legend:
| -- | -- | -- |
| RSASSA-PKCS1-v1_5 || RS256, RS384, RS512 |
| RSASSA-PSS || PS256, PS384, PS512 |
| ECDSA || ES256, ES384, ES512 |
| ECDSA || ES256, ES256K, ES384, ES512 |
| Edwards-curve DSA || EdDSA |
| HMAC with SHA-2 || HS256, HS384, HS512 |

Expand Down Expand Up @@ -247,6 +248,32 @@ jose.JWE.decrypt(
)
```

#### secp256k1

Note: the secp256k1 JOSE parameters registration and the RFC is still in a draft state. If the WG
draft changes its mind about the parameter names again the new values will be propagated as a MINOR
library version.

When you require `@panva/jose` you can work with `secp256k1` EC keys right away, the EC JWK `crv`
used is as per the specification `secp256k1`.

```js
const jose = require('@panva/jose')
let key = jose.JWK.generateSync('EC', 'secp256k1')
key = jose.JWK.asKey(fs.readFileSync('path/to/key/file'))
key.crv === 'secp256k1'
```

For legacy reasons the unregistered EC JWK `crv` value `P-256K` is also supported but you must
require `@panva/jose` like so to use it:

```js
const jose = require('@panva/jose/P-256K')
let key = jose.JWK.generateSync('EC', 'P-256K')
key = jose.JWK.asKey(fs.readFileSync('path/to/key/file'))
key.crv === 'P-256K'
```

## FAQ

#### Semver?
Expand Down Expand Up @@ -315,6 +342,7 @@ in terms of performance and API (not having well defined errors).
[spec-jws]: https://tools.ietf.org/html/rfc7515
[spec-jwt]: https://tools.ietf.org/html/rfc7519
[spec-okp]: https://tools.ietf.org/html/rfc8037
[draft-secp256k1]: https://tools.ietf.org/html/draft-ietf-cose-webauthn-algorithms-01
[spec-thumbprint]: https://tools.ietf.org/html/rfc7638
[suggest-feature]: https://github.com/panva/jose/issues/new?labels=enhancement&template=feature-request.md&title=proposal%3A+
[support-patreon]: https://www.patreon.com/panva
Expand Down
4 changes: 3 additions & 1 deletion lib/help/consts.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { name: secp256k1 } = require('../jwk/key/secp256k1_crv')

module.exports.KEYOBJECT = Symbol('KEYOBJECT')
module.exports.PRIVATE_MEMBERS = Symbol('PRIVATE_MEMBERS')
module.exports.PUBLIC_MEMBERS = Symbol('PUBLIC_MEMBERS')
Expand All @@ -18,7 +20,7 @@ module.exports.OPS = OPS
module.exports.USES = USES

module.exports.OKP_CURVES = new Set(['Ed25519', 'Ed448', 'X25519', 'X448'])
module.exports.EC_CURVES = new Set(['P-256', 'P-384', 'P-521'])
module.exports.EC_CURVES = new Set(['P-256', secp256k1, 'P-384', 'P-521'])
module.exports.ECDH_ALGS = ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']

module.exports.KEYLENGTHS = {
Expand Down
1 change: 1 addition & 0 deletions lib/help/ecdsa_signatures.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const getParamSize = keySize => ((keySize / 8) | 0) + (keySize % 8 === 0 ? 0 : 1

const paramBytesForAlg = {
ES256: getParamSize(256),
ES256K: getParamSize(256),
ES384: getParamSize(384),
ES512: getParamSize(521)
}
Expand Down
7 changes: 6 additions & 1 deletion lib/help/key_utils.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
const { createPublicKey } = require('crypto')

const base64url = require('./base64url')
const { name: secp256k1 } = require('../jwk/key/secp256k1_crv')
const errors = require('../errors')

const base64url = require('./base64url')
const asn1 = require('./asn1')
const computePrimes = require('./rsa_primes')
const { OKP_CURVES, EC_CURVES } = require('./consts')

const oidHexToCurve = new Map([
['06082a8648ce3d030107', 'P-256'],
['06052b8104000a', secp256k1],
['06052b81040022', 'P-384'],
['06052b81040023', 'P-521']
])
const EC_KEY_OID = '1.2.840.10045.2.1'.split('.')
const crvToOid = new Map([
['P-256', '1.2.840.10045.3.1.7'.split('.')],
[secp256k1, '1.3.132.0.10'.split('.')],
['P-384', '1.3.132.0.34'.split('.')],
['P-521', '1.3.132.0.35'.split('.')]
])
const crvToOidBuf = new Map([
['P-256', Buffer.from('06082a8648ce3d030107', 'hex')],
[secp256k1, Buffer.from('06052b8104000a', 'hex')],
['P-384', Buffer.from('06052b81040022', 'hex')],
['P-521', Buffer.from('06052b81040023', 'hex')]
])
Expand Down
2 changes: 1 addition & 1 deletion lib/help/node_alg.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
module.exports = alg => `sha${alg.substr(-3)}`
module.exports = alg => `sha${alg.substr(2, 3)}`
2 changes: 1 addition & 1 deletion lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface KeyParameters extends BasicParameters {
x5t?: string
'x5t#S256'?: string
}
type ECCurve = 'P-256' | 'P-384' | 'P-521'
type ECCurve = 'P-256' | 'secp256k1' | 'P-384' | 'P-521'
type OKPCurve = 'Ed25519' | 'Ed448' | 'X25519' | 'X448'
type keyType = 'RSA' | 'EC' | 'OKP' | 'oct'
type asymmetricKeyObjectTypes = 'private' | 'public'
Expand Down
4 changes: 4 additions & 0 deletions lib/jwa/ecdh/derive.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { createECDH, createHash, constants: { POINT_CONVERSION_UNCOMPRESSED } } = require('crypto')

const base64url = require('../../help/base64url')
const { name: secp256k1 } = require('../../jwk/key/secp256k1_crv')

const crvToCurve = (crv) => {
switch (crv) {
Expand All @@ -10,9 +11,12 @@ const crvToCurve = (crv) => {
return 'secp384r1'
case 'P-521':
return 'secp521r1'
case 'secp256k1':
case 'X448':
case 'X25519':
return crv
case secp256k1:
return 'secp256k1'
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/jwa/ecdsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const verify = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature)
}

module.exports = (JWA) => {
['ES256', 'ES384', 'ES512'].forEach((jwaAlg) => {
['ES256', 'ES384', 'ES512', 'ES256K'].forEach((jwaAlg) => {
const nodeAlg = resolveNodeAlg(jwaAlg)

assert(!JWA.sign.has(jwaAlg), `sign alg ${jwaAlg} already registered`)
Expand Down
11 changes: 11 additions & 0 deletions lib/jwk/key/ec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const {
} = require('../../help/consts')

const errors = require('../../errors')
const { name: secp256k1 } = require('./secp256k1_crv')

const Key = require('./base')

Expand All @@ -21,6 +22,8 @@ const crvToDSA = (crv) => {
switch (crv) {
case 'P-256':
return 'ES256'
case secp256k1:
return 'ES256K'
case 'P-384':
return 'ES384'
case 'P-521':
Expand Down Expand Up @@ -115,6 +118,10 @@ class ECKey extends Key {
throw new errors.JOSENotSupported(`unsupported EC key curve: ${crv}`)
}

if (crv === secp256k1 && crv !== 'secp256k1') {
crv = 'secp256k1'
}

const { privateKey, publicKey } = await generateKeyPair('ec', { namedCurve: crv })

return privat ? privateKey : publicKey
Expand All @@ -125,6 +132,10 @@ class ECKey extends Key {
throw new errors.JOSENotSupported(`unsupported EC key curve: ${crv}`)
}

if (crv === secp256k1 && crv !== 'secp256k1') {
crv = 'secp256k1'
}

const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: crv })

return privat ? privateKey : publicKey
Expand Down
6 changes: 6 additions & 0 deletions lib/jwk/key/secp256k1_crv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
name: 'secp256k1',
rename (value) {
module.exports.name = value
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"jwks",
"jws",
"jwt",
"secp256k1",
"sign",
"verify"
],
Expand Down
24 changes: 24 additions & 0 deletions test/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ module.exports.JWK = {
d: '_i_1Ac5oVmbBxGvEvOEFHMpzMXKZi8voUx8I3Gl6IxY'
},

'secp256k1': {
kty: 'EC',
crv: 'secp256k1',
x: 'VRaLqtMjg_JRaDzkbfit7zonkOGDZ42qbZyljhqsg3U',
y: '5qgTxoRAf0hJxcphVg1NE9r0Xv-HHZyVIJxEbo6SAsQ',
d: 'xTAmXNRL8ksBlr-F3yXDrUdRDn1gyIvY_PC2e_iUK7c'
},

'P-256K': {
kty: 'EC',
crv: 'P-256K',
x: 'VRaLqtMjg_JRaDzkbfit7zonkOGDZ42qbZyljhqsg3U',
y: '5qgTxoRAf0hJxcphVg1NE9r0Xv-HHZyVIJxEbo6SAsQ',
d: 'xTAmXNRL8ksBlr-F3yXDrUdRDn1gyIvY_PC2e_iUK7c'
},

'P-384': {
kty: 'EC',
crv: 'P-384',
Expand Down Expand Up @@ -100,6 +116,14 @@ module.exports.PEM = {
private: readFileSync(join(__dirname, 'P-256.key')),
public: readFileSync(join(__dirname, 'P-256.pem'))
},
'secp256k1': {
private: readFileSync(join(__dirname, 'secp256k1.key')),
public: readFileSync(join(__dirname, 'secp256k1.pem'))
},
'P-256K': {
private: readFileSync(join(__dirname, 'secp256k1.key')),
public: readFileSync(join(__dirname, 'secp256k1.pem'))
},
'P-384': {
private: readFileSync(join(__dirname, 'P-384.key')),
public: readFileSync(join(__dirname, 'P-384.pem'))
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/secp256k1.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgxTAmXNRL8ksBlr+F3yXD
rUdRDn1gyIvY/PC2e/iUK7ehRANCAARVFouq0yOD8lFoPORt+K3vOieQ4YNnjapt
nKWOGqyDdeaoE8aEQH9IScXKYVYNTRPa9F7/hx2clSCcRG6OkgLE
-----END PRIVATE KEY-----
4 changes: 4 additions & 0 deletions test/fixtures/secp256k1.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEVRaLqtMjg/JRaDzkbfit7zonkOGDZ42q
bZyljhqsg3XmqBPGhEB/SEnFymFWDU0T2vRe/4cdnJUgnERujpICxA==
-----END PUBLIC KEY-----
26 changes: 26 additions & 0 deletions test/help/P-256K.key_utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// require 'secp256k1' renamed to 'P-256K'
require('../../P-256K')

const test = require('ava')
const { createPublicKey, createPrivateKey } = require('crypto')

const { keyObjectToJWK, jwkToPem } = require('../../lib/help/key_utils')
const { JWK: fixtures } = require('../fixtures')
const clone = obj => JSON.parse(JSON.stringify(obj))

test('EC P-256K Public key', t => {
const expected = clone(fixtures['P-256K'])
delete expected.d
const pem = createPublicKey(jwkToPem(expected))
const actual = keyObjectToJWK(pem)

t.deepEqual(actual, expected)
})

test('EC P-256K Private key', t => {
const expected = fixtures['P-256K']
const pem = createPrivateKey(jwkToPem(expected))
const actual = keyObjectToJWK(pem)

t.deepEqual(actual, expected)
})
31 changes: 31 additions & 0 deletions test/jwe/smoke.P-256K.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// require 'secp256k1' renamed to 'P-256K'
require('../../P-256K')

const test = require('ava')

const { JWK: { asKey } } = require('../..')

const ENCS = [
'A128GCM',
'A192GCM',
'A256GCM',
'A128CBC-HS256',
'A192CBC-HS384',
'A256CBC-HS512'
]

const type = 'P-256K'
const { private: key, public: pub } = require('../fixtures').PEM[type]

const { JWE: { success, failure } } = require('../macros')

const eKey = asKey(pub)
const dKey = asKey(key)

;[...eKey.algorithms('wrapKey'), ...eKey.algorithms('deriveKey')].forEach((alg) => {
ENCS.forEach((enc) => {
if (alg === 'ECDH-ES' && ['A192CBC-HS384', 'A256CBC-HS512'].includes(enc)) return
test(`key ${type} > alg ${alg} > ${enc}`, success, eKey, dKey, alg, enc)
test(`key ${type} > alg ${alg} > ${enc} (negative cases)`, failure, eKey, dKey, alg, enc)
})
})
Loading

0 comments on commit 211d7af

Please sign in to comment.