Skip to content

Commit bd98f0b

Browse files
committed
crx-verify: add ecdsa support
1 parent f915376 commit bd98f0b

File tree

5 files changed

+73
-57
lines changed

5 files changed

+73
-57
lines changed

README.md

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,20 @@ The util is intentionally bare bone: it doesn't generate a private key
1313
for you (use openssl for that) & it doesn't compress your directory
1414
(use zip for that).
1515

16-
Print info:
16+
Print info for a .crx downloaded from the Chome Web Store:
1717

1818
~~~
1919
$ crx3-info < file.crx
20-
id nofnjfackdchgipjnedfdcmhldeejknj
21-
header 593
22-
payload 5581
23-
sha256_with_rsa 1
24-
sha256_with_ecdsa 0
20+
id ckbpebiaofifhmkecjijobfafcfngfkj
21+
header 1322
22+
payload 8233
23+
sha256_with_rsa 2 main_idx=1
24+
sha256_with_ecdsa 1
2525
~~~
2626

27+
(`main_idx` is the index of AsymmetricKeyProof that contains a public
28+
key from which the id was derived during the .crx creation.)
29+
2730
Extract zip:
2831

2932
~~~
@@ -36,19 +39,19 @@ Extract the 1st rsa public key:
3639
~~~
3740
$ crx3-info rsa 0 < file.crx
3841
-----BEGIN PUBLIC KEY-----
39-
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmxbf7fCk1V+XL9prvqpx
40-
BrI2JA6P5Y7rqABIC3PB8K2lJwo0NokwKwHxpC6yRQelvnFUsODrRjrEYMPeImgp
41-
skE9Tn36aRzPGFXAPxwRUzcjAPBYzSL6/ieNSmyMxkFXA/1+QS5ofm/IaLlOpe2V
42-
AQF6NRpsGWfkKHy09guMRNzcy7MXDbmPBMynPU+td2GHaql/6E9lN7Zk5KLpRLVJ
43-
WgAcrJsG8XORVMNTaOBpgcrEwGPBBdRG1A+foHNRQLNPE1dMLbgJcPWVbSCJsm1e
44-
I/blb/ULUw+jvgFlSxZblWHiiGoSdI0vLfJpxjAvIeqRnLW2EZuTvJaaKkYtJW8q
45-
9QIDAQAB
42+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj/u/XDdjlDyw7gHEtaaa
43+
sZ9GdG8WOKAyJzXd8HFrDtz2Jcuy7er7MtWvHgNDA0bwpznbI5YdZeV4UfCEsA4S
44+
rA5b3MnWTHwA1bgbiDM+L9rrqvcadcKuOlTeN48Q0ijmhHlNFbTzvT9W0zw/GKv8
45+
LgXAHggxtmHQ/Z9PP2QNF5O8rUHHSL4AJ6hNcEKSBVSmbbjeVm4gSXDuED5r0nwx
46+
vRtupDxGYp8IZpP5KlExqNu1nbkPc+igCTIB6XsqijagzxewUHCdovmkb2JNtskx
47+
/PMIEv+TvWIx2BzqGp71gSh/dV7SJ3rClvWd2xj8dtxG8FfAWDTIIi0qZXWn2Qhi
48+
zQIDAQAB
4649
-----END PUBLIC KEY-----
4750
~~~
4851

4952
Validate (returns 0 on success):
5053

51-
$ crx3-verify rsa public.pem < file.crx
54+
$ crx3-verify rsa 0 public.pem < file.crx
5255

5356
## License
5457

crx3-info

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,18 @@ function main() {
1515
}
1616

1717
function public_key_extract(hdr, proof, index) {
18-
let type = { 'rsa': 'sha256_with_rsa', 'ec': 'sha256_with_ecdsa' }
19-
let container = type[proof]
20-
if (!container) throw new Error(`no support for ${proof}`)
21-
if (!hdr[container][index]) throw new Error('invalid index')
22-
return crx.der2pem(hdr[container][index].public_key)
18+
return crx.der2pem(crx.container(hdr, proof, index).public_key)
2319
}
2420

2521
function dump(hdr) {
2622
return [
27-
["id", mpdecimal(hdr.signed_header_data.crx_id.toString('hex'))],
23+
["id", crx.mpdecimal(hdr.signed_header_data.crx_id)],
2824
["header", hdr.header_total_len],
29-
["payload", hdr.payload_len],
30-
["sha256_with_rsa", hdr.sha256_with_rsa.length],
25+
["payload", hdr.payload.length],
26+
["sha256_with_rsa",
27+
`${hdr.sha256_with_rsa.length} main_idx=${crx.rsa_main_index(hdr)}`],
3128
["sha256_with_ecdsa", hdr.sha256_with_ecdsa.length]
3229
].map( ([k,v]) => k.padEnd(20, ' ') + ' ' + v).join`\n` + "\n"
3330
}
3431

35-
/* https://stackoverflow.com/a/2050916/81081
36-
37-
'the encoding uses a-p instead of 0-9a-f. The reason is that
38-
leading numeric characters in the host field of an origin can wind
39-
up being treated as potential IP addresses by Chrome. We refer to
40-
it internally as "mpdecimal" after the guy who came up with it.' */
41-
function mpdecimal(hex) {
42-
let a = 'a'.charCodeAt(0)
43-
return hex.split('')
44-
.map( v => String.fromCharCode((parseInt(v, 16)+a))).join``
45-
}
46-
4732
main()

crx3-verify

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
#!/usr/bin/env node
22
'use strict';
33

4+
let crypto = require('crypto')
45
let crx = require('./index')
56
let u = crx.u
67
let argv = process.argv
78

89
function main() {
9-
if (argv.length !== 2+2)
10-
u.err(`Usage: ${u.progname()} rsa public.pem < file.zip`)
10+
if (argv.length !== 2+3)
11+
u.err(`Usage: ${u.progname()} rsa|ec index public.pem < file.zip`)
1112

1213
u.read().then(crx.parse).then(verify).catch(u.err)
1314
}
1415

1516
function verify(hdr) {
16-
if (argv[2] !== 'rsa') throw new Error(`no support for ${argv[2]}`)
17+
let container = crx.container(hdr, argv[2], argv[3])
1718

18-
return u.read(argv[3]).then(crx.pem2der).then( key => {
19-
if (!crx.verify_rsa(key, hdr.signed_header_data.crx_id))
20-
process.exitCode = 1
19+
return u.read(argv[4]).then( key => {
20+
let main_idx = crx.rsa_main_index(hdr)
21+
let main_public_key = hdr.sha256_with_rsa[main_idx].public_key
22+
23+
let maker = new crx.Maker({public_der: main_public_key}, hdr.payload)
24+
let v = maker.signature_data_update(crypto.createVerify('sha256')).end()
25+
if (!v.verify(key, container.signature)) process.exitCode = 1
2126
})
2227
}
2328

index.js

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ exports.parse = function(buf) {
3939
let crx_file_header = parse_header(header)
4040
return Object.assign({
4141
header_total_len: header.length + meta,
42-
payload_len: buf.length - header.length - meta
42+
payload: buf.slice(header.length + meta)
4343
}, crx_file_header)
4444
}
4545

@@ -52,12 +52,6 @@ function parse_header(buf) {
5252
return hdr
5353
}
5454

55-
// the first 128 bits of the sha256 hash of the public key must be == crx id
56-
exports.verify_rsa = function(public_key, id) {
57-
let a = crypto.createHash('sha256').update(public_key).digest().slice(0, 16)
58-
return a.equals(id)
59-
}
60-
6155
exports.pem2der = function(buf) {
6256
return crypto.createPublicKey(buf).export({type: 'spki', format: 'der'})
6357
}
@@ -80,10 +74,7 @@ exports.Maker = class {
8074
this.payload = payload
8175
}
8276

83-
id() {
84-
return crypto.createHash('sha256')
85-
.update(this.key.public_der).digest().slice(0, 16)
86-
}
77+
id() { return exports.crx_id(this.key.public_der) }
8778

8879
signed_data() {
8980
let pb
@@ -95,13 +86,16 @@ exports.Maker = class {
9586
}
9687

9788
sign() {
89+
return this.signature_data_update(crypto.createSign('sha256'))
90+
.sign(this.key.private)
91+
}
92+
93+
signature_data_update(signature) {
9894
let magic_str = "CRX3 SignedData\x00"
99-
return crypto.createSign('sha256')
100-
.update(magic_str)
95+
return signature.update(magic_str)
10196
.update(len(this.signed_data()))
10297
.update(this.signed_data())
10398
.update(this.payload)
104-
.sign(this.key.private)
10599
}
106100

107101
header() {
@@ -131,3 +125,32 @@ function len(o) { // 4 bytes, little-endian
131125
buf.writeUInt32LE(o.length, 0)
132126
return buf
133127
}
128+
129+
exports.crx_id = function(public_key) {
130+
return crypto.createHash('sha256').update(public_key).digest().slice(0, 16)
131+
}
132+
133+
/* https://stackoverflow.com/a/2050916/81081
134+
135+
'the encoding uses a-p instead of 0-9a-f. The reason is that
136+
leading numeric characters in the host field of an origin can wind
137+
up being treated as potential IP addresses by Chrome. We refer to
138+
it internally as "mpdecimal" after the guy who came up with it.' */
139+
exports.mpdecimal = function(buf) {
140+
let a = 'a'.charCodeAt(0)
141+
return buf.toString('hex').split('')
142+
.map( v => String.fromCharCode((parseInt(v, 16)+a))).join``
143+
}
144+
145+
exports.rsa_main_index = function(hdr) {
146+
let id = hdr.signed_header_data.crx_id
147+
return hdr.sha256_with_rsa
148+
.findIndex( proof => id.equals(exports.crx_id(proof.public_key)))
149+
}
150+
151+
exports.container = function(hdr, proof, index) {
152+
let type = { 'rsa': 'sha256_with_rsa', 'ec': 'sha256_with_ecdsa' }
153+
let ctr = type[proof]; if (!ctr) throw new Error(`no support for ${proof}`)
154+
ctr = hdr[ctr][index]; if (!ctr) throw new Error(`invalid index`)
155+
return ctr
156+
}

test/test_smoke.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ suite('cli', function() {
3030
`id efhdamkdoffheefhloplkcfimcfkjgip
3131
header 593
3232
payload 231
33-
sha256_with_rsa 1
33+
sha256_with_rsa 1 main_idx=0
3434
sha256_with_ecdsa 0
3535
`)
3636
})
@@ -55,11 +55,11 @@ bQIDAQAB
5555
{input: fs.readFileSync(tmp('foo.crx'))})
5656
fs.writeFileSync(tmp('foo.public_key.pem'), r.stdout)
5757

58-
r = sh('../crx3-verify', ['rsa', tmp('foo.public_key.pem')],
58+
r = sh('../crx3-verify', ['rsa', '0', tmp('foo.public_key.pem')],
5959
{input: fs.readFileSync(tmp('foo.crx'))})
6060
assert(r.status === 0)
6161

62-
r = sh('../crx3-verify', ['rsa', 'alien_public.pem'],
62+
r = sh('../crx3-verify', ['rsa', '0', 'alien_public.pem'],
6363
{input: fs.readFileSync(tmp('foo.crx'))})
6464
assert(r.status !== 0)
6565
})

0 commit comments

Comments
 (0)