Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ECDSA P-256 SHA-256 implementation #888

Merged
merged 6 commits into from
Apr 30, 2024
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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
@@ -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
2 changes: 1 addition & 1 deletion js/sign/README.md
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ npm install wbn-sign

## Requirements

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

## Usage

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",
@@ -33,7 +33,8 @@
],
"author": "Sonja Laurila <laurila@google.com> (https://github.com/sonkkeli)",
"contributors": [
"Christian Flach <cmfcmf@google.com> (https://github.com/cmfcmf)"
"Christian Flach <cmfcmf@google.com> (https://github.com/cmfcmf)",
"Andrew Rayskiy <greengrape@google.com> (https://github.com/GrapeGreen)"
],
"license": "W3C-20150513",
"dependencies": {
@@ -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": {
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 = {
@@ -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();
@@ -56,6 +56,7 @@ export class IntegrityBlockSigner {

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

return {
integrityBlock: signedIbCbor,
signedWebBundle: new Uint8Array(
@@ -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,
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);
}

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,
]); // 🖋📦
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.
@@ -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
) {
@@ -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();
Binary file added js/sign/telnet.swbn
Binary file not shown.
Loading