Skip to content

Commit 8e33d7d

Browse files
authored
fix: browser encrypt signature encoding (#157)
resolves: #154 DER encoding is always signed. Browsers use 'raw' encoding. Where the r and s values are just concatenated. If the first bit of r or s is 1, an additional 0 byte should be added. The current implementation of raw2der does not do this. Tests and test vectors. The basic solution was to let the asn1 library handle the encoding and not pass bytes.
1 parent d7b5e73 commit 8e33d7d

File tree

2 files changed

+165
-4
lines changed

2 files changed

+165
-4
lines changed

modules/serialize/src/ecdsa_signature.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,21 @@ export function raw2der (rawSignature: Uint8Array, { signatureCurve }: WebCrypto
9090
/* Precondition: The total raw signature length is twice the key length bytes. */
9191
needs(byteLength === 2 * _keyLengthBytes, 'Malformed signature.')
9292

93-
// A little more portable than Buffer.from, but not much
94-
const r = new asn.bignum.BN(rawSignature.slice(0, _keyLengthBytes)).toArrayLike(Buffer)
95-
const s = new asn.bignum.BN(rawSignature.slice(_keyLengthBytes)).toArrayLike(Buffer)
93+
/* A little more portable than Buffer.from, but not much.
94+
* DER encoding stores integers as signed values.
95+
* This means if the first bit is a 1,
96+
* the value will be interpreted as negative.
97+
* So an extra byte needs to be added on.
98+
* This is a problem because "raw" encoding is just r|s.
99+
* Without this "extra logic" a given DER signature `sig` *may*
100+
* raw2der(der2raw(sig)) !== sig
101+
* see: https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf 8.3
102+
* All of this means that s and r **MUST** be passed as BN,
103+
* and NOT bytes.
104+
* Otherwise you need to interpret this padding yourself.
105+
*/
106+
const r = new asn.bignum.BN(rawSignature.slice(0, _keyLengthBytes))
107+
const s = new asn.bignum.BN(rawSignature.slice(_keyLengthBytes))
96108

97109
return ECDSASignature.encode({ r, s }, 'der')
98110
}

modules/serialize/test/ecdsa_signatures.test.ts

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,85 @@ import 'mocha'
2020
import { der2raw, raw2der } from '../src/ecdsa_signature'
2121
import { WebCryptoAlgorithmSuite, AlgorithmSuiteIdentifier } from '@aws-crypto/material-management'
2222

23-
/* Data for both verification examples
23+
/*
24+
* This turns out the be very tricky.
25+
* Test vectors are the path to your success.
26+
* In case you need to create more,
27+
* I am reproducing the parts needed to copy/paste yourself to victory.
28+
*
29+
* DER encoding stores integers as signed values.
30+
* This means if the first bit is a 1,
31+
* the value will be interpreted as negative.
32+
* So an extra byte needs to be added on.
33+
* This is a problem because "raw" encoding is just r|s.
34+
* Without this "extra logic" a given DER signature `sig` *may*
35+
* raw2der(der2raw(sig)) !== sig
36+
* see: https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf 8.3
37+
38+
# Data for all verification examples
39+
```
2440
const dataToSign = new Uint8Array([1,2,3,4])
41+
```
42+
43+
# Generating a key
44+
45+
## For browsers:
46+
```
47+
const { publicKey, privateKey } = await window.crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-384' }, true, ['sign', 'verify'])
48+
// Set variables so you can import
49+
const publicKeyBytes = await window.crypto.subtle.exportKey('spki', publicKey)
50+
const privateKeyBytes = await window.crypto.subtle.exportKey('pkcs8', privateKey)
51+
```
52+
53+
## For node.js (v12)
54+
```
55+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {namedCurve: 'secp384r1' })
56+
// Set variables so you can import
57+
const publicKeyBytes = publicKey.export({ type: 'spki', format: 'der' })
58+
const privateKeyBytes = privateKey.export({ type: 'pkcs8', format: 'der' })
59+
```
60+
61+
### Helpful REPL lines to transfer data :)
62+
```
63+
'const publicKeyBytes = new Uint8Array(' + JSON.stringify([...new Uint8Array(publicKeyBytes)]) + ')'
64+
'const privateKeyBytes = new Uint8Array(' + JSON.stringify([...new Uint8Array(privateKeyBytes)]) + ')'
65+
```
66+
67+
# Import keys from a different environment
68+
69+
## For browsers, from node.js
70+
```
71+
const publicKey = await window.crypto.subtle.importKey('spki', publicKeyBytes, { name: 'ECDSA', namedCurve: 'P-384' }, true, ['verify'])
72+
const privateKey = await window.crypto.subtle.importKey('pkcs8', privateKeyBytes, { name: 'ECDSA', namedCurve: 'P-384' }, true, ['sign'])
73+
```
74+
75+
## For node.js (v12) from browsers
76+
```
77+
const publicKey = crypto.createPublicKey({key: publicKeyBytes, format: 'der', type: 'spki'})
78+
const privateKey = crypto.createPrivateKey({key: privateKeyBytes, format: 'der', type: 'pkcs8'})
79+
```
80+
81+
# Sign the data
82+
## For browsers:
83+
```
84+
const signature = await window.crypto.subtle.sign({ name: 'ECDSA', hash: { name: 'SHA-384' } }, privateKey, dataToSign)
85+
```
86+
87+
## For node.js v12
88+
```
89+
const signature = crypto.createSign('sha384').update(dataToSign).sign(privateKey)
90+
```
91+
92+
# Verify the signature
93+
## For browsers:
94+
```
95+
const verify = await window.crypto.subtle.verify({ name: 'ECDSA', hash: { name: 'SHA-384' } }, publicKey, signature, dataToSign)
96+
```
97+
98+
## For node.js v12
99+
```
100+
const isVerified = crypto.createVerify('sha384').update(dataToSign).verify(publicKey, signature)
101+
```
25102
*/
26103

27104
/* Browser verification
@@ -50,6 +127,58 @@ const validSuite = new WebCryptoAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES1
50127
const rawSignature = new Uint8Array([22, 77, 187, 192, 175, 104, 2, 240, 55, 2, 6, 138, 103, 148, 214, 240, 244, 65, 224, 254, 60, 52, 218, 22, 250, 245, 216, 228, 151, 151, 220, 234, 125, 9, 97, 8, 132, 123, 79, 193, 216, 207, 214, 0, 73, 183, 149, 173, 26, 173, 251, 132, 140, 139, 44, 122, 11, 50, 163, 105, 138, 221, 223, 29])
51128
const derSignature = new Uint8Array([48, 68, 2, 32, 22, 77, 187, 192, 175, 104, 2, 240, 55, 2, 6, 138, 103, 148, 214, 240, 244, 65, 224, 254, 60, 52, 218, 22, 250, 245, 216, 228, 151, 151, 220, 234, 2, 32, 125, 9, 97, 8, 132, 123, 79, 193, 216, 207, 214, 0, 73, 183, 149, 173, 26, 173, 251, 132, 140, 139, 44, 122, 11, 50, 163, 105, 138, 221, 223, 29])
52129

130+
/*
131+
// All signatures should be verified with this public key (spki in bytes)
132+
const publicKeyBytes = new Uint8Array([48,118,48,16,6,7,42,134,72,206,61,2,1,6,5,43,129,4,0,34,3,98,0,4,182,142,16,181,73,22,77,38,171,216,20,142,20,154,218,20,31,70,13,88,242,169,247,248,184,238,221,100,191,26,82,176,137,96,163,242,244,215,25,250,144,157,65,246,201,220,219,188,122,115,129,227,74,201,236,240,93,173,108,36,49,249,149,224,67,76,66,192,255,173,90,184,124,191,154,165,137,251,173,181,109,109,75,167,34,202,198,114,192,197,215,224,199,45,105,126])
133+
*/
134+
135+
/*
136+
The following may be useful along with the above code to create additional test vectors.
137+
```
138+
function test() {
139+
let i = 100
140+
while (i--) {
141+
const sig = crypto.createSign('sha384').update(dataToSign).sign(privateKey)
142+
if ((sig[4] === 0 ? sig[56] : sig[55]) === 128) return sig
143+
if (sig[5] === 128) return sig
144+
}
145+
}
146+
```
147+
*/
148+
149+
/* DER signatures for SHA384_ECDSA_P384 that exhibit different lengths,
150+
* and padding of r and s.
151+
*/
152+
// length == 102
153+
const derSigNoPadding = new Uint8Array([48, 100, 2, 48, 125, 32, 154, 168, 112, 11, 187, 171, 135, 119, 83, 66, 69, 164, 226, 80, 39, 176, 112, 210, 72, 159, 201, 242, 110, 212, 158, 170, 99, 155, 80, 29, 99, 77, 158, 81, 170, 46, 116, 246, 137, 197, 82, 112, 70, 36, 196, 49, 2, 48, 117, 43, 254, 192, 131, 207, 80, 1, 152, 238, 154, 139, 42, 81, 244, 230, 42, 114, 137, 98, 127, 86, 166, 26, 172, 80, 132, 251, 97, 249, 4, 39, 47, 250, 132, 44, 187, 235, 197, 157, 56, 216, 39, 130, 69, 46, 185, 150])
154+
const rawSigNoPadding = new Uint8Array([125, 32, 154, 168, 112, 11, 187, 171, 135, 119, 83, 66, 69, 164, 226, 80, 39, 176, 112, 210, 72, 159, 201, 242, 110, 212, 158, 170, 99, 155, 80, 29, 99, 77, 158, 81, 170, 46, 116, 246, 137, 197, 82, 112, 70, 36, 196, 49, 117, 43, 254, 192, 131, 207, 80, 1, 152, 238, 154, 139, 42, 81, 244, 230, 42, 114, 137, 98, 127, 86, 166, 26, 172, 80, 132, 251, 97, 249, 4, 39, 47, 250, 132, 44, 187, 235, 197, 157, 56, 216, 39, 130, 69, 46, 185, 150])
155+
156+
// length == 103, r is padded
157+
const derSigRPadded = new Uint8Array([48, 101, 2, 49, 0, 163, 81, 253, 131, 61, 166, 239, 242, 68, 133, 70, 219, 243, 67, 220, 94, 57, 115, 92, 119, 17, 93, 152, 78, 78, 177, 110, 48, 164, 12, 53, 146, 223, 8, 57, 108, 177, 237, 187, 165, 39, 162, 214, 193, 112, 220, 132, 13, 2, 48, 10, 2, 53, 95, 195, 68, 6, 79, 110, 220, 215, 130, 204, 182, 125, 44, 47, 198, 226, 17, 115, 207, 22, 89, 113, 18, 90, 63, 0, 105, 104, 221, 159, 156, 17, 168, 95, 96, 254, 88, 45, 120, 223, 180, 12, 44, 118, 18])
158+
const rawSigRPadded = new Uint8Array([163, 81, 253, 131, 61, 166, 239, 242, 68, 133, 70, 219, 243, 67, 220, 94, 57, 115, 92, 119, 17, 93, 152, 78, 78, 177, 110, 48, 164, 12, 53, 146, 223, 8, 57, 108, 177, 237, 187, 165, 39, 162, 214, 193, 112, 220, 132, 13, 10, 2, 53, 95, 195, 68, 6, 79, 110, 220, 215, 130, 204, 182, 125, 44, 47, 198, 226, 17, 115, 207, 22, 89, 113, 18, 90, 63, 0, 105, 104, 221, 159, 156, 17, 168, 95, 96, 254, 88, 45, 120, 223, 180, 12, 44, 118, 18])
159+
160+
// length == 103, s is padded
161+
const derSigSPadded = new Uint8Array([48, 101, 2, 48, 13, 237, 65, 195, 0, 118, 121, 114, 12, 187, 102, 24, 62, 8, 42, 43, 27, 18, 27, 123, 222, 46, 84, 53, 255, 198, 169, 180, 206, 77, 60, 3, 171, 209, 129, 25, 245, 157, 197, 128, 191, 153, 226, 52, 170, 3, 93, 180, 2, 49, 0, 191, 191, 7, 215, 111, 31, 5, 75, 245, 134, 50, 255, 118, 224, 243, 133, 233, 162, 55, 22, 203, 124, 69, 231, 1, 190, 191, 175, 158, 82, 80, 168, 172, 29, 97, 13, 141, 126, 184, 238, 159, 214, 213, 92, 114, 94, 61, 82])
162+
const rawSigSPadded = new Uint8Array([13, 237, 65, 195, 0, 118, 121, 114, 12, 187, 102, 24, 62, 8, 42, 43, 27, 18, 27, 123, 222, 46, 84, 53, 255, 198, 169, 180, 206, 77, 60, 3, 171, 209, 129, 25, 245, 157, 197, 128, 191, 153, 226, 52, 170, 3, 93, 180, 191, 191, 7, 215, 111, 31, 5, 75, 245, 134, 50, 255, 118, 224, 243, 133, 233, 162, 55, 22, 203, 124, 69, 231, 1, 190, 191, 175, 158, 82, 80, 168, 172, 29, 97, 13, 141, 126, 184, 238, 159, 214, 213, 92, 114, 94, 61, 82])
163+
164+
// length == 104 both r and s are padded
165+
const derSigBothSandRPadded = new Uint8Array([48, 102, 2, 49, 0, 161, 31, 228, 163, 249, 149, 236, 238, 15, 140, 163, 28, 152, 199, 168, 83, 187, 60, 79, 26, 71, 243, 120, 0, 44, 200, 217, 82, 162, 181, 168, 194, 181, 56, 20, 193, 213, 40, 112, 59, 13, 254, 55, 177, 231, 189, 128, 71, 2, 49, 0, 241, 232, 224, 60, 113, 203, 248, 143, 34, 63, 98, 221, 156, 143, 58, 106, 169, 169, 63, 126, 103, 145, 63, 246, 255, 32, 74, 11, 197, 255, 13, 244, 105, 188, 157, 210, 200, 36, 140, 218, 1, 115, 99, 255, 212, 71, 156, 38])
166+
const rawSigBothSandRPadded = new Uint8Array([161, 31, 228, 163, 249, 149, 236, 238, 15, 140, 163, 28, 152, 199, 168, 83, 187, 60, 79, 26, 71, 243, 120, 0, 44, 200, 217, 82, 162, 181, 168, 194, 181, 56, 20, 193, 213, 40, 112, 59, 13, 254, 55, 177, 231, 189, 128, 71, 241, 232, 224, 60, 113, 203, 248, 143, 34, 63, 98, 221, 156, 143, 58, 106, 169, 169, 63, 126, 103, 145, 63, 246, 255, 32, 74, 11, 197, 255, 13, 244, 105, 188, 157, 210, 200, 36, 140, 218, 1, 115, 99, 255, 212, 71, 156, 38])
167+
168+
/* This vector has the "first byte" of r === 128.
169+
* This means that the "first bit" of r === 1.
170+
* This means the DER signature is padded.
171+
*/
172+
const derSigRonBoundary = new Uint8Array([48, 102, 2, 49, 0, 128, 193, 160, 46, 142, 254, 87, 100, 216, 114, 75, 154, 209, 17, 184, 155, 141, 178, 118, 99, 34, 161, 229, 195, 144, 1, 183, 41, 165, 115, 107, 123, 234, 39, 90, 43, 247, 108, 227, 88, 144, 107, 230, 39, 103, 213, 174, 206, 2, 49, 0, 209, 70, 36, 78, 124, 248, 10, 77, 80, 102, 88, 38, 166, 138, 237, 192, 93, 189, 17, 157, 57, 203, 245, 93, 178, 19, 206, 31, 13, 117, 4, 241, 176, 107, 169, 23, 39, 71, 127, 32, 210, 157, 82, 115, 163, 177, 42, 74])
173+
const rawSigRonBoundary = new Uint8Array([128, 193, 160, 46, 142, 254, 87, 100, 216, 114, 75, 154, 209, 17, 184, 155, 141, 178, 118, 99, 34, 161, 229, 195, 144, 1, 183, 41, 165, 115, 107, 123, 234, 39, 90, 43, 247, 108, 227, 88, 144, 107, 230, 39, 103, 213, 174, 206, 209, 70, 36, 78, 124, 248, 10, 77, 80, 102, 88, 38, 166, 138, 237, 192, 93, 189, 17, 157, 57, 203, 245, 93, 178, 19, 206, 31, 13, 117, 4, 241, 176, 107, 169, 23, 39, 71, 127, 32, 210, 157, 82, 115, 163, 177, 42, 74])
174+
175+
/* This vector has the "first byte" of s === 128.
176+
* This means that the "first bit" of s === 1.
177+
* This means the DER signature is padded.
178+
*/
179+
const derSigSonBoundary = new Uint8Array([48, 101, 2, 48, 99, 9, 32, 95, 74, 230, 183, 174, 87, 124, 144, 130, 171, 98, 39, 162, 23, 207, 58, 218, 73, 183, 190, 173, 107, 46, 130, 60, 185, 45, 245, 81, 57, 191, 60, 41, 6, 8, 68, 241, 221, 25, 122, 145, 25, 229, 148, 158, 2, 49, 0, 128, 50, 250, 23, 18, 25, 233, 203, 214, 199, 87, 201, 51, 187, 231, 99, 99, 114, 101, 252, 197, 48, 94, 2, 1, 12, 154, 225, 237, 112, 63, 95, 149, 14, 159, 190, 177, 241, 121, 75, 133, 77, 148, 78, 11, 34, 215, 58])
180+
const rawSigSonBoundary = new Uint8Array([99, 9, 32, 95, 74, 230, 183, 174, 87, 124, 144, 130, 171, 98, 39, 162, 23, 207, 58, 218, 73, 183, 190, 173, 107, 46, 130, 60, 185, 45, 245, 81, 57, 191, 60, 41, 6, 8, 68, 241, 221, 25, 122, 145, 25, 229, 148, 158, 128, 50, 250, 23, 18, 25, 233, 203, 214, 199, 87, 201, 51, 187, 231, 99, 99, 114, 101, 252, 197, 48, 94, 2, 1, 12, 154, 225, 237, 112, 63, 95, 149, 14, 159, 190, 177, 241, 121, 75, 133, 77, 148, 78, 11, 34, 215, 58])
181+
53182
describe('der2raw', () => {
54183
it('transform to raw signature', () => {
55184
const signature = der2raw(derSignature, validSuite)
@@ -72,4 +201,24 @@ describe('raw2der', () => {
72201
const suite = new WebCryptoAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16)
73202
expect(() => raw2der(rawSignature, suite)).to.throw()
74203
})
204+
const suite = new WebCryptoAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384)
205+
206+
it('transform to der signature with no padding', () => {
207+
expect(raw2der(rawSigNoPadding, suite)).to.deep.equal(derSigNoPadding)
208+
})
209+
it('transform to der signature with r padded', () => {
210+
expect(raw2der(rawSigRPadded, suite)).to.deep.equal(derSigRPadded)
211+
})
212+
it('transform to der signature with s padded', () => {
213+
expect(raw2der(rawSigSPadded, suite)).to.deep.equal(derSigSPadded)
214+
})
215+
it('transform to der signature with both r and s padded', () => {
216+
expect(raw2der(rawSigBothSandRPadded, suite)).to.deep.equal(derSigBothSandRPadded)
217+
})
218+
it('transform to der signature with with r padded, but r is on the padding boundary', () => {
219+
expect(raw2der(rawSigRonBoundary, suite)).to.deep.equal(derSigRonBoundary)
220+
})
221+
it('transform to der signature with s padded, but s is on the padding boundary', () => {
222+
expect(raw2der(rawSigSonBoundary, suite)).to.deep.equal(derSigSonBoundary)
223+
})
75224
})

0 commit comments

Comments
 (0)