Skip to content

Commit

Permalink
Go ham on adding more type guards
Browse files Browse the repository at this point in the history
  • Loading branch information
MasterKale committed Nov 17, 2022
1 parent 1edd5bb commit 2d122b4
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 67 deletions.
74 changes: 66 additions & 8 deletions packages/server/src/helpers/convertCOSEtoPKCS.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { COSEAlgorithmIdentifier } from '@simplewebauthn/typescript-types';

import { isoCBOR, isoUint8Array } from './iso';

/**
* Takes COSE-encoded public key and converts it to PKCS key
*/
export function convertCOSEtoPKCS(cosePublicKey: Uint8Array): Uint8Array {
const struct = isoCBOR.decodeFirst<COSEPublicKey>(cosePublicKey);
// This is a little sloppy, I'm using COSEPublicKeyEC2 since it could have both x and y, but when
// there's no y it means it's probably better typed as COSEPublicKeyOKP. I'll leave this for now
// and revisit it later if it ever becomes an actual problem.
const struct = isoCBOR.decodeFirst<COSEPublicKeyEC2>(cosePublicKey);

const tag = Uint8Array.from([0x04]);
const x = struct.get(COSEKEYS.x);
Expand All @@ -17,13 +18,62 @@ export function convertCOSEtoPKCS(cosePublicKey: Uint8Array): Uint8Array {
}

if (y) {
return isoUint8Array.concat([tag, x as Uint8Array, y as Uint8Array]);
return isoUint8Array.concat([tag, x, y]);
}

return isoUint8Array.concat([tag, x as Uint8Array]);
return isoUint8Array.concat([tag, x]);
}

export type COSEPublicKey = Map<COSEAlgorithmIdentifier, number | Uint8Array>;
/**
* Fundamental values that are needed to discern the more specific COSE public key types below
*/
export type COSEPublicKey = {
// Getters
get(key: COSEKEYS.kty): COSEKTY | undefined;
get(key: COSEKEYS.alg): COSEALG | undefined;
// Setters
set(key: COSEKEYS.kty, value: COSEKTY): void;
set(key: COSEKEYS.alg, value: COSEALG): void;
};

export type COSEPublicKeyOKP = COSEPublicKey & {
// Getters
get(key: COSEKEYS.x): Uint8Array | undefined;
// Setters
set(key: COSEKEYS.x, value: Uint8Array): void;
};

export type COSEPublicKeyEC2 = COSEPublicKey & {
// Getters
get(key: COSEKEYS.crv): number | undefined;
get(key: COSEKEYS.x): Uint8Array | undefined;
get(key: COSEKEYS.y): Uint8Array | undefined;
// Setters
set(key: COSEKEYS.crv, value: number): void;
set(key: COSEKEYS.x, value: Uint8Array): void;
set(key: COSEKEYS.y, value: Uint8Array): void;
};

export type COSEPublicKeyRSA = COSEPublicKey & {
// Getters
get(key: COSEKEYS.n): Uint8Array | undefined;
get(key: COSEKEYS.e): Uint8Array | undefined;
// Setters
set(key: COSEKEYS.n, value: Uint8Array): void;
set(key: COSEKEYS.e, value: Uint8Array): void;
};

export function isCOSEPublicKeyOKP(publicKey: COSEPublicKey): publicKey is COSEPublicKeyOKP {
return publicKey.get(COSEKEYS.kty) === COSEKTY.OKP;
}

export function isCOSEPublicKeyEC2(publicKey: COSEPublicKey): publicKey is COSEPublicKeyEC2 {
return publicKey.get(COSEKEYS.kty) === COSEKTY.EC2;
}

export function isCOSEPublicKeyRSA(publicKey: COSEPublicKey): publicKey is COSEPublicKeyRSA {
return publicKey.get(COSEKEYS.kty) === COSEKTY.RSA;
}

export enum COSEKEYS {
kty = 1,
Expand All @@ -48,7 +98,14 @@ export enum COSECRV {
ED25519 = 6,
}

export type COSEALG = number;
export const coseAlgs = [-65535, -259, -258, -257, -47, -39, -38, -37, -36, -35, -8, -7] as const;
export type COSEALG = typeof coseAlgs[number];
/**
* Ensure that a number is a valid COSE algorithm ID
*/
export function isCOSEAlg(alg: number): alg is COSEALG {
return coseAlgs.indexOf(alg as COSEALG) >= 0;
}

export const COSERSASCHEME: { [key: string]: SigningSchemeHash } = {
'-3': 'pss-sha256',
Expand All @@ -72,11 +129,12 @@ export const coseCRV: { [key: number]: string } = {
6: 'ed25519',
};

export const COSEALGHASH: { [key: string]: string } = {
export const coseAlgSHAHashMap: { [K in COSEALG]: string } = {
'-65535': 'sha1',
'-259': 'sha512',
'-258': 'sha384',
'-257': 'sha256',
'-47': 'sha256',
'-39': 'sha512',
'-38': 'sha384',
'-37': 'sha256',
Expand Down
30 changes: 15 additions & 15 deletions packages/server/src/helpers/convertPublicKeyToPEM.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import jwkToPem from 'jwk-to-pem';

import { COSEKEYS, COSEKTY, coseCRV, COSEPublicKey } from './convertCOSEtoPKCS';
import { COSEKEYS, coseCRV, COSEPublicKey, isCOSEPublicKeyEC2, isCOSEPublicKeyRSA } from './convertCOSEtoPKCS';
import { isoBase64URL, isoCBOR } from './iso';

export function convertPublicKeyToPEM(publicKey: Uint8Array): string {
let struct;
let cosePublicKey;
try {
struct = isoCBOR.decodeFirst<COSEPublicKey>(publicKey);
cosePublicKey = isoCBOR.decodeFirst<COSEPublicKey>(publicKey);
} catch (err) {
const _err = err as Error;
throw new Error(`Error decoding public key while converting to PEM: ${_err.message}`);
}

const kty = struct.get(COSEKEYS.kty);
const kty = cosePublicKey.get(COSEKEYS.kty);

if (!kty) {
throw new Error('Public key was missing kty');
}

if (kty === COSEKTY.EC2) {
const crv = struct.get(COSEKEYS.crv);
const x = struct.get(COSEKEYS.x);
const y = struct.get(COSEKEYS.y);
if (isCOSEPublicKeyEC2(cosePublicKey)) {
const crv = cosePublicKey.get(COSEKEYS.crv);
const x = cosePublicKey.get(COSEKEYS.x);
const y = cosePublicKey.get(COSEKEYS.y);

if (!crv) {
throw new Error('Public key was missing crv (EC2)');
Expand All @@ -39,14 +39,14 @@ export function convertPublicKeyToPEM(publicKey: Uint8Array): string {
kty: 'EC',
// Specify curve as "P-256" from "p256"
crv: coseCRV[crv as number].replace('p', 'P-'),
x: isoBase64URL.fromBuffer(x as Uint8Array, 'base64'),
y: isoBase64URL.fromBuffer(y as Uint8Array, 'base64'),
x: isoBase64URL.fromBuffer(x, 'base64'),
y: isoBase64URL.fromBuffer(y, 'base64'),
});

return ecPEM;
} else if (kty === COSEKTY.RSA) {
const n = struct.get(COSEKEYS.n);
const e = struct.get(COSEKEYS.e);
} else if (isCOSEPublicKeyRSA(cosePublicKey)) {
const n = cosePublicKey.get(COSEKEYS.n);
const e = cosePublicKey.get(COSEKEYS.e);

if (!n) {
throw new Error('Public key was missing n (RSA)');
Expand All @@ -58,8 +58,8 @@ export function convertPublicKeyToPEM(publicKey: Uint8Array): string {

const rsaPEM = jwkToPem({
kty: 'RSA',
n: isoBase64URL.fromBuffer(n as Uint8Array, 'base64'),
e: isoBase64URL.fromBuffer(e as Uint8Array, 'base64'),
n: isoBase64URL.fromBuffer(n, 'base64'),
e: isoBase64URL.fromBuffer(e, 'base64'),
});

return rsaPEM;
Expand Down
32 changes: 18 additions & 14 deletions packages/server/src/helpers/iso/isoCrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ECDSASigValue } from "@peculiar/asn1-ecc";
import { AsnParser } from '@peculiar/asn1-schema';

import { isoUint8Array, isoBase64URL } from './index';
import { COSECRV, COSEKEYS, COSEKTY, COSEALG, COSEPublicKey } from '../convertCOSEtoPKCS';
import { COSECRV, COSEKEYS, COSEKTY, COSEALG, COSEPublicKeyEC2, COSEPublicKeyRSA, isCOSEAlg, isCOSEPublicKeyEC2, isCOSEPublicKeyRSA } from '../convertCOSEtoPKCS';

/**
* Fill up the provided bytes array with random bytes equal to its length.
Expand Down Expand Up @@ -96,18 +96,18 @@ export async function verify({
* @param rsaHashAlgorithm A SHA hashing identifier for use when verifying signatures with the
* returned RSA public key (e.g. `"sha1"`, `"sha256"`, etc...), if applicable
*/
export async function importKey(publicKey: COSEPublicKey, rsaHashAlgorithm?: string): Promise<CryptoKey> {
export async function importKey(publicKey: COSEPublicKeyEC2 | COSEPublicKeyRSA, rsaHashAlgorithm?: string): Promise<CryptoKey> {
const kty = publicKey.get(COSEKEYS.kty);

if (!kty) {
throw new Error('Public key was missing kty');
}

if (kty === COSEKTY.EC2) {
if (isCOSEPublicKeyEC2(publicKey)) {
return importECKey(publicKey);
}

if (kty === COSEKTY.RSA) {
if (isCOSEPublicKeyRSA(publicKey)) {
return importRSAKey(publicKey, rsaHashAlgorithm);
}

Expand All @@ -117,7 +117,7 @@ export async function importKey(publicKey: COSEPublicKey, rsaHashAlgorithm?: str
/**
* Import an EC2 public key from its COSE representation
*/
async function importECKey(publicKey: COSEPublicKey): Promise<CryptoKey> {
async function importECKey(publicKey: COSEPublicKeyEC2): Promise<CryptoKey> {
const crv = publicKey.get(COSEKEYS.crv);
const x = publicKey.get(COSEKEYS.x);
const y = publicKey.get(COSEKEYS.y);
Expand Down Expand Up @@ -151,8 +151,8 @@ async function importECKey(publicKey: COSEPublicKey): Promise<CryptoKey> {
const jwk: JsonWebKey = {
kty: "EC",
crv: _crv,
x: isoBase64URL.fromBuffer(x as Uint8Array),
y: isoBase64URL.fromBuffer(y as Uint8Array),
x: isoBase64URL.fromBuffer(x),
y: isoBase64URL.fromBuffer(y),
ext: false,
};

Expand Down Expand Up @@ -197,35 +197,39 @@ async function verifyECSignature(
/**
* Import an RSA public key from its COSE representation
*/
async function importRSAKey(publicKey: COSEPublicKey, hashAlgorithm?: string): Promise<CryptoKey> {
async function importRSAKey(publicKey: COSEPublicKeyRSA, hashAlgorithm?: string): Promise<CryptoKey> {
const alg = publicKey.get(COSEKEYS.alg);
const n = publicKey.get(COSEKEYS.n);
const e = publicKey.get(COSEKEYS.e);

if (!alg) {
throw new Error('RSA public key was missing alg');
throw new Error('Public key was missing alg (RSA)');
}

if (!isCOSEAlg(alg)) {
throw new Error(`Public key had invalid alg ${alg} (RSA)`);
}

if (!n) {
throw new Error('RSA public key was missing n');
throw new Error('Public key was missing n (RSA)');
}

if (!e) {
throw new Error('RSA public key was missing e');
throw new Error('Public key was missing e (RSA)');
}

const jwk: JsonWebKey = {
kty: 'RSA',
alg: '',
n: isoBase64URL.fromBuffer(n as Uint8Array),
e: isoBase64URL.fromBuffer(e as Uint8Array),
n: isoBase64URL.fromBuffer(n),
e: isoBase64URL.fromBuffer(e),
ext: false,
};

const keyAlgorithm = {
name: 'RSASSA-PKCS1-v1_5',
// This is actually the digest hash that'll get used by `.verify()`
hash: { name: mapCoseAlgToWebCryptoAlg(alg as number) },
hash: { name: mapCoseAlgToWebCryptoAlg(alg) },
};

if (hashAlgorithm) {
Expand Down
22 changes: 13 additions & 9 deletions packages/server/src/helpers/verifySignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Certificate } from '@peculiar/asn1-x509';
import { ECParameters, id_ecPublicKey, id_secp256r1 } from '@peculiar/asn1-ecc';
import { RSAPublicKey } from '@peculiar/asn1-rsa';

import { COSECRV, COSEKEYS, COSEKTY, COSEPublicKey } from './convertCOSEtoPKCS';
import { COSEALG, COSECRV, COSEKEYS, COSEKTY, COSEPublicKey, COSEPublicKeyEC2, COSEPublicKeyRSA, isCOSEAlg, isCOSEPublicKeyOKP } from './convertCOSEtoPKCS';
import { isoCrypto } from './iso';
import { decodeCredentialPublicKey } from './decodeCredentialPublicKey';

Expand Down Expand Up @@ -43,7 +43,7 @@ export async function verifySignature(

let subtlePublicKey: CryptoKey;
let kty: COSEKTY;
let alg: number;
let alg: COSEALG;

if (_isCredPubKeyOpts) {
const { publicKey } = opts;
Expand All @@ -61,21 +61,25 @@ export async function verifySignature(
throw new Error('Public key was missing alg');
}

if (!isCOSEAlg(_alg)) {
throw new Error(`Public key contained invalid alg ${_alg}`);
}

// Verify Ed25519 slightly differently
if (_kty === COSEKTY.OKP) {
if (isCOSEPublicKeyOKP(cosePublicKey)) {
const x = cosePublicKey.get(COSEKEYS.x);

if (!x) {
throw new Error('Public key was missing x (OKP)');
}

return ed25519Verify(signature, data, (x as Uint8Array));
return ed25519Verify(signature, data, x);
}

// Assume we're handling COSEKTY.EC2 or COSEKTY.RSA key from here on
subtlePublicKey = await isoCrypto.importKey(cosePublicKey);
subtlePublicKey = await isoCrypto.importKey(cosePublicKey as COSEPublicKeyEC2 | COSEPublicKeyRSA);
kty = _kty as COSEKTY;
alg = _alg as number;
alg = _alg;
} else if (_isLeafcertOpts) {
/**
* Time to extract the public key from an X.509 leaf certificate
Expand Down Expand Up @@ -130,7 +134,7 @@ export async function verifySignature(
throw new Error('TODO: Figure out how to handle public keys in "compressed form"');
}

const coseEC2PubKey: COSEPublicKey = new Map();
const coseEC2PubKey: COSEPublicKeyEC2 = new Map();
coseEC2PubKey.set(COSEKEYS.kty, COSEKTY.EC2);
coseEC2PubKey.set(COSEKEYS.crv, crv);
coseEC2PubKey.set(COSEKEYS.x, x);
Expand All @@ -145,7 +149,7 @@ export async function verifySignature(
kty = COSEKTY.RSA;
const rsaPublicKey = AsnParser.parse(subjectPublicKeyInfo.subjectPublicKey, RSAPublicKey);

let _alg = -999;
let _alg: COSEALG;
if (signatureAlgorithm === '1.2.840.113549.1.1.11') {
_alg = -257; // RS256
} else if (signatureAlgorithm === '1.2.840.113549.1.1.12') {
Expand All @@ -158,7 +162,7 @@ export async function verifySignature(
);
}

const coseRSAPubKey: COSEPublicKey = new Map();
const coseRSAPubKey: COSEPublicKeyRSA = new Map();
coseRSAPubKey.set(COSEKEYS.kty, COSEKTY.RSA);
coseRSAPubKey.set(COSEKEYS.alg, _alg);
coseRSAPubKey.set(COSEKEYS.n, new Uint8Array(rsaPublicKey.modulus));
Expand Down

0 comments on commit 2d122b4

Please sign in to comment.