Skip to content

Commit

Permalink
Merge pull request #888 from GrapeGreen/js_sign_ecdsa
Browse files Browse the repository at this point in the history
ECDSA P-256 SHA-256 implementation
  • Loading branch information
GrapeGreen authored Apr 30, 2024
2 parents 225cd5b + e91846b commit 2187f0d
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 112 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12, 14, 16]
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
Expand All @@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14, 16]
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
Expand Down
6 changes: 5 additions & 1 deletion js/sign/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ npm install wbn-sign

## Requirements

This plugin requires Node v14.0.0+.
This plugin requires Node v16.0.0+.

## Usage

Expand Down Expand Up @@ -179,6 +179,10 @@ environment variable named `WEB_BUNDLE_SIGNING_PASSPHRASE`.

## Release Notes

### v0.1.3

- Add support for ECDSA P-256 SHA-256 signatures

### v0.1.2

- Add support for calculating the Web Bundle ID with the CLI tool.
Expand Down
16 changes: 8 additions & 8 deletions js/sign/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions js/sign/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wbn-sign",
"version": "0.1.2",
"version": "0.1.3",
"description": "Signing tool to sign a web bundle with integrity block",
"homepage": "https://github.com/WICG/webpackage/tree/main/js/sign",
"main": "./lib/wbn-sign.cjs",
Expand Down Expand Up @@ -33,7 +33,8 @@
],
"author": "Sonja Laurila <[email protected]> (https://github.com/sonkkeli)",
"contributors": [
"Christian Flach <[email protected]> (https://github.com/cmfcmf)"
"Christian Flach <[email protected]> (https://github.com/cmfcmf)",
"Andrew Rayskiy <[email protected]> (https://github.com/GrapeGreen)"
],
"license": "W3C-20150513",
"dependencies": {
Expand All @@ -43,15 +44,15 @@
"read": "^2.0.0"
},
"devDependencies": {
"@types/node": "^14.0.0",
"@types/node": "^16.0.0",
"esbuild": "^0.14.47",
"jasmine": "^4.2.1",
"mock-stdin": "^1.0.0",
"prettier": "2.8.0",
"typescript": "^4.7.3"
},
"engines": {
"node": ">= 14.0.0",
"node": ">= 16.0.0",
"npm": ">= 8.0.0"
},
"prettier": {
Expand Down
20 changes: 11 additions & 9 deletions js/sign/src/signers/integrity-block-signer.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import crypto, { KeyObject } from 'crypto';
import * as cborg from 'cborg';
import {
ED25519_PK_SIGNATURE_ATTRIBUTE_NAME,
INTEGRITY_BLOCK_MAGIC,
VERSION_B1,
} from '../utils/constants.js';
import { INTEGRITY_BLOCK_MAGIC, VERSION_B1 } from '../utils/constants.js';
import { checkDeterministic } from '../cbor/deterministic.js';
import { getRawPublicKey, checkIsValidEd25519Key } from '../utils/utils.js';
import {
getRawPublicKey,
checkIsValidKey,
getPublicKeyAttributeName,
} from '../utils/utils.js';
import { ISigningStrategy } from './signing-strategy-interface.js';

type SignatureAttributeKey = typeof ED25519_PK_SIGNATURE_ATTRIBUTE_NAME;
type SignatureAttributeKey = string;
type SignatureAttributes = { [SignatureAttributeKey: string]: Uint8Array };

type IntegritySignature = {
Expand All @@ -29,10 +29,10 @@ export class IntegrityBlockSigner {
}> {
const integrityBlock = this.obtainIntegrityBlock().integrityBlock;
const publicKey = await this.signingStrategy.getPublicKey();
checkIsValidEd25519Key('public', publicKey);
checkIsValidKey('public', publicKey);

const newAttributes: SignatureAttributes = {
[ED25519_PK_SIGNATURE_ATTRIBUTE_NAME]: getRawPublicKey(publicKey),
[getPublicKeyAttributeName(publicKey)]: getRawPublicKey(publicKey),
};

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

const signedIbCbor = integrityBlock.toCBOR();
checkDeterministic(signedIbCbor);

return {
integrityBlock: signedIbCbor,
signedWebBundle: new Uint8Array(
Expand Down Expand Up @@ -132,6 +133,7 @@ export class IntegrityBlockSigner {
signature: Uint8Array,
publicKey: KeyObject
): void {
// For ECDSA P-256 keys the algorithm is implicitly selected as SHA-256.
const isVerified = crypto.verify(
/*algorithm=*/ undefined,
data,
Expand Down
5 changes: 3 additions & 2 deletions js/sign/src/signers/node-crypto-signing-strategy.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import crypto, { KeyObject } from 'crypto';
import { checkIsValidEd25519Key } from '../utils/utils.js';
import { checkIsValidKey } from '../utils/utils.js';
import { ISigningStrategy } from './signing-strategy-interface.js';

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

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

Expand Down
13 changes: 12 additions & 1 deletion js/sign/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
export const ED25519_PK_SIGNATURE_ATTRIBUTE_NAME = 'ed25519PublicKey';
export enum SignatureType {
Ed25519,
EcdsaP256SHA256,
}

export const PUBLIC_KEY_ATTRIBUTE_NAME_MAPPING = new Map<SignatureType, string>(
[
[SignatureType.Ed25519, 'ed25519PublicKey'],
[SignatureType.EcdsaP256SHA256, 'ecdsaP256SHA256PublicKey'],
]
);

export const INTEGRITY_BLOCK_MAGIC = new Uint8Array([
0xf0, 0x9f, 0x96, 0x8b, 0xf0, 0x9f, 0x93, 0xa6,
]); // 🖋📦
Expand Down
70 changes: 60 additions & 10 deletions js/sign/src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import crypto, { KeyObject } from 'crypto';
import read from 'read';
import assert from 'assert';
import {
PUBLIC_KEY_ATTRIBUTE_NAME_MAPPING,
SignatureType,
} from './constants.js';

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

export function getRawPublicKey(publicKey: crypto.KeyObject) {
// Currently this is the only way for us to get the raw 32 bytes of the public key.
return new Uint8Array(
publicKey.export({ type: 'spki', format: 'der' }).slice(-32)
function maybeGetSignatureType(key: crypto.KeyObject): SignatureType | null {
switch (key.asymmetricKeyType) {
case 'ed25519':
return SignatureType.Ed25519;
case 'ec':
if (key.asymmetricKeyDetails?.namedCurve === 'prime256v1') {
return SignatureType.EcdsaP256SHA256;
}
break;
default:
break;
}
return null;
}

export function isAsymmetricKeyTypeSupported(key: crypto.KeyObject): boolean {
return maybeGetSignatureType(key) !== null;
}

export function getSignatureType(key: crypto.KeyObject): SignatureType {
const signatureType = maybeGetSignatureType(key);
assert(
signatureType !== null,
'Expected either "Ed25519" or "ECDSA P-256" key.'
);
return signatureType;
}

export function getPublicKeyAttributeName(key: crypto.KeyObject) {
return PUBLIC_KEY_ATTRIBUTE_NAME_MAPPING.get(getSignatureType(key))!;
}

// Throws an error if the key is not a valid Ed25519 key of the specified type.
export function checkIsValidEd25519Key(
export function getRawPublicKey(publicKey: crypto.KeyObject) {
const exportedKey = publicKey.export({ type: 'spki', format: 'der' });
switch (getSignatureType(publicKey)) {
case SignatureType.Ed25519:
// Currently this is the only way for us to get the raw 32 bytes of the public key.
return new Uint8Array(exportedKey.subarray(-32));
case SignatureType.EcdsaP256SHA256: {
// The last 65 bytes are the raw bytes of the ECDSA P-256 public key.
// For the purposes of signing, we'd like to convert it to its compressed form that takes only 33 bytes.
const uncompressedKey = exportedKey.subarray(-65);
const compressedKey = crypto.ECDH.convertKey(
uncompressedKey,
'prime256v1',
/*inputEncoding=*/ undefined,
/*outputEncoding=*/ undefined,
'compressed'
) as Buffer;
return new Uint8Array(compressedKey);
}
}
}

// Throws an error if the key is not a valid Ed25519 or ECDSA P-256 key of the specified type.
export function checkIsValidKey(
expectedKeyType: crypto.KeyObjectType,
key: KeyObject
) {
Expand All @@ -49,9 +101,7 @@ export function checkIsValidEd25519Key(
);
}

if (key.asymmetricKeyType !== 'ed25519') {
throw new Error(
`Expected asymmetric key type to be "ed25519", but it was "${key.asymmetricKeyType}".`
);
if (!isAsymmetricKeyTypeSupported(key)) {
throw new Error(`Expected either "Ed25519" or "ECDSA P-256" key.`);
}
}
29 changes: 20 additions & 9 deletions js/sign/src/web-bundle-id.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,44 @@
import crypto, { KeyObject } from 'crypto';
import base32Encode from 'base32-encode';
import { getRawPublicKey } from './utils/utils.js';
import {
getRawPublicKey,
isAsymmetricKeyTypeSupported,
getSignatureType,
} from './utils/utils.js';
import { SignatureType } from './utils/constants.js';

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

constructor(ed25519key: KeyObject) {
if (ed25519key.asymmetricKeyType !== 'ed25519') {
constructor(key: KeyObject) {
if (!isAsymmetricKeyTypeSupported(key)) {
throw new Error(
`WebBundleId: Only ed25519 keys are currently supported. Your key's type is ${ed25519key.asymmetricKeyType}.`
`WebBundleId: Only Ed25519 and ECDSA P-256 keys are currently supported.`
);
}

if (ed25519key.type === 'private') {
this.key = crypto.createPublicKey(ed25519key);
if (key.type === 'private') {
this.key = crypto.createPublicKey(key);
} else {
this.key = ed25519key;
this.key = key;
}

this.typeSuffix = this.TYPE_SUFFIX_MAPPING.get(getSignatureType(this.key))!;
}

serialize() {
return base32Encode(
new Uint8Array([...getRawPublicKey(this.key), ...this.appIdSuffix]),
new Uint8Array([...getRawPublicKey(this.key), ...this.typeSuffix]),
'RFC4648',
{ padding: false }
).toLowerCase();
Expand Down
Binary file added js/sign/telnet.swbn
Binary file not shown.
Loading

0 comments on commit 2187f0d

Please sign in to comment.