Skip to content

Commit 8b87f2e

Browse files
authored
Merge pull request #19 from mrkvon/ES256
Add ES256 algorithm
2 parents dc233d3 + 056430d commit 8b87f2e

File tree

4 files changed

+373
-2
lines changed

4 files changed

+373
-2
lines changed

src/algorithms/ECDSA.js

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
'use strict'
2+
3+
/**
4+
* Dependencies
5+
* @ignore
6+
*/
7+
const base64url = require('base64url')
8+
const crypto = require('isomorphic-webcrypto')
9+
const TextEncoder = require('../text-encoder')
10+
11+
/**
12+
* ECDSA with SHA-2 Functions and P Curves
13+
*/
14+
class ECDSA {
15+
16+
/**
17+
* Constructor
18+
*
19+
* @param {string} bitlength
20+
*/
21+
constructor (params) {
22+
this.params = params
23+
}
24+
25+
/**
26+
* Sign
27+
*
28+
* @description
29+
* Generate a hash-based message authentication code for a
30+
* given input and key. Enforce the key length is equal to
31+
* or greater than the bitlength.
32+
*
33+
* @param {CryptoKey} key
34+
* @param {string} data
35+
*
36+
* @returns {string}
37+
*/
38+
sign (key, data) {
39+
let algorithm = this.params
40+
41+
// TODO: validate key length
42+
43+
data = new TextEncoder().encode(data)
44+
45+
return crypto.subtle
46+
.sign(algorithm, key, data)
47+
.then(signature => base64url(Buffer.from(signature)))
48+
}
49+
50+
/**
51+
* Verify
52+
*
53+
* @description
54+
* Verify a digital signature for a given input and private key.
55+
*
56+
* @param {CryptoKey} key
57+
* @param {string} signature
58+
* @param {string} data
59+
*
60+
* @returns {Boolean}
61+
*/
62+
verify (key, signature, data) {
63+
let algorithm = this.params
64+
65+
if (typeof signature === 'string') {
66+
signature = Uint8Array.from(base64url.toBuffer(signature))
67+
}
68+
69+
if (typeof data === 'string') {
70+
data = new TextEncoder().encode(data)
71+
}
72+
73+
return crypto.subtle.verify(algorithm, key, signature, data)
74+
}
75+
76+
/**
77+
* Assert Sufficient Key Length
78+
*
79+
* @description Assert that the key length is sufficient
80+
* @param {string} key
81+
*/
82+
assertSufficientKeyLength (key) {
83+
if (key.length < this.bitlength) {
84+
throw new Error('The key is too short.')
85+
}
86+
}
87+
88+
/**
89+
* importKey
90+
* copied from ./RSASSA-PKCS1-v1_5.js, and it works!
91+
*
92+
* @param {JWK} key
93+
* @returns {Promise}
94+
*/
95+
async importKey (key) {
96+
let jwk = Object.assign({}, key)
97+
let algorithm = this.params
98+
let usages = key['key_ops'] || []
99+
100+
if (key.use === 'sig') {
101+
usages.push('verify')
102+
}
103+
104+
if (key.use === 'enc') {
105+
// TODO: handle encryption keys
106+
return Promise.resolve(key)
107+
}
108+
109+
if (key.key_ops) {
110+
usages = key.key_ops
111+
}
112+
113+
return crypto.subtle
114+
.importKey('jwk', jwk, algorithm, true, usages)
115+
.then(cryptoKey => {
116+
Object.defineProperty(jwk, 'cryptoKey', {
117+
enumerable: false,
118+
value: cryptoKey
119+
})
120+
121+
return jwk
122+
})
123+
}
124+
}
125+
126+
/**
127+
* Export
128+
*/
129+
module.exports = ECDSA

src/algorithms/index.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
const None = require('./NONE')
55
const HMAC = require('./HMAC')
66
const RSASSA_PKCS1_v1_5 = require('./RSASSA-PKCS1-v1_5')
7+
const ECDSA = require('./ECDSA')
78
const SupportedAlgorithms = require('./SupportedAlgorithms')
89

910
/**
@@ -55,7 +56,14 @@ supportedAlgorithms.define('RS512', 'sign', new RSASSA_PKCS1_v1_5({
5556
name: 'SHA-512'
5657
}
5758
}))
58-
//supportedAlgorithms.define('ES256', 'sign', {})
59+
60+
supportedAlgorithms.define('ES256', 'sign', new ECDSA({
61+
name: 'ECDSA',
62+
hash: {
63+
name: 'SHA-256'
64+
},
65+
namedCurve: 'P-256'
66+
}))
5967
//supportedAlgorithms.define('ES384', 'sign', {})
6068
//supportedAlgorithms.define('ES512', 'sign', {})
6169
//supportedAlgorithms.define('PS256', 'sign', {})
@@ -110,7 +118,14 @@ supportedAlgorithms.define('RS512', 'verify', new RSASSA_PKCS1_v1_5({
110118
name: 'SHA-512'
111119
}
112120
}))
113-
//supportedAlgorithms.define('ES256', 'verify', {})
121+
122+
supportedAlgorithms.define('ES256', 'verify', new ECDSA({
123+
name: 'ECDSA',
124+
hash: {
125+
name: 'SHA-256'
126+
},
127+
namedCurve: 'P-256'
128+
}))
114129
//supportedAlgorithms.define('ES384', 'verify', {})
115130
//supportedAlgorithms.define('ES512', 'verify', {})
116131
//supportedAlgorithms.define('PS256', 'verify', {})
@@ -142,6 +157,14 @@ supportedAlgorithms.define('RS512', 'importKey', new RSASSA_PKCS1_v1_5({
142157
}
143158
}))
144159

160+
supportedAlgorithms.define('ES256', 'importKey', new ECDSA({
161+
name: 'ECDSA',
162+
hash: {
163+
name: 'SHA-256'
164+
},
165+
namedCurve: 'P-256'
166+
}))
167+
145168
/**
146169
* Export
147170
*/

test/algorithms/ECDSASpec.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
'use strict'
2+
3+
/**
4+
* Test dependencies
5+
*/
6+
const chai = require('chai')
7+
8+
/**
9+
* Assertions
10+
*/
11+
chai.should()
12+
let expect = chai.expect
13+
14+
/**
15+
* Code under test
16+
*/
17+
const ECDSA = require('../../src/algorithms/ECDSA')
18+
const {TextEncoder} = require('@sinonjs/text-encoding')
19+
const crypto = require('isomorphic-webcrypto')
20+
const base64url = require('base64url')
21+
const { getPublicKey, getPrivateKey } = require('../keys/ES256')
22+
const { should } = require('chai')
23+
24+
/**
25+
* Reused test constants
26+
*/
27+
const alg = { name: 'ECDSA', hash: { name: 'SHA-256' }, namedCurve: "P-256" }
28+
29+
const data = 'signed with Chrome generated webcrypto key'
30+
31+
/**
32+
* Tests
33+
*/
34+
describe('ECDSA', () => {
35+
36+
/**
37+
* constructor
38+
*/
39+
describe('constructor', () => {
40+
it('should set params', () => {
41+
let alg = { name: 'ECDSA' }
42+
let ecdsa = new ECDSA(alg)
43+
ecdsa.params.should.equal(alg)
44+
})
45+
})
46+
47+
/**
48+
* sign
49+
*/
50+
describe('sign', () => {
51+
let importedEcdsaPrivateKey, importedEcdsaPublicKey
52+
53+
before(async () => {
54+
importedEcdsaPrivateKey = await getPrivateKey()
55+
importedEcdsaPublicKey = await getPublicKey()
56+
})
57+
58+
it('should return a promise', () => {
59+
let ecdsa = new ECDSA(alg)
60+
return ecdsa.sign(importedEcdsaPrivateKey, data).should.be.instanceof(Promise)
61+
})
62+
63+
it('should reject an insufficient key length')
64+
65+
it('should resolve a base64url encoded value and verification should pass', async () => {
66+
const ecdsa = new ECDSA(alg)
67+
const signature = await ecdsa.sign(importedEcdsaPrivateKey, data)
68+
69+
// this will fail if signature is anything but base64 string
70+
expect(base64url(base64url.toBuffer(signature))).to.equal(signature)
71+
72+
// ECDSA is non-deterministic. Therefore we test that the generated signature gets verified with crypto library
73+
const verified = await crypto.subtle.verify(
74+
{
75+
name: 'ECDSA',
76+
hash: { name: 'SHA-256' },
77+
namedCurve: "P-256"
78+
},
79+
importedEcdsaPublicKey,
80+
base64url.toBuffer(signature),
81+
new TextEncoder().encode(data)
82+
)
83+
84+
verified.should.eql(true)
85+
})
86+
})
87+
88+
/**
89+
* verify
90+
*/
91+
describe('verify', () => {
92+
let importedEcdsaPublicKey, signature
93+
94+
before(async () => {
95+
/**
96+
* This signature was produced in Chromium
97+
* deails can be found in comments of ../keys/ES256.js
98+
*/
99+
signature = 'AUNkOnr//z999flIoTMebaf5EQC56WVQizK3GXW/u4EOQBvs9CtvfgWi0pQ3bi0k8p357ajtNvN/dJ1Vr8gbYg=='
100+
101+
importedEcdsaPublicKey = await getPublicKey()
102+
})
103+
104+
it('should return a promise', () => {
105+
let ecdsa = new ECDSA(alg)
106+
ecdsa.verify(importedEcdsaPublicKey, signature, data).should.be.instanceof(Promise)
107+
})
108+
109+
it('should resolve to true', async () => {
110+
const ecdsa = new ECDSA(alg)
111+
const verified = await ecdsa.verify(importedEcdsaPublicKey, signature, data)
112+
expect(verified).to.equal(true)
113+
})
114+
})
115+
116+
/**
117+
* importKey
118+
*/
119+
describe('importKey', () => {
120+
let exampleJwkPublicKey
121+
122+
before(() => {
123+
exampleJwkPublicKey = {
124+
crv: 'P-256',
125+
kty: 'EC',
126+
x: '-c0_z0ly3xRDR0XQuvIirfgal59hq7BzF9ObdUXrgmI',
127+
y: '_7QdnDYKrkrkYaCqZko0ebDQ1L1RpHLtzg8YwdT79n8',
128+
alg: 'ES256'
129+
}
130+
})
131+
132+
it('should successfully import public jwk key', async () => {
133+
const ecdsa = new ECDSA(alg)
134+
const key = await ecdsa.importKey(exampleJwkPublicKey)
135+
key.cryptoKey.constructor.name.should.equal('CryptoKey')
136+
})
137+
})
138+
})
139+

0 commit comments

Comments
 (0)