Skip to content

Commit 2187f0d

Browse files
authored
Merge pull request #888 from GrapeGreen/js_sign_ecdsa
ECDSA P-256 SHA-256 implementation
2 parents 225cd5b + e91846b commit 2187f0d

11 files changed

+259
-112
lines changed

.github/workflows/build.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ubuntu-latest
1515
strategy:
1616
matrix:
17-
node-version: [12, 14, 16]
17+
node-version: [16, 18, 20]
1818
steps:
1919
- uses: actions/checkout@v2
2020
- uses: actions/setup-node@v3
@@ -28,7 +28,7 @@ jobs:
2828
runs-on: ubuntu-latest
2929
strategy:
3030
matrix:
31-
node-version: [14, 16]
31+
node-version: [16, 18, 20]
3232
steps:
3333
- uses: actions/checkout@v2
3434
- uses: actions/setup-node@v3

js/sign/README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ npm install wbn-sign
1717

1818
## Requirements
1919

20-
This plugin requires Node v14.0.0+.
20+
This plugin requires Node v16.0.0+.
2121

2222
## Usage
2323

@@ -179,6 +179,10 @@ environment variable named `WEB_BUNDLE_SIGNING_PASSPHRASE`.
179179

180180
## Release Notes
181181

182+
### v0.1.3
183+
184+
- Add support for ECDSA P-256 SHA-256 signatures
185+
182186
### v0.1.2
183187

184188
- Add support for calculating the Web Bundle ID with the CLI tool.

js/sign/package-lock.json

+8-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js/sign/package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "wbn-sign",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"description": "Signing tool to sign a web bundle with integrity block",
55
"homepage": "https://github.com/WICG/webpackage/tree/main/js/sign",
66
"main": "./lib/wbn-sign.cjs",
@@ -33,7 +33,8 @@
3333
],
3434
"author": "Sonja Laurila <[email protected]> (https://github.com/sonkkeli)",
3535
"contributors": [
36-
"Christian Flach <[email protected]> (https://github.com/cmfcmf)"
36+
"Christian Flach <[email protected]> (https://github.com/cmfcmf)",
37+
"Andrew Rayskiy <[email protected]> (https://github.com/GrapeGreen)"
3738
],
3839
"license": "W3C-20150513",
3940
"dependencies": {
@@ -43,15 +44,15 @@
4344
"read": "^2.0.0"
4445
},
4546
"devDependencies": {
46-
"@types/node": "^14.0.0",
47+
"@types/node": "^16.0.0",
4748
"esbuild": "^0.14.47",
4849
"jasmine": "^4.2.1",
4950
"mock-stdin": "^1.0.0",
5051
"prettier": "2.8.0",
5152
"typescript": "^4.7.3"
5253
},
5354
"engines": {
54-
"node": ">= 14.0.0",
55+
"node": ">= 16.0.0",
5556
"npm": ">= 8.0.0"
5657
},
5758
"prettier": {

js/sign/src/signers/integrity-block-signer.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import crypto, { KeyObject } from 'crypto';
22
import * as cborg from 'cborg';
3-
import {
4-
ED25519_PK_SIGNATURE_ATTRIBUTE_NAME,
5-
INTEGRITY_BLOCK_MAGIC,
6-
VERSION_B1,
7-
} from '../utils/constants.js';
3+
import { INTEGRITY_BLOCK_MAGIC, VERSION_B1 } from '../utils/constants.js';
84
import { checkDeterministic } from '../cbor/deterministic.js';
9-
import { getRawPublicKey, checkIsValidEd25519Key } from '../utils/utils.js';
5+
import {
6+
getRawPublicKey,
7+
checkIsValidKey,
8+
getPublicKeyAttributeName,
9+
} from '../utils/utils.js';
1010
import { ISigningStrategy } from './signing-strategy-interface.js';
1111

12-
type SignatureAttributeKey = typeof ED25519_PK_SIGNATURE_ATTRIBUTE_NAME;
12+
type SignatureAttributeKey = string;
1313
type SignatureAttributes = { [SignatureAttributeKey: string]: Uint8Array };
1414

1515
type IntegritySignature = {
@@ -29,10 +29,10 @@ export class IntegrityBlockSigner {
2929
}> {
3030
const integrityBlock = this.obtainIntegrityBlock().integrityBlock;
3131
const publicKey = await this.signingStrategy.getPublicKey();
32-
checkIsValidEd25519Key('public', publicKey);
32+
checkIsValidKey('public', publicKey);
3333

3434
const newAttributes: SignatureAttributes = {
35-
[ED25519_PK_SIGNATURE_ATTRIBUTE_NAME]: getRawPublicKey(publicKey),
35+
[getPublicKeyAttributeName(publicKey)]: getRawPublicKey(publicKey),
3636
};
3737

3838
const ibCbor = integrityBlock.toCBOR();
@@ -56,6 +56,7 @@ export class IntegrityBlockSigner {
5656

5757
const signedIbCbor = integrityBlock.toCBOR();
5858
checkDeterministic(signedIbCbor);
59+
5960
return {
6061
integrityBlock: signedIbCbor,
6162
signedWebBundle: new Uint8Array(
@@ -132,6 +133,7 @@ export class IntegrityBlockSigner {
132133
signature: Uint8Array,
133134
publicKey: KeyObject
134135
): void {
136+
// For ECDSA P-256 keys the algorithm is implicitly selected as SHA-256.
135137
const isVerified = crypto.verify(
136138
/*algorithm=*/ undefined,
137139
data,

js/sign/src/signers/node-crypto-signing-strategy.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import crypto, { KeyObject } from 'crypto';
2-
import { checkIsValidEd25519Key } from '../utils/utils.js';
2+
import { checkIsValidKey } from '../utils/utils.js';
33
import { ISigningStrategy } from './signing-strategy-interface.js';
44

55
// Class to be used when signing with parsed `crypto.KeyObject` private key
66
// provided directly in the constructor.
77
export class NodeCryptoSigningStrategy implements ISigningStrategy {
88
constructor(private readonly privateKey: KeyObject) {
9-
checkIsValidEd25519Key('private', privateKey);
9+
checkIsValidKey('private', privateKey);
1010
}
1111

1212
async sign(data: Uint8Array): Promise<Uint8Array> {
13+
// For ECDSA P-256 keys the algorithm is implicitly selected as SHA-256.
1314
return crypto.sign(/*algorithm=*/ undefined, data, this.privateKey);
1415
}
1516

js/sign/src/utils/constants.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1-
export const ED25519_PK_SIGNATURE_ATTRIBUTE_NAME = 'ed25519PublicKey';
1+
export enum SignatureType {
2+
Ed25519,
3+
EcdsaP256SHA256,
4+
}
5+
6+
export const PUBLIC_KEY_ATTRIBUTE_NAME_MAPPING = new Map<SignatureType, string>(
7+
[
8+
[SignatureType.Ed25519, 'ed25519PublicKey'],
9+
[SignatureType.EcdsaP256SHA256, 'ecdsaP256SHA256PublicKey'],
10+
]
11+
);
12+
213
export const INTEGRITY_BLOCK_MAGIC = new Uint8Array([
314
0xf0, 0x9f, 0x96, 0x8b, 0xf0, 0x9f, 0x93, 0xa6,
415
]); // 🖋📦

js/sign/src/utils/utils.ts

+60-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import crypto, { KeyObject } from 'crypto';
22
import read from 'read';
3+
import assert from 'assert';
4+
import {
5+
PUBLIC_KEY_ATTRIBUTE_NAME_MAPPING,
6+
SignatureType,
7+
} from './constants.js';
38

49
// A helper function that can be used to read the passphrase to decrypt a
510
// password-decrypted private key.
@@ -31,15 +36,62 @@ export function parsePemKey(
3136
});
3237
}
3338

34-
export function getRawPublicKey(publicKey: crypto.KeyObject) {
35-
// Currently this is the only way for us to get the raw 32 bytes of the public key.
36-
return new Uint8Array(
37-
publicKey.export({ type: 'spki', format: 'der' }).slice(-32)
39+
function maybeGetSignatureType(key: crypto.KeyObject): SignatureType | null {
40+
switch (key.asymmetricKeyType) {
41+
case 'ed25519':
42+
return SignatureType.Ed25519;
43+
case 'ec':
44+
if (key.asymmetricKeyDetails?.namedCurve === 'prime256v1') {
45+
return SignatureType.EcdsaP256SHA256;
46+
}
47+
break;
48+
default:
49+
break;
50+
}
51+
return null;
52+
}
53+
54+
export function isAsymmetricKeyTypeSupported(key: crypto.KeyObject): boolean {
55+
return maybeGetSignatureType(key) !== null;
56+
}
57+
58+
export function getSignatureType(key: crypto.KeyObject): SignatureType {
59+
const signatureType = maybeGetSignatureType(key);
60+
assert(
61+
signatureType !== null,
62+
'Expected either "Ed25519" or "ECDSA P-256" key.'
3863
);
64+
return signatureType;
65+
}
66+
67+
export function getPublicKeyAttributeName(key: crypto.KeyObject) {
68+
return PUBLIC_KEY_ATTRIBUTE_NAME_MAPPING.get(getSignatureType(key))!;
3969
}
4070

41-
// Throws an error if the key is not a valid Ed25519 key of the specified type.
42-
export function checkIsValidEd25519Key(
71+
export function getRawPublicKey(publicKey: crypto.KeyObject) {
72+
const exportedKey = publicKey.export({ type: 'spki', format: 'der' });
73+
switch (getSignatureType(publicKey)) {
74+
case SignatureType.Ed25519:
75+
// Currently this is the only way for us to get the raw 32 bytes of the public key.
76+
return new Uint8Array(exportedKey.subarray(-32));
77+
case SignatureType.EcdsaP256SHA256: {
78+
// The last 65 bytes are the raw bytes of the ECDSA P-256 public key.
79+
// For the purposes of signing, we'd like to convert it to its compressed form that takes only 33 bytes.
80+
const uncompressedKey = exportedKey.subarray(-65);
81+
const compressedKey = crypto.ECDH.convertKey(
82+
uncompressedKey,
83+
'prime256v1',
84+
/*inputEncoding=*/ undefined,
85+
/*outputEncoding=*/ undefined,
86+
'compressed'
87+
) as Buffer;
88+
return new Uint8Array(compressedKey);
89+
}
90+
}
91+
}
92+
93+
// Throws an error if the key is not a valid Ed25519 or ECDSA P-256 key of the specified type.
94+
export function checkIsValidKey(
4395
expectedKeyType: crypto.KeyObjectType,
4496
key: KeyObject
4597
) {
@@ -49,9 +101,7 @@ export function checkIsValidEd25519Key(
49101
);
50102
}
51103

52-
if (key.asymmetricKeyType !== 'ed25519') {
53-
throw new Error(
54-
`Expected asymmetric key type to be "ed25519", but it was "${key.asymmetricKeyType}".`
55-
);
104+
if (!isAsymmetricKeyTypeSupported(key)) {
105+
throw new Error(`Expected either "Ed25519" or "ECDSA P-256" key.`);
56106
}
57107
}

js/sign/src/web-bundle-id.ts

+20-9
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,44 @@
11
import crypto, { KeyObject } from 'crypto';
22
import base32Encode from 'base32-encode';
3-
import { getRawPublicKey } from './utils/utils.js';
3+
import {
4+
getRawPublicKey,
5+
isAsymmetricKeyTypeSupported,
6+
getSignatureType,
7+
} from './utils/utils.js';
8+
import { SignatureType } from './utils/constants.js';
49

510
// Web Bundle ID is a base32-encoded (without padding) ed25519 public key
611
// transformed to lowercase. More information:
712
// https://github.com/WICG/isolated-web-apps/blob/main/Scheme.md#signed-web-bundle-ids
813
export class WebBundleId {
914
// https://github.com/WICG/isolated-web-apps/blob/main/Scheme.md#suffix
10-
private readonly appIdSuffix = [0x00, 0x01, 0x02];
15+
private readonly TYPE_SUFFIX_MAPPING = new Map<SignatureType, number[]>([
16+
[SignatureType.Ed25519, [0x00, 0x01, 0x02]],
17+
[SignatureType.EcdsaP256SHA256, [0x00, 0x02, 0x02]],
18+
]);
1119
private readonly scheme = 'isolated-app://';
1220
private readonly key: KeyObject;
21+
private readonly typeSuffix: number[];
1322

14-
constructor(ed25519key: KeyObject) {
15-
if (ed25519key.asymmetricKeyType !== 'ed25519') {
23+
constructor(key: KeyObject) {
24+
if (!isAsymmetricKeyTypeSupported(key)) {
1625
throw new Error(
17-
`WebBundleId: Only ed25519 keys are currently supported. Your key's type is ${ed25519key.asymmetricKeyType}.`
26+
`WebBundleId: Only Ed25519 and ECDSA P-256 keys are currently supported.`
1827
);
1928
}
2029

21-
if (ed25519key.type === 'private') {
22-
this.key = crypto.createPublicKey(ed25519key);
30+
if (key.type === 'private') {
31+
this.key = crypto.createPublicKey(key);
2332
} else {
24-
this.key = ed25519key;
33+
this.key = key;
2534
}
35+
36+
this.typeSuffix = this.TYPE_SUFFIX_MAPPING.get(getSignatureType(this.key))!;
2637
}
2738

2839
serialize() {
2940
return base32Encode(
30-
new Uint8Array([...getRawPublicKey(this.key), ...this.appIdSuffix]),
41+
new Uint8Array([...getRawPublicKey(this.key), ...this.typeSuffix]),
3142
'RFC4648',
3243
{ padding: false }
3344
).toLowerCase();

js/sign/telnet.swbn

381 KB
Binary file not shown.

0 commit comments

Comments
 (0)