feat: 초기 프로젝트 설정 및 룰.md 파일 추가

This commit is contained in:
2025-07-28 09:53:31 +09:00
commit 09a4d38512
8165 changed files with 1021855 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
MIT License
Copyright (c) 2020 Matthew Miller
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,47 @@
# @simplewebauthn/server <!-- omit in toc -->
![WebAuthn](https://img.shields.io/badge/WebAuthn-Simplified-blueviolet?style=for-the-badge&logo=WebAuthn)
[![npm (scoped)](https://img.shields.io/npm/v/@simplewebauthn/server?style=for-the-badge&logo=npm)](https://www.npmjs.com/package/@simplewebauthn/server)
[![JSR](https://jsr.io/badges/@simplewebauthn/server?style=for-the-badge)](https://jsr.io/@simplewebauthn/server)
- [Installation](#installation)
- [Node LTS 20.x and higher](#node-lts-20x-and-higher)
- [Deno v1.43 and higher](#deno-v143-and-higher)
- [Documentation](#documentation)
- [Supported Attestation Formats](#supported-attestation-formats)
## Installation
This package can be installed from **[NPM](https://www.npmjs.com/package/@simplewebauthn/server)**
and **[JSR](https://jsr.io/@simplewebauthn/server)**:
### Node LTS 20.x and higher
```sh
npm install @simplewebauthn/server
```
### Deno v1.43 and higher
```sh
deno add jsr:@simplewebauthn/server
```
## Documentation
You can find in-depth documentation on this package here:
https://simplewebauthn.dev/docs/packages/server
## Supported Attestation Formats
SimpleWebAuthn supports
[all current WebAuthn attestation formats](https://w3c.github.io/webauthn/#sctn-defined-attestation-formats),
including:
- **Android Key**
- **Android SafetyNet**
- **Apple**
- **FIDO U2F**
- **Packed**
- **TPM**
- **None**

View File

@@ -0,0 +1,26 @@
import type { AuthenticationExtensionsClientInputs, AuthenticatorTransportFuture, Base64URLString, PublicKeyCredentialRequestOptionsJSON } from '../types/index.js';
export type GenerateAuthenticationOptionsOpts = Parameters<typeof generateAuthenticationOptions>[0];
/**
* Prepare a value to pass into navigator.credentials.get(...) for authenticator authentication
*
* **Options:**
*
* @param rpID - Valid domain name (after `https://`)
* @param allowCredentials **(Optional)** - Authenticators previously registered by the user, if any. If undefined the client will ask the user which credential they want to use
* @param challenge **(Optional)** - Random value the authenticator needs to sign and pass back user for authentication. Defaults to generating a random value
* @param timeout **(Optional)** - How long (in ms) the user can take to complete authentication. Defaults to `60000`
* @param userVerification **(Optional)** - Set to `'discouraged'` when asserting as part of a 2FA flow, otherwise set to `'preferred'` or `'required'` as desired. Defaults to `"preferred"`
* @param extensions **(Optional)** - Additional plugins the authenticator or browser should use during authentication
*/
export declare function generateAuthenticationOptions(options: {
rpID: string;
allowCredentials?: {
id: Base64URLString;
transports?: AuthenticatorTransportFuture[];
}[];
challenge?: string | Uint8Array;
timeout?: number;
userVerification?: 'required' | 'preferred' | 'discouraged';
extensions?: AuthenticationExtensionsClientInputs;
}): Promise<PublicKeyCredentialRequestOptionsJSON>;
//# sourceMappingURL=generateAuthenticationOptions.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"generateAuthenticationOptions.d.ts","sourceRoot":"","sources":["../../src/authentication/generateAuthenticationOptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oCAAoC,EACpC,4BAA4B,EAC5B,eAAe,EACf,qCAAqC,EACtC,MAAM,mBAAmB,CAAC;AAI3B,MAAM,MAAM,iCAAiC,GAAG,UAAU,CAAC,OAAO,6BAA6B,CAAC,CAAC,CAAC,CAAC,CAAC;AAEpG;;;;;;;;;;;GAWG;AACH,wBAAsB,6BAA6B,CACjD,OAAO,EAAE;IACP,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE;QACjB,EAAE,EAAE,eAAe,CAAC;QACpB,UAAU,CAAC,EAAE,4BAA4B,EAAE,CAAC;KAC7C,EAAE,CAAC;IACJ,SAAS,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,UAAU,GAAG,WAAW,GAAG,aAAa,CAAC;IAC5D,UAAU,CAAC,EAAE,oCAAoC,CAAC;CACnD,GACA,OAAO,CAAC,qCAAqC,CAAC,CAoChD"}

View File

@@ -0,0 +1,41 @@
import { isoBase64URL, isoUint8Array } from '../helpers/iso/index.js';
import { generateChallenge } from '../helpers/generateChallenge.js';
/**
* Prepare a value to pass into navigator.credentials.get(...) for authenticator authentication
*
* **Options:**
*
* @param rpID - Valid domain name (after `https://`)
* @param allowCredentials **(Optional)** - Authenticators previously registered by the user, if any. If undefined the client will ask the user which credential they want to use
* @param challenge **(Optional)** - Random value the authenticator needs to sign and pass back user for authentication. Defaults to generating a random value
* @param timeout **(Optional)** - How long (in ms) the user can take to complete authentication. Defaults to `60000`
* @param userVerification **(Optional)** - Set to `'discouraged'` when asserting as part of a 2FA flow, otherwise set to `'preferred'` or `'required'` as desired. Defaults to `"preferred"`
* @param extensions **(Optional)** - Additional plugins the authenticator or browser should use during authentication
*/
export async function generateAuthenticationOptions(options) {
const { allowCredentials, challenge = await generateChallenge(), timeout = 60000, userVerification = 'preferred', extensions, rpID, } = options;
/**
* Preserve ability to specify `string` values for challenges
*/
let _challenge = challenge;
if (typeof _challenge === 'string') {
_challenge = isoUint8Array.fromUTF8String(_challenge);
}
return {
rpId: rpID,
challenge: isoBase64URL.fromBuffer(_challenge),
allowCredentials: allowCredentials?.map((cred) => {
if (!isoBase64URL.isBase64URL(cred.id)) {
throw new Error(`excludeCredential id "${cred.id}" is not a valid base64url string`);
}
return {
...cred,
id: isoBase64URL.trimPadding(cred.id),
type: 'public-key',
};
}),
timeout,
userVerification,
extensions,
};
}

View File

@@ -0,0 +1,67 @@
import type { AuthenticationResponseJSON, Base64URLString, CredentialDeviceType, UserVerificationRequirement, WebAuthnCredential } from '../types/index.js';
import type { AuthenticationExtensionsAuthenticatorOutputs } from '../helpers/decodeAuthenticatorExtensions.js';
/**
* Configurable options when calling `verifyAuthenticationResponse()`
*/
export type VerifyAuthenticationResponseOpts = Parameters<typeof verifyAuthenticationResponse>[0];
/**
* Verify that the user has legitimately completed the authentication process
*
* **Options:**
*
* @param response - Response returned by **@simplewebauthn/browser**'s `startAssertion()`
* @param expectedChallenge - The base64url-encoded `options.challenge` returned by `generateAuthenticationOptions()`
* @param expectedOrigin - Website URL (or array of URLs) that the registration should have occurred on
* @param expectedRPID - RP ID (or array of IDs) that was specified in the registration options
* @param credential - An internal {@link WebAuthnCredential} corresponding to `id` in the authentication response
* @param expectedType **(Optional)** - The response type expected ('webauthn.get')
* @param requireUserVerification **(Optional)** - Enforce user verification by the authenticator (via PIN, fingerprint, etc...) Defaults to `true`
* @param advancedFIDOConfig **(Optional)** - Options for satisfying more stringent FIDO RP feature requirements
* @param advancedFIDOConfig.userVerification **(Optional)** - Enable alternative rules for evaluating the User Presence and User Verified flags in authenticator data: UV (and UP) flags are optional unless this value is `"required"`
*/
export declare function verifyAuthenticationResponse(options: {
response: AuthenticationResponseJSON;
expectedChallenge: string | ((challenge: string) => boolean | Promise<boolean>);
expectedOrigin: string | string[];
expectedRPID: string | string[];
credential: WebAuthnCredential;
expectedType?: string | string[];
requireUserVerification?: boolean;
advancedFIDOConfig?: {
userVerification?: UserVerificationRequirement;
};
}): Promise<VerifiedAuthenticationResponse>;
/**
* Result of authentication verification
*
* @param verified If the authentication response could be verified
* @param authenticationInfo.credentialID The ID of the authenticator used during authentication.
* Should be used to identify which DB authenticator entry needs its `counter` updated to the value
* below
* @param authenticationInfo.newCounter The number of times the authenticator identified above
* reported it has been used. **Should be kept in a DB for later reference to help prevent replay
* attacks!**
* @param authenticationInfo.credentialDeviceType Whether this is a single-device or multi-device
* credential. **Should be kept in a DB for later reference!**
* @param authenticationInfo.credentialBackedUp Whether or not the multi-device credential has been
* backed up. Always `false` for single-device credentials. **Should be kept in a DB for later
* reference!**
* @param authenticationInfo.origin The origin of the website that the authentication occurred on
* @param authenticationInfo.rpID The RP ID that the authentication occurred on
* @param authenticationInfo?.authenticatorExtensionResults The authenticator extensions returned
* by the browser
*/
export type VerifiedAuthenticationResponse = {
verified: boolean;
authenticationInfo: {
credentialID: Base64URLString;
newCounter: number;
userVerified: boolean;
credentialDeviceType: CredentialDeviceType;
credentialBackedUp: boolean;
origin: string;
rpID: string;
authenticatorExtensionResults?: AuthenticationExtensionsAuthenticatorOutputs;
};
};
//# sourceMappingURL=verifyAuthenticationResponse.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"verifyAuthenticationResponse.d.ts","sourceRoot":"","sources":["../../src/authentication/verifyAuthenticationResponse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,eAAe,EACf,oBAAoB,EACpB,2BAA2B,EAC3B,kBAAkB,EACnB,MAAM,mBAAmB,CAAC;AAM3B,OAAO,KAAK,EAAE,4CAA4C,EAAE,MAAM,6CAA6C,CAAC;AAIhH;;GAEG;AACH,MAAM,MAAM,gCAAgC,GAAG,UAAU,CAAC,OAAO,4BAA4B,CAAC,CAAC,CAAC,CAAC,CAAC;AAElG;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE;IACP,QAAQ,EAAE,0BAA0B,CAAC;IACrC,iBAAiB,EAAE,MAAM,GAAG,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAChF,cAAc,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAClC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAChC,UAAU,EAAE,kBAAkB,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACjC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE;QACnB,gBAAgB,CAAC,EAAE,2BAA2B,CAAC;KAChD,CAAC;CACH,GACA,OAAO,CAAC,8BAA8B,CAAC,CAmNzC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,8BAA8B,GAAG;IAC3C,QAAQ,EAAE,OAAO,CAAC;IAClB,kBAAkB,EAAE;QAClB,YAAY,EAAE,eAAe,CAAC;QAC9B,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,OAAO,CAAC;QACtB,oBAAoB,EAAE,oBAAoB,CAAC;QAC3C,kBAAkB,EAAE,OAAO,CAAC;QAC5B,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,6BAA6B,CAAC,EAAE,4CAA4C,CAAC;KAC9E,CAAC;CACH,CAAC"}

View File

@@ -0,0 +1,171 @@
import { decodeClientDataJSON } from '../helpers/decodeClientDataJSON.js';
import { toHash } from '../helpers/toHash.js';
import { verifySignature } from '../helpers/verifySignature.js';
import { parseAuthenticatorData } from '../helpers/parseAuthenticatorData.js';
import { parseBackupFlags } from '../helpers/parseBackupFlags.js';
import { matchExpectedRPID } from '../helpers/matchExpectedRPID.js';
import { isoBase64URL, isoUint8Array } from '../helpers/iso/index.js';
/**
* Verify that the user has legitimately completed the authentication process
*
* **Options:**
*
* @param response - Response returned by **@simplewebauthn/browser**'s `startAssertion()`
* @param expectedChallenge - The base64url-encoded `options.challenge` returned by `generateAuthenticationOptions()`
* @param expectedOrigin - Website URL (or array of URLs) that the registration should have occurred on
* @param expectedRPID - RP ID (or array of IDs) that was specified in the registration options
* @param credential - An internal {@link WebAuthnCredential} corresponding to `id` in the authentication response
* @param expectedType **(Optional)** - The response type expected ('webauthn.get')
* @param requireUserVerification **(Optional)** - Enforce user verification by the authenticator (via PIN, fingerprint, etc...) Defaults to `true`
* @param advancedFIDOConfig **(Optional)** - Options for satisfying more stringent FIDO RP feature requirements
* @param advancedFIDOConfig.userVerification **(Optional)** - Enable alternative rules for evaluating the User Presence and User Verified flags in authenticator data: UV (and UP) flags are optional unless this value is `"required"`
*/
export async function verifyAuthenticationResponse(options) {
const { response, expectedChallenge, expectedOrigin, expectedRPID, expectedType, credential, requireUserVerification = true, advancedFIDOConfig, } = options;
const { id, rawId, type: credentialType, response: assertionResponse } = response;
// Ensure credential specified an ID
if (!id) {
throw new Error('Missing credential ID');
}
// Ensure ID is base64url-encoded
if (id !== rawId) {
throw new Error('Credential ID was not base64url-encoded');
}
// Make sure credential type is public-key
if (credentialType !== 'public-key') {
throw new Error(`Unexpected credential type ${credentialType}, expected "public-key"`);
}
if (!response) {
throw new Error('Credential missing response');
}
if (typeof assertionResponse?.clientDataJSON !== 'string') {
throw new Error('Credential response clientDataJSON was not a string');
}
const clientDataJSON = decodeClientDataJSON(assertionResponse.clientDataJSON);
const { type, origin, challenge, tokenBinding } = clientDataJSON;
// Make sure we're handling an authentication
if (Array.isArray(expectedType)) {
if (!expectedType.includes(type)) {
const joinedExpectedType = expectedType.join(', ');
throw new Error(`Unexpected authentication response type "${type}", expected one of: ${joinedExpectedType}`);
}
}
else if (expectedType) {
if (type !== expectedType) {
throw new Error(`Unexpected authentication response type "${type}", expected "${expectedType}"`);
}
}
else if (type !== 'webauthn.get') {
throw new Error(`Unexpected authentication response type: ${type}`);
}
// Ensure the device provided the challenge we gave it
if (typeof expectedChallenge === 'function') {
if (!(await expectedChallenge(challenge))) {
throw new Error(`Custom challenge verifier returned false for registration response challenge "${challenge}"`);
}
}
else if (challenge !== expectedChallenge) {
throw new Error(`Unexpected authentication response challenge "${challenge}", expected "${expectedChallenge}"`);
}
// Check that the origin is our site
if (Array.isArray(expectedOrigin)) {
if (!expectedOrigin.includes(origin)) {
const joinedExpectedOrigin = expectedOrigin.join(', ');
throw new Error(`Unexpected authentication response origin "${origin}", expected one of: ${joinedExpectedOrigin}`);
}
}
else {
if (origin !== expectedOrigin) {
throw new Error(`Unexpected authentication response origin "${origin}", expected "${expectedOrigin}"`);
}
}
if (!isoBase64URL.isBase64URL(assertionResponse.authenticatorData)) {
throw new Error('Credential response authenticatorData was not a base64url string');
}
if (!isoBase64URL.isBase64URL(assertionResponse.signature)) {
throw new Error('Credential response signature was not a base64url string');
}
if (assertionResponse.userHandle &&
typeof assertionResponse.userHandle !== 'string') {
throw new Error('Credential response userHandle was not a string');
}
if (tokenBinding) {
if (typeof tokenBinding !== 'object') {
throw new Error('ClientDataJSON tokenBinding was not an object');
}
if (['present', 'supported', 'notSupported'].indexOf(tokenBinding.status) < 0) {
throw new Error(`Unexpected tokenBinding status ${tokenBinding.status}`);
}
}
const authDataBuffer = isoBase64URL.toBuffer(assertionResponse.authenticatorData);
const parsedAuthData = parseAuthenticatorData(authDataBuffer);
const { rpIdHash, flags, counter, extensionsData } = parsedAuthData;
// Make sure the response's RP ID is ours
let expectedRPIDs = [];
if (typeof expectedRPID === 'string') {
expectedRPIDs = [expectedRPID];
}
else {
expectedRPIDs = expectedRPID;
}
const matchedRPID = await matchExpectedRPID(rpIdHash, expectedRPIDs);
if (advancedFIDOConfig !== undefined) {
const { userVerification: fidoUserVerification } = advancedFIDOConfig;
/**
* Use FIDO Conformance-defined rules for verifying UP and UV flags
*/
if (fidoUserVerification === 'required') {
// Require `flags.uv` be true (implies `flags.up` is true)
if (!flags.uv) {
throw new Error('User verification required, but user could not be verified');
}
}
else if (fidoUserVerification === 'preferred' ||
fidoUserVerification === 'discouraged') {
// Ignore `flags.uv`
}
}
else {
/**
* Use WebAuthn spec-defined rules for verifying UP and UV flags
*/
// WebAuthn only requires the user presence flag be true
if (!flags.up) {
throw new Error('User not present during authentication');
}
// Enforce user verification if required
if (requireUserVerification && !flags.uv) {
throw new Error('User verification required, but user could not be verified');
}
}
const clientDataHash = await toHash(isoBase64URL.toBuffer(assertionResponse.clientDataJSON));
const signatureBase = isoUint8Array.concat([authDataBuffer, clientDataHash]);
const signature = isoBase64URL.toBuffer(assertionResponse.signature);
if ((counter > 0 || credential.counter > 0) &&
counter <= credential.counter) {
// Error out when the counter in the DB is greater than or equal to the counter in the
// dataStruct. It's related to how the authenticator maintains the number of times its been
// used for this client. If this happens, then someone's somehow increased the counter
// on the device without going through this site
throw new Error(`Response counter value ${counter} was lower than expected ${credential.counter}`);
}
const { credentialDeviceType, credentialBackedUp } = parseBackupFlags(flags);
const toReturn = {
verified: await verifySignature({
signature,
data: signatureBase,
credentialPublicKey: credential.publicKey,
}),
authenticationInfo: {
newCounter: counter,
credentialID: credential.id,
userVerified: flags.uv,
credentialDeviceType,
credentialBackedUp,
authenticatorExtensionResults: extensionsData,
origin: clientDataJSON.origin,
rpID: matchedRPID,
},
};
return toReturn;
}

View File

@@ -0,0 +1,5 @@
/**
* Convert the aaguid buffer in authData into a UUID string
*/
export declare function convertAAGUIDToString(aaguid: Uint8Array): string;
//# sourceMappingURL=convertAAGUIDToString.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"convertAAGUIDToString.d.ts","sourceRoot":"","sources":["../../src/helpers/convertAAGUIDToString.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAchE"}

View File

@@ -0,0 +1,17 @@
import { isoUint8Array } from './iso/index.js';
/**
* Convert the aaguid buffer in authData into a UUID string
*/
export function convertAAGUIDToString(aaguid) {
// Raw Hex: adce000235bcc60a648b0b25f1f05503
const hex = isoUint8Array.toHex(aaguid);
const segments = [
hex.slice(0, 8), // 8
hex.slice(8, 12), // 4
hex.slice(12, 16), // 4
hex.slice(16, 20), // 4
hex.slice(20, 32), // 8
];
// Formatted: adce0002-35bc-c60a-648b-0b25f1f05503
return segments.join('-');
}

View File

@@ -0,0 +1,5 @@
/**
* Takes COSE-encoded public key and converts it to PKCS key
*/
export declare function convertCOSEtoPKCS(cosePublicKey: Uint8Array): Uint8Array;
//# sourceMappingURL=convertCOSEtoPKCS.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"convertCOSEtoPKCS.d.ts","sourceRoot":"","sources":["../../src/helpers/convertCOSEtoPKCS.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,UAAU,GAAG,UAAU,CAmBvE"}

View File

@@ -0,0 +1,21 @@
import { isoCBOR, isoUint8Array } from './iso/index.js';
import { COSEKEYS } from './cose.js';
/**
* Takes COSE-encoded public key and converts it to PKCS key
*/
export function convertCOSEtoPKCS(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(cosePublicKey);
const tag = Uint8Array.from([0x04]);
const x = struct.get(COSEKEYS.x);
const y = struct.get(COSEKEYS.y);
if (!x) {
throw new Error('COSE public key was missing x');
}
if (y) {
return isoUint8Array.concat([tag, x, y]);
}
return isoUint8Array.concat([tag, x]);
}

View File

@@ -0,0 +1,6 @@
import type { Base64URLString } from '../types/index.js';
/**
* Convert buffer to an OpenSSL-compatible PEM text format.
*/
export declare function convertCertBufferToPEM(certBuffer: Uint8Array | Base64URLString): string;
//# sourceMappingURL=convertCertBufferToPEM.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"convertCertBufferToPEM.d.ts","sourceRoot":"","sources":["../../src/helpers/convertCertBufferToPEM.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGzD;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,UAAU,GAAG,eAAe,GACvC,MAAM,CA4BR"}

View File

@@ -0,0 +1,31 @@
import { isoBase64URL } from './iso/index.js';
/**
* Convert buffer to an OpenSSL-compatible PEM text format.
*/
export function convertCertBufferToPEM(certBuffer) {
let b64cert;
/**
* Get certBuffer to a base64 representation
*/
if (typeof certBuffer === 'string') {
if (isoBase64URL.isBase64URL(certBuffer)) {
b64cert = isoBase64URL.toBase64(certBuffer);
}
else if (isoBase64URL.isBase64(certBuffer)) {
b64cert = certBuffer;
}
else {
throw new Error('Certificate is not a valid base64 or base64url string');
}
}
else {
b64cert = isoBase64URL.fromBuffer(certBuffer, 'base64');
}
let PEMKey = '';
for (let i = 0; i < Math.ceil(b64cert.length / 64); i += 1) {
const start = 64 * i;
PEMKey += `${b64cert.substr(start, 64)}\n`;
}
PEMKey = `-----BEGIN CERTIFICATE-----\n${PEMKey}-----END CERTIFICATE-----\n`;
return PEMKey;
}

View File

@@ -0,0 +1,5 @@
/**
* Take a certificate in PEM format and convert it to bytes
*/
export declare function convertPEMToBytes(pem: string): Uint8Array;
//# sourceMappingURL=convertPEMToBytes.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"convertPEMToBytes.d.ts","sourceRoot":"","sources":["../../src/helpers/convertPEMToBytes.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAOzD"}

View File

@@ -0,0 +1,11 @@
import { isoBase64URL } from './iso/index.js';
/**
* Take a certificate in PEM format and convert it to bytes
*/
export function convertPEMToBytes(pem) {
const certBase64 = pem
.replace('-----BEGIN CERTIFICATE-----', '')
.replace('-----END CERTIFICATE-----', '')
.replace(/[\n ]/g, '');
return isoBase64URL.toBuffer(certBase64, 'base64');
}

View File

@@ -0,0 +1,3 @@
import { COSEPublicKey } from './cose.js';
export declare function convertX509PublicKeyToCOSE(x509Certificate: Uint8Array): COSEPublicKey;
//# sourceMappingURL=convertX509PublicKeyToCOSE.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"convertX509PublicKeyToCOSE.d.ts","sourceRoot":"","sources":["../../src/helpers/convertX509PublicKeyToCOSE.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,aAAa,EAGd,MAAM,WAAW,CAAC;AAKnB,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,UAAU,GAC1B,aAAa,CA+Ff"}

View File

@@ -0,0 +1,73 @@
import { AsnParser } from '@peculiar/asn1-schema';
import { Certificate } from '@peculiar/asn1-x509';
import { ECParameters, id_ecPublicKey, id_secp256r1, id_secp384r1 } from '@peculiar/asn1-ecc';
import { COSECRV, COSEKEYS, COSEKTY, } from './cose.js';
import { RSAPublicKey } from '@peculiar/asn1-rsa';
import { mapX509SignatureAlgToCOSEAlg } from './mapX509SignatureAlgToCOSEAlg.js';
export function convertX509PublicKeyToCOSE(x509Certificate) {
let cosePublicKey = new Map();
/**
* Time to extract the public key from an X.509 certificate
*/
const x509 = AsnParser.parse(x509Certificate, Certificate);
const { tbsCertificate } = x509;
const { subjectPublicKeyInfo, signature: _tbsSignature } = tbsCertificate;
const signatureAlgorithm = _tbsSignature.algorithm;
const publicKeyAlgorithmID = subjectPublicKeyInfo.algorithm.algorithm;
if (publicKeyAlgorithmID === id_ecPublicKey) {
/**
* EC2 Public Key
*/
if (!subjectPublicKeyInfo.algorithm.parameters) {
throw new Error('Certificate public key was missing parameters (EC2)');
}
const ecParameters = AsnParser.parse(new Uint8Array(subjectPublicKeyInfo.algorithm.parameters), ECParameters);
let crv = -999;
const { namedCurve } = ecParameters;
if (namedCurve === id_secp256r1) {
crv = COSECRV.P256;
}
else if (namedCurve === id_secp384r1) {
crv = COSECRV.P384;
}
else {
throw new Error(`Certificate public key contained unexpected namedCurve ${namedCurve} (EC2)`);
}
const subjectPublicKey = new Uint8Array(subjectPublicKeyInfo.subjectPublicKey);
let x;
let y;
if (subjectPublicKey[0] === 0x04) {
// Public key is in "uncompressed form", so we can split the remaining bytes in half
let pointer = 1;
const halfLength = (subjectPublicKey.length - 1) / 2;
x = subjectPublicKey.slice(pointer, pointer += halfLength);
y = subjectPublicKey.slice(pointer);
}
else {
throw new Error('TODO: Figure out how to handle public keys in "compressed form"');
}
const coseEC2PubKey = new Map();
coseEC2PubKey.set(COSEKEYS.kty, COSEKTY.EC2);
coseEC2PubKey.set(COSEKEYS.alg, mapX509SignatureAlgToCOSEAlg(signatureAlgorithm));
coseEC2PubKey.set(COSEKEYS.crv, crv);
coseEC2PubKey.set(COSEKEYS.x, x);
coseEC2PubKey.set(COSEKEYS.y, y);
cosePublicKey = coseEC2PubKey;
}
else if (publicKeyAlgorithmID === '1.2.840.113549.1.1.1') {
/**
* RSA public key
*/
const rsaPublicKey = AsnParser.parse(subjectPublicKeyInfo.subjectPublicKey, RSAPublicKey);
const coseRSAPubKey = new Map();
coseRSAPubKey.set(COSEKEYS.kty, COSEKTY.RSA);
coseRSAPubKey.set(COSEKEYS.alg, mapX509SignatureAlgToCOSEAlg(signatureAlgorithm));
coseRSAPubKey.set(COSEKEYS.n, new Uint8Array(rsaPublicKey.modulus));
coseRSAPubKey.set(COSEKEYS.e, new Uint8Array(rsaPublicKey.publicExponent));
cosePublicKey = coseRSAPubKey;
}
else {
throw new Error(`Certificate public key contained unexpected algorithm ID ${publicKeyAlgorithmID}`);
}
return cosePublicKey;
}

View File

@@ -0,0 +1,121 @@
/**
* Fundamental values that are needed to discern the more specific COSE public key types below.
*
* The use of `Maps` here is due to CBOR encoding being used with public keys, and the CBOR "Map"
* type is being decoded to JavaScript's `Map` type instead of, say, a basic Object as us JS
* developers might prefer.
*
* These types are an unorthodox way of saying "these Maps should involve these discrete lists of
* keys", but it works.
* @module
*/
/**
* COSE public key common values
*/
export type COSEPublicKey = {
get(key: COSEKEYS.kty): COSEKTY | undefined;
get(key: COSEKEYS.alg): COSEALG | undefined;
set(key: COSEKEYS.kty, value: COSEKTY): void;
set(key: COSEKEYS.alg, value: COSEALG): void;
};
/**
* Values specific to Octet Key Pair public keys
*/
export type COSEPublicKeyOKP = COSEPublicKey & {
get(key: COSEKEYS.crv): number | undefined;
get(key: COSEKEYS.x): Uint8Array | undefined;
set(key: COSEKEYS.crv, value: number): void;
set(key: COSEKEYS.x, value: Uint8Array): void;
};
/**
* Values specific to Elliptic Curve Cryptography public keys
*/
export type COSEPublicKeyEC2 = COSEPublicKey & {
get(key: COSEKEYS.crv): number | undefined;
get(key: COSEKEYS.x): Uint8Array | undefined;
get(key: COSEKEYS.y): Uint8Array | undefined;
set(key: COSEKEYS.crv, value: number): void;
set(key: COSEKEYS.x, value: Uint8Array): void;
set(key: COSEKEYS.y, value: Uint8Array): void;
};
/**
* Values specific to RSA public keys
*/
export type COSEPublicKeyRSA = COSEPublicKey & {
get(key: COSEKEYS.n): Uint8Array | undefined;
get(key: COSEKEYS.e): Uint8Array | undefined;
set(key: COSEKEYS.n, value: Uint8Array): void;
set(key: COSEKEYS.e, value: Uint8Array): void;
};
/**
* A type guard for determining if a COSE public key is an OKP key pair
*/
export declare function isCOSEPublicKeyOKP(cosePublicKey: COSEPublicKey): cosePublicKey is COSEPublicKeyOKP;
/**
* A type guard for determining if a COSE public key is an EC2 key pair
*/
export declare function isCOSEPublicKeyEC2(cosePublicKey: COSEPublicKey): cosePublicKey is COSEPublicKeyEC2;
/**
* A type guard for determining if a COSE public key is an RSA key pair
*/
export declare function isCOSEPublicKeyRSA(cosePublicKey: COSEPublicKey): cosePublicKey is COSEPublicKeyRSA;
/**
* COSE Keys
*
* https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters
* https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters
*/
export declare enum COSEKEYS {
kty = 1,
alg = 3,
crv = -1,
x = -2,
y = -3,
n = -1,
e = -2
}
/**
* COSE Key Types
*
* https://www.iana.org/assignments/cose/cose.xhtml#key-type
*/
export declare enum COSEKTY {
OKP = 1,
EC2 = 2,
RSA = 3
}
export declare function isCOSEKty(kty: number | undefined): kty is COSEKTY;
/**
* COSE Curves
*
* https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves
*/
export declare enum COSECRV {
P256 = 1,
P384 = 2,
P521 = 3,
ED25519 = 6,
SECP256K1 = 8
}
export declare function isCOSECrv(crv: number | undefined): crv is COSECRV;
/**
* COSE Algorithms
*
* https://www.iana.org/assignments/cose/cose.xhtml#algorithms
*/
export declare enum COSEALG {
ES256 = -7,
EdDSA = -8,
ES384 = -35,
ES512 = -36,
PS256 = -37,
PS384 = -38,
PS512 = -39,
ES256K = -47,
RS256 = -257,
RS384 = -258,
RS512 = -259,
RS1 = -65535
}
export declare function isCOSEAlg(alg: number | undefined): alg is COSEALG;
//# sourceMappingURL=cose.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"cose.d.ts","sourceRoot":"","sources":["../../src/helpers/cose.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAE1B,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,GAAG,OAAO,GAAG,SAAS,CAAC;IAC5C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,GAAG,OAAO,GAAG,SAAS,CAAC;IAE5C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9C,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG;IAE7C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,GAAG,MAAM,GAAG,SAAS,CAAC;IAC3C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,GAAG,UAAU,GAAG,SAAS,CAAC;IAE7C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAC/C,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG;IAE7C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,GAAG,MAAM,GAAG,SAAS,CAAC;IAC3C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,GAAG,UAAU,GAAG,SAAS,CAAC;IAC7C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,GAAG,UAAU,GAAG,SAAS,CAAC;IAE7C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAC/C,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG;IAE7C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,GAAG,UAAU,GAAG,SAAS,CAAC;IAC7C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,GAAG,UAAU,GAAG,SAAS,CAAC;IAE7C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9C,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAC/C,CAAC;AAEF;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,aAAa,GAC3B,aAAa,IAAI,gBAAgB,CAGnC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,aAAa,GAC3B,aAAa,IAAI,gBAAgB,CAGnC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,aAAa,GAC3B,aAAa,IAAI,gBAAgB,CAGnC;AAED;;;;;GAKG;AACH,oBAAY,QAAQ;IAClB,GAAG,IAAI;IACP,GAAG,IAAI;IACP,GAAG,KAAK;IACR,CAAC,KAAK;IACN,CAAC,KAAK;IACN,CAAC,KAAK;IACN,CAAC,KAAK;CACP;AAED;;;;GAIG;AACH,oBAAY,OAAO;IACjB,GAAG,IAAI;IACP,GAAG,IAAI;IACP,GAAG,IAAI;CACR;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,GAAG,IAAI,OAAO,CAEjE;AAED;;;;GAIG;AACH,oBAAY,OAAO;IACjB,IAAI,IAAI;IACR,IAAI,IAAI;IACR,IAAI,IAAI;IACR,OAAO,IAAI;IACX,SAAS,IAAI;CACd;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,GAAG,IAAI,OAAO,CAEjE;AAED;;;;GAIG;AACH,oBAAY,OAAO;IACjB,KAAK,KAAK;IACV,KAAK,KAAK;IACV,KAAK,MAAM;IACX,KAAK,MAAM;IACX,KAAK,MAAM;IACX,KAAK,MAAM;IACX,KAAK,MAAM;IACX,MAAM,MAAM;IACZ,KAAK,OAAO;IACZ,KAAK,OAAO;IACZ,KAAK,OAAO;IACZ,GAAG,SAAS;CACb;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,GAAG,IAAI,OAAO,CAEjE"}

View File

@@ -0,0 +1,101 @@
/**
* Fundamental values that are needed to discern the more specific COSE public key types below.
*
* The use of `Maps` here is due to CBOR encoding being used with public keys, and the CBOR "Map"
* type is being decoded to JavaScript's `Map` type instead of, say, a basic Object as us JS
* developers might prefer.
*
* These types are an unorthodox way of saying "these Maps should involve these discrete lists of
* keys", but it works.
* @module
*/
/**
* A type guard for determining if a COSE public key is an OKP key pair
*/
export function isCOSEPublicKeyOKP(cosePublicKey) {
const kty = cosePublicKey.get(COSEKEYS.kty);
return isCOSEKty(kty) && kty === COSEKTY.OKP;
}
/**
* A type guard for determining if a COSE public key is an EC2 key pair
*/
export function isCOSEPublicKeyEC2(cosePublicKey) {
const kty = cosePublicKey.get(COSEKEYS.kty);
return isCOSEKty(kty) && kty === COSEKTY.EC2;
}
/**
* A type guard for determining if a COSE public key is an RSA key pair
*/
export function isCOSEPublicKeyRSA(cosePublicKey) {
const kty = cosePublicKey.get(COSEKEYS.kty);
return isCOSEKty(kty) && kty === COSEKTY.RSA;
}
/**
* COSE Keys
*
* https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters
* https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters
*/
export var COSEKEYS;
(function (COSEKEYS) {
COSEKEYS[COSEKEYS["kty"] = 1] = "kty";
COSEKEYS[COSEKEYS["alg"] = 3] = "alg";
COSEKEYS[COSEKEYS["crv"] = -1] = "crv";
COSEKEYS[COSEKEYS["x"] = -2] = "x";
COSEKEYS[COSEKEYS["y"] = -3] = "y";
COSEKEYS[COSEKEYS["n"] = -1] = "n";
COSEKEYS[COSEKEYS["e"] = -2] = "e";
})(COSEKEYS || (COSEKEYS = {}));
/**
* COSE Key Types
*
* https://www.iana.org/assignments/cose/cose.xhtml#key-type
*/
export var COSEKTY;
(function (COSEKTY) {
COSEKTY[COSEKTY["OKP"] = 1] = "OKP";
COSEKTY[COSEKTY["EC2"] = 2] = "EC2";
COSEKTY[COSEKTY["RSA"] = 3] = "RSA";
})(COSEKTY || (COSEKTY = {}));
export function isCOSEKty(kty) {
return Object.values(COSEKTY).indexOf(kty) >= 0;
}
/**
* COSE Curves
*
* https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves
*/
export var COSECRV;
(function (COSECRV) {
COSECRV[COSECRV["P256"] = 1] = "P256";
COSECRV[COSECRV["P384"] = 2] = "P384";
COSECRV[COSECRV["P521"] = 3] = "P521";
COSECRV[COSECRV["ED25519"] = 6] = "ED25519";
COSECRV[COSECRV["SECP256K1"] = 8] = "SECP256K1";
})(COSECRV || (COSECRV = {}));
export function isCOSECrv(crv) {
return Object.values(COSECRV).indexOf(crv) >= 0;
}
/**
* COSE Algorithms
*
* https://www.iana.org/assignments/cose/cose.xhtml#algorithms
*/
export var COSEALG;
(function (COSEALG) {
COSEALG[COSEALG["ES256"] = -7] = "ES256";
COSEALG[COSEALG["EdDSA"] = -8] = "EdDSA";
COSEALG[COSEALG["ES384"] = -35] = "ES384";
COSEALG[COSEALG["ES512"] = -36] = "ES512";
COSEALG[COSEALG["PS256"] = -37] = "PS256";
COSEALG[COSEALG["PS384"] = -38] = "PS384";
COSEALG[COSEALG["PS512"] = -39] = "PS512";
COSEALG[COSEALG["ES256K"] = -47] = "ES256K";
COSEALG[COSEALG["RS256"] = -257] = "RS256";
COSEALG[COSEALG["RS384"] = -258] = "RS384";
COSEALG[COSEALG["RS512"] = -259] = "RS512";
COSEALG[COSEALG["RS1"] = -65535] = "RS1";
})(COSEALG || (COSEALG = {}));
export function isCOSEAlg(alg) {
return Object.values(COSEALG).indexOf(alg) >= 0;
}

View File

@@ -0,0 +1,34 @@
/**
* Convert an AttestationObject buffer to a proper object
*
* @param base64AttestationObject Attestation Object buffer
*/
export declare function decodeAttestationObject(attestationObject: Uint8Array): AttestationObject;
export type AttestationFormat = 'fido-u2f' | 'packed' | 'android-safetynet' | 'android-key' | 'tpm' | 'apple' | 'none';
export type AttestationObject = {
get(key: 'fmt'): AttestationFormat;
get(key: 'attStmt'): AttestationStatement;
get(key: 'authData'): Uint8Array;
};
/**
* `AttestationStatement` will be an instance of `Map`, but these keys help make finite the list of
* possible values within it.
*/
export type AttestationStatement = {
get(key: 'sig'): Uint8Array | undefined;
get(key: 'x5c'): Uint8Array[] | undefined;
get(key: 'response'): Uint8Array | undefined;
get(key: 'alg'): number | undefined;
get(key: 'ver'): string | undefined;
get(key: 'certInfo'): Uint8Array | undefined;
get(key: 'pubArea'): Uint8Array | undefined;
readonly size: number;
};
/**
* Make it possible to stub the return value during testing
* @ignore Don't include this in docs output
*/
export declare const _decodeAttestationObjectInternals: {
stubThis: (value: AttestationObject) => AttestationObject;
};
//# sourceMappingURL=decodeAttestationObject.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"decodeAttestationObject.d.ts","sourceRoot":"","sources":["../../src/helpers/decodeAttestationObject.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,iBAAiB,EAAE,UAAU,GAC5B,iBAAiB,CAInB;AAED,MAAM,MAAM,iBAAiB,GACzB,UAAU,GACV,QAAQ,GACR,mBAAmB,GACnB,aAAa,GACb,KAAK,GACL,OAAO,GACP,MAAM,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,CAAC,GAAG,EAAE,KAAK,GAAG,iBAAiB,CAAC;IACnC,GAAG,CAAC,GAAG,EAAE,SAAS,GAAG,oBAAoB,CAAC;IAC1C,GAAG,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAAC;CAClC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,GAAG,CAAC,GAAG,EAAE,KAAK,GAAG,UAAU,GAAG,SAAS,CAAC;IACxC,GAAG,CAAC,GAAG,EAAE,KAAK,GAAG,UAAU,EAAE,GAAG,SAAS,CAAC;IAC1C,GAAG,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;IAC7C,GAAG,CAAC,GAAG,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;IACpC,GAAG,CAAC,GAAG,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;IACpC,GAAG,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;IAC7C,GAAG,CAAC,GAAG,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;IAE5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,iCAAiC;sBAC1B,iBAAiB;CACpC,CAAC"}

View File

@@ -0,0 +1,16 @@
import { isoCBOR } from './iso/index.js';
/**
* Convert an AttestationObject buffer to a proper object
*
* @param base64AttestationObject Attestation Object buffer
*/
export function decodeAttestationObject(attestationObject) {
return _decodeAttestationObjectInternals.stubThis(isoCBOR.decodeFirst(attestationObject));
}
/**
* Make it possible to stub the return value during testing
* @ignore Don't include this in docs output
*/
export const _decodeAttestationObjectInternals = {
stubThis: (value) => value,
};

View File

@@ -0,0 +1,11 @@
/**
* Convert authenticator extension data buffer to a proper object
*
* @param extensionData Authenticator Extension Data buffer
*/
export declare function decodeAuthenticatorExtensions(extensionData: Uint8Array): AuthenticationExtensionsAuthenticatorOutputs | undefined;
/**
* Attempt to support authenticator extensions we might not know about in WebAuthn
*/
export type AuthenticationExtensionsAuthenticatorOutputs = unknown;
//# sourceMappingURL=decodeAuthenticatorExtensions.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"decodeAuthenticatorExtensions.d.ts","sourceRoot":"","sources":["../../src/helpers/decodeAuthenticatorExtensions.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,aAAa,EAAE,UAAU,GACxB,4CAA4C,GAAG,SAAS,CAU1D;AAED;;GAEG;AACH,MAAM,MAAM,4CAA4C,GAAG,OAAO,CAAC"}

View File

@@ -0,0 +1,34 @@
import { isoCBOR } from './iso/index.js';
/**
* Convert authenticator extension data buffer to a proper object
*
* @param extensionData Authenticator Extension Data buffer
*/
export function decodeAuthenticatorExtensions(extensionData) {
let toCBOR;
try {
toCBOR = isoCBOR.decodeFirst(extensionData);
}
catch (err) {
const _err = err;
throw new Error(`Error decoding authenticator extensions: ${_err.message}`);
}
return convertMapToObjectDeep(toCBOR);
}
/**
* CBOR-encoded extensions can be deeply-nested Maps, which are too deep for a simple
* `Object.entries()`. This method will recursively make sure that all Maps are converted into
* basic objects.
*/
function convertMapToObjectDeep(input) {
const mapped = {};
for (const [key, value] of input) {
if (value instanceof Map) {
mapped[key] = convertMapToObjectDeep(value);
}
else {
mapped[key] = value;
}
}
return mapped;
}

View File

@@ -0,0 +1,23 @@
import type { Base64URLString } from '../types/index.js';
/**
* Decode an authenticator's base64url-encoded clientDataJSON to JSON
*/
export declare function decodeClientDataJSON(data: Base64URLString): ClientDataJSON;
export type ClientDataJSON = {
type: string;
challenge: string;
origin: string;
crossOrigin?: boolean;
tokenBinding?: {
id?: string;
status: 'present' | 'supported' | 'not-supported';
};
};
/**
* Make it possible to stub the return value during testing
* @ignore Don't include this in docs output
*/
export declare const _decodeClientDataJSONInternals: {
stubThis: (value: ClientDataJSON) => ClientDataJSON;
};
//# sourceMappingURL=decodeClientDataJSON.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"decodeClientDataJSON.d.ts","sourceRoot":"","sources":["../../src/helpers/decodeClientDataJSON.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGzD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,eAAe,GAAG,cAAc,CAK1E;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE;QACb,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,eAAe,CAAC;KACnD,CAAC;CACH,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,8BAA8B;sBACvB,cAAc;CACjC,CAAC"}

View File

@@ -0,0 +1,16 @@
import { isoBase64URL } from './iso/index.js';
/**
* Decode an authenticator's base64url-encoded clientDataJSON to JSON
*/
export function decodeClientDataJSON(data) {
const toString = isoBase64URL.toUTF8String(data);
const clientData = JSON.parse(toString);
return _decodeClientDataJSONInternals.stubThis(clientData);
}
/**
* Make it possible to stub the return value during testing
* @ignore Don't include this in docs output
*/
export const _decodeClientDataJSONInternals = {
stubThis: (value) => value,
};

View File

@@ -0,0 +1,10 @@
import { COSEPublicKey } from './cose.js';
export declare function decodeCredentialPublicKey(publicKey: Uint8Array): COSEPublicKey;
/**
* Make it possible to stub the return value during testing
* @ignore Don't include this in docs output
*/
export declare const _decodeCredentialPublicKeyInternals: {
stubThis: (value: COSEPublicKey) => COSEPublicKey;
};
//# sourceMappingURL=decodeCredentialPublicKey.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"decodeCredentialPublicKey.d.ts","sourceRoot":"","sources":["../../src/helpers/decodeCredentialPublicKey.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,UAAU,GACpB,aAAa,CAIf;AAED;;;GAGG;AACH,eAAO,MAAM,mCAAmC;sBAC5B,aAAa;CAChC,CAAC"}

View File

@@ -0,0 +1,11 @@
import { isoCBOR } from './iso/index.js';
export function decodeCredentialPublicKey(publicKey) {
return _decodeCredentialPublicKeyInternals.stubThis(isoCBOR.decodeFirst(publicKey));
}
/**
* Make it possible to stub the return value during testing
* @ignore Don't include this in docs output
*/
export const _decodeCredentialPublicKeyInternals = {
stubThis: (value) => value,
};

View File

@@ -0,0 +1,13 @@
/**
* A simple method for requesting data via standard `fetch`. Should work
* across multiple runtimes.
*/
export declare function fetch(url: string): Promise<Response>;
/**
* Make it possible to stub the return value during testing
* @ignore Don't include this in docs output
*/
export declare const _fetchInternals: {
stubThis: (url: string) => Promise<Response>;
};
//# sourceMappingURL=fetch.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/helpers/fetch.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEpD;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe;oBACV,MAAM;CACvB,CAAC"}

View File

@@ -0,0 +1,14 @@
/**
* A simple method for requesting data via standard `fetch`. Should work
* across multiple runtimes.
*/
export function fetch(url) {
return _fetchInternals.stubThis(url);
}
/**
* Make it possible to stub the return value during testing
* @ignore Don't include this in docs output
*/
export const _fetchInternals = {
stubThis: (url) => globalThis.fetch(url),
};

View File

@@ -0,0 +1,12 @@
/**
* Generate a suitably random value to be used as an attestation or assertion challenge
*/
export declare function generateChallenge(): Promise<Uint8Array>;
/**
* Make it possible to stub the return value during testing
* @ignore Don't include this in docs output
*/
export declare const _generateChallengeInternals: {
stubThis: (value: Uint8Array) => Uint8Array;
};
//# sourceMappingURL=generateChallenge.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"generateChallenge.d.ts","sourceRoot":"","sources":["../../src/helpers/generateChallenge.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,UAAU,CAAC,CAc7D;AAED;;;GAGG;AACH,eAAO,MAAM,2BAA2B;sBACpB,UAAU;CAC7B,CAAC"}

View File

@@ -0,0 +1,24 @@
import { isoCrypto } from './iso/index.js';
/**
* Generate a suitably random value to be used as an attestation or assertion challenge
*/
export async function generateChallenge() {
/**
* WebAuthn spec says that 16 bytes is a good minimum:
*
* "In order to prevent replay attacks, the challenges MUST contain enough entropy to make
* guessing them infeasible. Challenges SHOULD therefore be at least 16 bytes long."
*
* Just in case, let's double it
*/
const challenge = new Uint8Array(32);
await isoCrypto.getRandomValues(challenge);
return _generateChallengeInternals.stubThis(challenge);
}
/**
* Make it possible to stub the return value during testing
* @ignore Don't include this in docs output
*/
export const _generateChallengeInternals = {
stubThis: (value) => value,
};

View File

@@ -0,0 +1,12 @@
/**
* Generate a suitably random value to be used as user ID
*/
export declare function generateUserID(): Promise<Uint8Array>;
/**
* Make it possible to stub the return value during testing
* @ignore Don't include this in docs output
*/
export declare const _generateUserIDInternals: {
stubThis: (value: Uint8Array) => Uint8Array;
};
//# sourceMappingURL=generateUserID.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"generateUserID.d.ts","sourceRoot":"","sources":["../../src/helpers/generateUserID.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,UAAU,CAAC,CAU1D;AAED;;;GAGG;AACH,eAAO,MAAM,wBAAwB;sBACjB,UAAU;CAC7B,CAAC"}

View File

@@ -0,0 +1,20 @@
import { isoCrypto } from './iso/index.js';
/**
* Generate a suitably random value to be used as user ID
*/
export async function generateUserID() {
/**
* WebAuthn spec says user.id has a max length of 64 bytes. I prefer how 32 random bytes look
* after they're base64url-encoded so I'm choosing to go with that here.
*/
const newUserID = new Uint8Array(32);
await isoCrypto.getRandomValues(newUserID);
return _generateUserIDInternals.stubThis(newUserID);
}
/**
* Make it possible to stub the return value during testing
* @ignore Don't include this in docs output
*/
export const _generateUserIDInternals = {
stubThis: (value) => value,
};

View File

@@ -0,0 +1,32 @@
import { Certificate } from '@peculiar/asn1-x509';
export type CertificateInfo = {
issuer: Issuer;
subject: Subject;
version: number;
basicConstraintsCA: boolean;
notBefore: Date;
notAfter: Date;
parsedCertificate: Certificate;
};
type Issuer = {
C?: string;
O?: string;
OU?: string;
CN?: string;
combined: string;
};
type Subject = {
C?: string;
O?: string;
OU?: string;
CN?: string;
combined: string;
};
/**
* Extract PEM certificate info
*
* @param pemCertificate Result from call to `convertASN1toPEM(x5c[0])`
*/
export declare function getCertificateInfo(leafCertBuffer: Uint8Array): CertificateInfo;
export {};
//# sourceMappingURL=getCertificateInfo.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"getCertificateInfo.d.ts","sourceRoot":"","sources":["../../src/helpers/getCertificateInfo.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,WAAW,EAA0B,MAAM,qBAAqB,CAAC;AAE5F,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,IAAI,CAAC;IACf,iBAAiB,EAAE,WAAW,CAAC;CAChC,CAAC;AAEF,KAAK,MAAM,GAAG;IACZ,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,KAAK,OAAO,GAAG;IACb,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AASF;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,cAAc,EAAE,UAAU,GACzB,eAAe,CA+CjB"}

View File

@@ -0,0 +1,77 @@
import { AsnParser } from '@peculiar/asn1-schema';
import { BasicConstraints, Certificate, id_ce_basicConstraints } from '@peculiar/asn1-x509';
const issuerSubjectIDKey = {
'2.5.4.6': 'C',
'2.5.4.10': 'O',
'2.5.4.11': 'OU',
'2.5.4.3': 'CN',
};
/**
* Extract PEM certificate info
*
* @param pemCertificate Result from call to `convertASN1toPEM(x5c[0])`
*/
export function getCertificateInfo(leafCertBuffer) {
const x509 = AsnParser.parse(leafCertBuffer, Certificate);
const parsedCert = x509.tbsCertificate;
// Issuer
const issuer = { combined: '' };
parsedCert.issuer.forEach(([iss]) => {
const key = issuerSubjectIDKey[iss.type];
if (key) {
issuer[key] = iss.value.toString();
}
});
issuer.combined = issuerSubjectToString(issuer);
// Subject
const subject = { combined: '' };
parsedCert.subject.forEach(([iss]) => {
const key = issuerSubjectIDKey[iss.type];
if (key) {
subject[key] = iss.value.toString();
}
});
subject.combined = issuerSubjectToString(subject);
let basicConstraintsCA = false;
if (parsedCert.extensions) {
// console.log(parsedCert.extensions);
for (const ext of parsedCert.extensions) {
if (ext.extnID === id_ce_basicConstraints) {
const basicConstraints = AsnParser.parse(ext.extnValue, BasicConstraints);
basicConstraintsCA = basicConstraints.cA;
}
}
}
return {
issuer,
subject,
version: parsedCert.version,
basicConstraintsCA,
notBefore: parsedCert.validity.notBefore.getTime(),
notAfter: parsedCert.validity.notAfter.getTime(),
parsedCertificate: x509,
};
}
/**
* Stringify the parts of Issuer or Subject info for easier comparison of subject issuers with
* issuer subjects.
*
* The order might seem arbitrary, because it is. It should be enough that the two are stringified
* in the same order.
*/
function issuerSubjectToString(input) {
const parts = [];
if (input.C) {
parts.push(input.C);
}
if (input.O) {
parts.push(input.O);
}
if (input.OU) {
parts.push(input.OU);
}
if (input.CN) {
parts.push(input.CN);
}
return parts.join(' : ');
}

View File

@@ -0,0 +1,17 @@
export * from './convertAAGUIDToString.js';
export * from './convertCertBufferToPEM.js';
export * from './convertCOSEtoPKCS.js';
export * from './decodeAttestationObject.js';
export * from './decodeClientDataJSON.js';
export * from './decodeCredentialPublicKey.js';
export * from './generateChallenge.js';
export * from './generateUserID.js';
export * from './getCertificateInfo.js';
export * from './isCertRevoked.js';
export * from './parseAuthenticatorData.js';
export * from './toHash.js';
export * from './validateCertificatePath.js';
export * from './verifySignature.js';
export * from './iso/index.js';
export * as cose from './cose.js';
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,aAAa,CAAC;AAC5B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC"}

View File

@@ -0,0 +1,16 @@
export * from './convertAAGUIDToString.js';
export * from './convertCertBufferToPEM.js';
export * from './convertCOSEtoPKCS.js';
export * from './decodeAttestationObject.js';
export * from './decodeClientDataJSON.js';
export * from './decodeCredentialPublicKey.js';
export * from './generateChallenge.js';
export * from './generateUserID.js';
export * from './getCertificateInfo.js';
export * from './isCertRevoked.js';
export * from './parseAuthenticatorData.js';
export * from './toHash.js';
export * from './validateCertificatePath.js';
export * from './verifySignature.js';
export * from './iso/index.js';
export * as cose from './cose.js';

View File

@@ -0,0 +1,9 @@
import { Certificate } from '@peculiar/asn1-x509';
/**
* A method to pull a CRL from a certificate and compare its serial number to the list of revoked
* certificate serial numbers within the CRL.
*
* CRL certificate structure referenced from https://tools.ietf.org/html/rfc5280#page-117
*/
export declare function isCertRevoked(cert: Certificate): Promise<boolean>;
//# sourceMappingURL=isCertRevoked.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"isCertRevoked.d.ts","sourceRoot":"","sources":["../../src/helpers/isCertRevoked.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,WAAW,EAOZ,MAAM,qBAAqB,CAAC;AAgB7B;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAgHvE"}

View File

@@ -0,0 +1,99 @@
import { AsnParser } from '@peculiar/asn1-schema';
import { AuthorityKeyIdentifier, CertificateList, CRLDistributionPoints, id_ce_authorityKeyIdentifier, id_ce_cRLDistributionPoints, id_ce_subjectKeyIdentifier, SubjectKeyIdentifier, } from '@peculiar/asn1-x509';
import { isoUint8Array } from './iso/index.js';
import { fetch } from './fetch.js';
const cacheRevokedCerts = {};
/**
* A method to pull a CRL from a certificate and compare its serial number to the list of revoked
* certificate serial numbers within the CRL.
*
* CRL certificate structure referenced from https://tools.ietf.org/html/rfc5280#page-117
*/
export async function isCertRevoked(cert) {
const { extensions } = cert.tbsCertificate;
if (!extensions) {
return false;
}
let extAuthorityKeyID;
let extSubjectKeyID;
let extCRLDistributionPoints;
extensions.forEach((ext) => {
if (ext.extnID === id_ce_authorityKeyIdentifier) {
extAuthorityKeyID = AsnParser.parse(ext.extnValue, AuthorityKeyIdentifier);
}
else if (ext.extnID === id_ce_subjectKeyIdentifier) {
extSubjectKeyID = AsnParser.parse(ext.extnValue, SubjectKeyIdentifier);
}
else if (ext.extnID === id_ce_cRLDistributionPoints) {
extCRLDistributionPoints = AsnParser.parse(ext.extnValue, CRLDistributionPoints);
}
});
// Check to see if we've got cached info for the cert's CA
let keyIdentifier = undefined;
if (extAuthorityKeyID && extAuthorityKeyID.keyIdentifier) {
keyIdentifier = isoUint8Array.toHex(new Uint8Array(extAuthorityKeyID.keyIdentifier.buffer));
}
else if (extSubjectKeyID) {
/**
* We might be dealing with a self-signed root certificate. Check the
* Subject key Identifier extension next.
*/
keyIdentifier = isoUint8Array.toHex(new Uint8Array(extSubjectKeyID.buffer));
}
const certSerialHex = isoUint8Array.toHex(new Uint8Array(cert.tbsCertificate.serialNumber));
if (keyIdentifier) {
const cached = cacheRevokedCerts[keyIdentifier];
if (cached) {
const now = new Date();
// If there's a nextUpdate then make sure we're before it
if (!cached.nextUpdate || cached.nextUpdate > now) {
return cached.revokedCerts.indexOf(certSerialHex) >= 0;
}
}
}
const crlURL = extCRLDistributionPoints?.[0].distributionPoint?.fullName?.[0]
.uniformResourceIdentifier;
// If no URL is provided then we have nothing to check
if (!crlURL) {
return false;
}
// Download and read the CRL
let certListBytes;
try {
const respCRL = await fetch(crlURL);
certListBytes = await respCRL.arrayBuffer();
}
catch (_err) {
return false;
}
let data;
try {
data = AsnParser.parse(certListBytes, CertificateList);
}
catch (_err) {
// Something was malformed with the CRL, so pass
return false;
}
const newCached = {
revokedCerts: [],
nextUpdate: undefined,
};
// nextUpdate
if (data.tbsCertList.nextUpdate) {
newCached.nextUpdate = data.tbsCertList.nextUpdate.getTime();
}
// revokedCertificates
const revokedCerts = data.tbsCertList.revokedCertificates;
if (revokedCerts) {
for (const cert of revokedCerts) {
const revokedHex = isoUint8Array.toHex(new Uint8Array(cert.userCertificate));
newCached.revokedCerts.push(revokedHex);
}
// Cache the results
if (keyIdentifier) {
cacheRevokedCerts[keyIdentifier] = newCached;
}
return newCached.revokedCerts.indexOf(certSerialHex) >= 0;
}
return false;
}

View File

@@ -0,0 +1,12 @@
/**
* A collection of methods for isomorphic manipulation of trickier data types
*
* The goal with these is to make it easier to replace dependencies later that might not play well
* with specific server-like runtimes that expose global Web APIs (CloudFlare Workers, Deno, Bun,
* etc...), while also supporting execution in Node.
*/
export * as isoBase64URL from './isoBase64URL.js';
export * as isoCBOR from './isoCBOR.js';
export * as isoCrypto from './isoCrypto/index.js';
export * as isoUint8Array from './isoUint8Array.js';
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/helpers/iso/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAC"}

View File

@@ -0,0 +1,11 @@
/**
* A collection of methods for isomorphic manipulation of trickier data types
*
* The goal with these is to make it easier to replace dependencies later that might not play well
* with specific server-like runtimes that expose global Web APIs (CloudFlare Workers, Deno, Bun,
* etc...), while also supporting execution in Node.
*/
export * as isoBase64URL from './isoBase64URL.js';
export * as isoCBOR from './isoCBOR.js';
export * as isoCrypto from './isoCrypto/index.js';
export * as isoUint8Array from './isoUint8Array.js';

View File

@@ -0,0 +1,43 @@
import type { Base64URLString } from '../../types/index.js';
/**
* Decode from a Base64URL-encoded string to an ArrayBuffer. Best used when converting a
* credential ID from a JSON string to an ArrayBuffer, like in allowCredentials or
* excludeCredentials.
*
* @param buffer Value to decode from base64
* @param to (optional) The decoding to use, in case it's desirable to decode from base64 instead
*/
export declare function toBuffer(base64urlString: string, from?: 'base64' | 'base64url'): Uint8Array;
/**
* Encode the given array buffer into a Base64URL-encoded string. Ideal for converting various
* credential response ArrayBuffers to string for sending back to the server as JSON.
*
* @param buffer Value to encode to base64
* @param to (optional) The encoding to use, in case it's desirable to encode to base64 instead
*/
export declare function fromBuffer(buffer: Uint8Array, to?: 'base64' | 'base64url'): string;
/**
* Convert a base64url string into base64
*/
export declare function toBase64(base64urlString: string): string;
/**
* Encode a UTF-8 string to base64url
*/
export declare function fromUTF8String(utf8String: string): string;
/**
* Decode a base64url string into its original UTF-8 string
*/
export declare function toUTF8String(base64urlString: string): string;
/**
* Confirm that the string is encoded into base64
*/
export declare function isBase64(input: string): boolean;
/**
* Confirm that the string is encoded into base64url, with support for optional padding
*/
export declare function isBase64URL(input: string): boolean;
/**
* Remove optional padding from a base64url-encoded string
*/
export declare function trimPadding(input: Base64URLString): Base64URLString;
//# sourceMappingURL=isoBase64URL.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"isoBase64URL.d.ts","sourceRoot":"","sources":["../../../src/helpers/iso/isoBase64URL.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CACtB,eAAe,EAAE,MAAM,EACvB,IAAI,GAAE,QAAQ,GAAG,WAAyB,GACzC,UAAU,CAGZ;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,UAAU,EAClB,EAAE,GAAE,QAAQ,GAAG,WAAyB,GACvC,MAAM,CAER;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAIlD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,eAAe,GAAG,eAAe,CAEnE"}

View File

@@ -0,0 +1,67 @@
/**
* A runtime-agnostic collection of methods for working with Base64URL encoding
* @module
*/
import base64 from '@hexagon/base64';
/**
* Decode from a Base64URL-encoded string to an ArrayBuffer. Best used when converting a
* credential ID from a JSON string to an ArrayBuffer, like in allowCredentials or
* excludeCredentials.
*
* @param buffer Value to decode from base64
* @param to (optional) The decoding to use, in case it's desirable to decode from base64 instead
*/
export function toBuffer(base64urlString, from = 'base64url') {
const _buffer = base64.toArrayBuffer(base64urlString, from === 'base64url');
return new Uint8Array(_buffer);
}
/**
* Encode the given array buffer into a Base64URL-encoded string. Ideal for converting various
* credential response ArrayBuffers to string for sending back to the server as JSON.
*
* @param buffer Value to encode to base64
* @param to (optional) The encoding to use, in case it's desirable to encode to base64 instead
*/
export function fromBuffer(buffer, to = 'base64url') {
return base64.fromArrayBuffer(buffer, to === 'base64url');
}
/**
* Convert a base64url string into base64
*/
export function toBase64(base64urlString) {
const fromBase64Url = base64.toArrayBuffer(base64urlString, true);
const toBase64 = base64.fromArrayBuffer(fromBase64Url);
return toBase64;
}
/**
* Encode a UTF-8 string to base64url
*/
export function fromUTF8String(utf8String) {
return base64.fromString(utf8String, true);
}
/**
* Decode a base64url string into its original UTF-8 string
*/
export function toUTF8String(base64urlString) {
return base64.toString(base64urlString, true);
}
/**
* Confirm that the string is encoded into base64
*/
export function isBase64(input) {
return base64.validate(input, false);
}
/**
* Confirm that the string is encoded into base64url, with support for optional padding
*/
export function isBase64URL(input) {
// Trim padding characters from the string if present
input = trimPadding(input);
return base64.validate(input, true);
}
/**
* Remove optional padding from a base64url-encoded string
*/
export function trimPadding(input) {
return input.replace(/=/g, '');
}

View File

@@ -0,0 +1,28 @@
/**
* A runtime-agnostic collection of methods for working with CBOR encoding
* @module
*/
import * as tinyCbor from '@levischuck/tiny-cbor';
/**
* Whatever CBOR encoder is used should keep CBOR data the same length when data is re-encoded
*
* MOST CRITICALLY, this means the following needs to be true of whatever CBOR library we use:
* - CBOR Map type values MUST decode to JavaScript Maps
* - CBOR tag 64 (uint8 Typed Array) MUST NOT be used when encoding Uint8Arrays back to CBOR
*
* So long as these requirements are maintained, then CBOR sequences can be encoded and decoded
* freely while maintaining their lengths for the most accurate pointer movement across them.
*/
/**
* Decode and return the first item in a sequence of CBOR-encoded values
*
* @param input The CBOR data to decode
* @param asObject (optional) Whether to convert any CBOR Maps into JavaScript Objects. Defaults to
* `false`
*/
export declare function decodeFirst<Type>(input: Uint8Array): Type;
/**
* Encode data to CBOR
*/
export declare function encode(input: tinyCbor.CBORType): Uint8Array;
//# sourceMappingURL=isoCBOR.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"isoCBOR.d.ts","sourceRoot":"","sources":["../../../src/helpers/iso/isoCBOR.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,QAAQ,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;GASG;AAEH;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAQzD;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,GAAG,UAAU,CAE3D"}

View File

@@ -0,0 +1,35 @@
/**
* A runtime-agnostic collection of methods for working with CBOR encoding
* @module
*/
import * as tinyCbor from '@levischuck/tiny-cbor';
/**
* Whatever CBOR encoder is used should keep CBOR data the same length when data is re-encoded
*
* MOST CRITICALLY, this means the following needs to be true of whatever CBOR library we use:
* - CBOR Map type values MUST decode to JavaScript Maps
* - CBOR tag 64 (uint8 Typed Array) MUST NOT be used when encoding Uint8Arrays back to CBOR
*
* So long as these requirements are maintained, then CBOR sequences can be encoded and decoded
* freely while maintaining their lengths for the most accurate pointer movement across them.
*/
/**
* Decode and return the first item in a sequence of CBOR-encoded values
*
* @param input The CBOR data to decode
* @param asObject (optional) Whether to convert any CBOR Maps into JavaScript Objects. Defaults to
* `false`
*/
export function decodeFirst(input) {
// Make a copy so we don't mutate the original
const _input = new Uint8Array(input);
const decoded = tinyCbor.decodePartialCBOR(_input, 0);
const [first] = decoded;
return first;
}
/**
* Encode data to CBOR
*/
export function encode(input) {
return tinyCbor.encodeCBOR(input);
}

View File

@@ -0,0 +1,9 @@
import { COSEALG } from '../../cose.js';
/**
* Generate a digest of the provided data.
*
* @param data The data to generate a digest of
* @param algorithm A COSE algorithm ID that maps to a desired SHA algorithm
*/
export declare function digest(data: Uint8Array, algorithm: COSEALG): Promise<Uint8Array>;
//# sourceMappingURL=digest.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../../../src/helpers/iso/isoCrypto/digest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAIxC;;;;;GAKG;AACH,wBAAsB,MAAM,CAC1B,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,OAAO,GACjB,OAAO,CAAC,UAAU,CAAC,CAQrB"}

View File

@@ -0,0 +1,14 @@
import { mapCoseAlgToWebCryptoAlg } from './mapCoseAlgToWebCryptoAlg.js';
import { getWebCrypto } from './getWebCrypto.js';
/**
* Generate a digest of the provided data.
*
* @param data The data to generate a digest of
* @param algorithm A COSE algorithm ID that maps to a desired SHA algorithm
*/
export async function digest(data, algorithm) {
const WebCrypto = await getWebCrypto();
const subtleAlgorithm = mapCoseAlgToWebCryptoAlg(algorithm);
const hashed = await WebCrypto.subtle.digest(subtleAlgorithm, data);
return new Uint8Array(hashed);
}

View File

@@ -0,0 +1,7 @@
/**
* Fill up the provided bytes array with random bytes equal to its length.
*
* @returns the same bytes array passed into the method
*/
export declare function getRandomValues(array: Uint8Array): Promise<Uint8Array>;
//# sourceMappingURL=getRandomValues.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"getRandomValues.d.ts","sourceRoot":"","sources":["../../../../src/helpers/iso/isoCrypto/getRandomValues.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAM5E"}

View File

@@ -0,0 +1,11 @@
import { getWebCrypto } from './getWebCrypto.js';
/**
* Fill up the provided bytes array with random bytes equal to its length.
*
* @returns the same bytes array passed into the method
*/
export async function getRandomValues(array) {
const WebCrypto = await getWebCrypto();
WebCrypto.getRandomValues(array);
return array;
}

View File

@@ -0,0 +1,14 @@
import type { Crypto } from '../../../types/index.js';
/**
* Try to get an instance of the Crypto API from the current runtime. Should support Node,
* as well as others, like Deno, that implement Web APIs.
*/
export declare function getWebCrypto(): Promise<Crypto>;
export declare class MissingWebCrypto extends Error {
constructor();
}
export declare const _getWebCryptoInternals: {
stubThisGlobalThisCrypto: () => import("crypto").webcrypto.Crypto;
setCachedCrypto: (newCrypto: Crypto | undefined) => void;
};
//# sourceMappingURL=getWebCrypto.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"getWebCrypto.d.ts","sourceRoot":"","sources":["../../../../src/helpers/iso/isoCrypto/getWebCrypto.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAItD;;;GAGG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAgC9C;AAED,qBAAa,gBAAiB,SAAQ,KAAK;;CAM1C;AAGD,eAAO,MAAM,sBAAsB;;iCAGJ,MAAM,GAAG,SAAS;CAGhD,CAAC"}

View File

@@ -0,0 +1,49 @@
let webCrypto = undefined;
/**
* Try to get an instance of the Crypto API from the current runtime. Should support Node,
* as well as others, like Deno, that implement Web APIs.
*/
export function getWebCrypto() {
/**
* Hello there! If you came here wondering why this method is asynchronous when use of
* `globalThis.crypto` is not, it's to minimize a bunch of refactor related to making this
* synchronous. For example, `generateRegistrationOptions()` and `generateAuthenticationOptions()`
* become synchronous if we make this synchronous (since nothing else in that method is async)
* which represents a breaking API change in this library's core API.
*
* TODO: If it's after February 2025 when you read this then consider whether it still makes sense
* to keep this method asynchronous.
*/
const toResolve = new Promise((resolve, reject) => {
if (webCrypto) {
return resolve(webCrypto);
}
/**
* Naively attempt to access Crypto as a global object, which popular ESM-centric run-times
* support (and Node v20+)
*/
const _globalThisCrypto = _getWebCryptoInternals.stubThisGlobalThisCrypto();
if (_globalThisCrypto) {
webCrypto = _globalThisCrypto;
return resolve(webCrypto);
}
// We tried to access it both in Node and globally, so bail out
return reject(new MissingWebCrypto());
});
return toResolve;
}
export class MissingWebCrypto extends Error {
constructor() {
const message = 'An instance of the Crypto API could not be located';
super(message);
this.name = 'MissingWebCrypto';
}
}
// Make it possible to stub return values during testing
export const _getWebCryptoInternals = {
stubThisGlobalThisCrypto: () => globalThis.crypto,
// Make it possible to reset the `webCrypto` at the top of the file
setCachedCrypto: (newCrypto) => {
webCrypto = newCrypto;
},
};

View File

@@ -0,0 +1,5 @@
export declare function importKey(opts: {
keyData: JsonWebKey;
algorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams;
}): Promise<CryptoKey>;
//# sourceMappingURL=importKey.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"importKey.d.ts","sourceRoot":"","sources":["../../../../src/helpers/iso/isoCrypto/importKey.ts"],"names":[],"mappings":"AAEA,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,OAAO,EAAE,UAAU,CAAC;IACpB,SAAS,EAAE,mBAAmB,GAAG,qBAAqB,GAAG,iBAAiB,CAAC;CAC5E,GAAG,OAAO,CAAC,SAAS,CAAC,CAQrB"}

View File

@@ -0,0 +1,8 @@
import { getWebCrypto } from './getWebCrypto.js';
export async function importKey(opts) {
const WebCrypto = await getWebCrypto();
const { keyData, algorithm } = opts;
return WebCrypto.subtle.importKey('jwk', keyData, algorithm, false, [
'verify',
]);
}

View File

@@ -0,0 +1,8 @@
/**
* A runtime-agnostic collection of methods for working with the WebCrypto API
* @module
*/
export { digest } from './digest.js';
export { getRandomValues } from './getRandomValues.js';
export { verify } from './verify.js';
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/helpers/iso/isoCrypto/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}

View File

@@ -0,0 +1,7 @@
/**
* A runtime-agnostic collection of methods for working with the WebCrypto API
* @module
*/
export { digest } from './digest.js';
export { getRandomValues } from './getRandomValues.js';
export { verify } from './verify.js';

View File

@@ -0,0 +1,7 @@
import { SubtleCryptoAlg } from './structs.js';
import { COSEALG } from '../../cose.js';
/**
* Convert a COSE alg ID into a corresponding string value that WebCrypto APIs expect
*/
export declare function mapCoseAlgToWebCryptoAlg(alg: COSEALG): SubtleCryptoAlg;
//# sourceMappingURL=mapCoseAlgToWebCryptoAlg.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"mapCoseAlgToWebCryptoAlg.d.ts","sourceRoot":"","sources":["../../../../src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG,eAAe,CAetE"}

View File

@@ -0,0 +1,20 @@
import { COSEALG } from '../../cose.js';
/**
* Convert a COSE alg ID into a corresponding string value that WebCrypto APIs expect
*/
export function mapCoseAlgToWebCryptoAlg(alg) {
if ([COSEALG.RS1].indexOf(alg) >= 0) {
return 'SHA-1';
}
else if ([COSEALG.ES256, COSEALG.PS256, COSEALG.RS256].indexOf(alg) >= 0) {
return 'SHA-256';
}
else if ([COSEALG.ES384, COSEALG.PS384, COSEALG.RS384].indexOf(alg) >= 0) {
return 'SHA-384';
}
else if ([COSEALG.ES512, COSEALG.PS512, COSEALG.RS512, COSEALG.EdDSA].indexOf(alg) >=
0) {
return 'SHA-512';
}
throw new Error(`Could not map COSE alg value of ${alg} to a WebCrypto alg`);
}

View File

@@ -0,0 +1,7 @@
import { COSEALG } from '../../cose.js';
import { SubtleCryptoKeyAlgName } from './structs.js';
/**
* Convert a COSE alg ID into a corresponding key algorithm string value that WebCrypto APIs expect
*/
export declare function mapCoseAlgToWebCryptoKeyAlgName(alg: COSEALG): SubtleCryptoKeyAlgName;
//# sourceMappingURL=mapCoseAlgToWebCryptoKeyAlgName.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"mapCoseAlgToWebCryptoKeyAlgName.d.ts","sourceRoot":"","sources":["../../../../src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAEtD;;GAEG;AACH,wBAAgB,+BAA+B,CAC7C,GAAG,EAAE,OAAO,GACX,sBAAsB,CAoBxB"}

View File

@@ -0,0 +1,19 @@
import { COSEALG } from '../../cose.js';
/**
* Convert a COSE alg ID into a corresponding key algorithm string value that WebCrypto APIs expect
*/
export function mapCoseAlgToWebCryptoKeyAlgName(alg) {
if ([COSEALG.EdDSA].indexOf(alg) >= 0) {
return 'Ed25519';
}
else if ([COSEALG.ES256, COSEALG.ES384, COSEALG.ES512, COSEALG.ES256K].indexOf(alg) >= 0) {
return 'ECDSA';
}
else if ([COSEALG.RS256, COSEALG.RS384, COSEALG.RS512, COSEALG.RS1].indexOf(alg) >= 0) {
return 'RSASSA-PKCS1-v1_5';
}
else if ([COSEALG.PS256, COSEALG.PS384, COSEALG.PS512].indexOf(alg) >= 0) {
return 'RSA-PSS';
}
throw new Error(`Could not map COSE alg value of ${alg} to a WebCrypto key alg name`);
}

View File

@@ -0,0 +1,4 @@
export type SubtleCryptoAlg = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
export type SubtleCryptoCrv = 'P-256' | 'P-384' | 'P-521' | 'Ed25519';
export type SubtleCryptoKeyAlgName = 'ECDSA' | 'Ed25519' | 'RSASSA-PKCS1-v1_5' | 'RSA-PSS';
//# sourceMappingURL=structs.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"structs.d.ts","sourceRoot":"","sources":["../../../../src/helpers/iso/isoCrypto/structs.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAC1E,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;AACtE,MAAM,MAAM,sBAAsB,GAC9B,OAAO,GACP,SAAS,GACT,mBAAmB,GACnB,SAAS,CAAC"}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,8 @@
import { COSECRV } from '../../cose.js';
/**
* In WebAuthn, EC2 signatures are wrapped in ASN.1 structure so we need to peel r and s apart.
*
* See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types
*/
export declare function unwrapEC2Signature(signature: Uint8Array, crv: COSECRV): Uint8Array;
//# sourceMappingURL=unwrapEC2Signature.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"unwrapEC2Signature.d.ts","sourceRoot":"","sources":["../../../../src/helpers/iso/isoCrypto/unwrapEC2Signature.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAGxC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,GAAG,UAAU,CAelF"}

View File

@@ -0,0 +1,73 @@
import { AsnParser } from '@peculiar/asn1-schema';
import { ECDSASigValue } from '@peculiar/asn1-ecc';
import { COSECRV } from '../../cose.js';
import { isoUint8Array } from '../index.js';
/**
* In WebAuthn, EC2 signatures are wrapped in ASN.1 structure so we need to peel r and s apart.
*
* See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types
*/
export function unwrapEC2Signature(signature, crv) {
const parsedSignature = AsnParser.parse(signature, ECDSASigValue);
const rBytes = new Uint8Array(parsedSignature.r);
const sBytes = new Uint8Array(parsedSignature.s);
const componentLength = getSignatureComponentLength(crv);
const rNormalizedBytes = toNormalizedBytes(rBytes, componentLength);
const sNormalizedBytes = toNormalizedBytes(sBytes, componentLength);
const finalSignature = isoUint8Array.concat([
rNormalizedBytes,
sNormalizedBytes,
]);
return finalSignature;
}
/**
* The SubtleCrypto Web Crypto API expects ECDSA signatures with `r` and `s` values to be encoded
* to a specific length depending on the order of the curve. This function returns the expected
* byte-length for each of the `r` and `s` signature components.
*
* See <https://www.w3.org/TR/WebCryptoAPI/#ecdsa-operations>
*/
function getSignatureComponentLength(crv) {
switch (crv) {
case COSECRV.P256:
return 32;
case COSECRV.P384:
return 48;
case COSECRV.P521:
return 66;
default:
throw new Error(`Unexpected COSE crv value of ${crv} (EC2)`);
}
}
/**
* Converts the ASN.1 integer representation to bytes of a specific length `n`.
*
* DER encodes integers as big-endian byte arrays, with as small as possible representation and
* requires a leading `0` byte to disambiguate between negative and positive numbers. This means
* that `r` and `s` can potentially not be the expected byte-length that is needed by the
* SubtleCrypto Web Crypto API: if there are leading `0`s it can be shorter than expected, and if
* it has a leading `1` bit, it can be one byte longer.
*
* See <https://www.itu.int/rec/T-REC-X.690-202102-I/en>
* See <https://www.w3.org/TR/WebCryptoAPI/#ecdsa-operations>
*/
function toNormalizedBytes(bytes, componentLength) {
let normalizedBytes;
if (bytes.length < componentLength) {
// In case the bytes are shorter than expected, we need to pad it with leading `0`s.
normalizedBytes = new Uint8Array(componentLength);
normalizedBytes.set(bytes, componentLength - bytes.length);
}
else if (bytes.length === componentLength) {
normalizedBytes = bytes;
}
else if (bytes.length === componentLength + 1 && bytes[0] === 0 && (bytes[1] & 0x80) === 0x80) {
// The bytes contain a leading `0` to encode that the integer is positive. This leading `0`
// needs to be removed for compatibility with the SubtleCrypto Web Crypto API.
normalizedBytes = bytes.subarray(1);
}
else {
throw new Error(`Invalid signature component length ${bytes.length}, expected ${componentLength}`);
}
return normalizedBytes;
}

View File

@@ -0,0 +1,11 @@
import { COSEALG, COSEPublicKey } from '../../cose.js';
/**
* Verify signatures with their public key. Supports EC2 and RSA public keys.
*/
export declare function verify(opts: {
cosePublicKey: COSEPublicKey;
signature: Uint8Array;
data: Uint8Array;
shaHashOverride?: COSEALG;
}): Promise<boolean>;
//# sourceMappingURL=verify.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../../src/helpers/iso/isoCrypto/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EAEP,aAAa,EAKd,MAAM,eAAe,CAAC;AAMvB;;GAEG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE;IAC3B,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,UAAU,CAAC;IACtB,IAAI,EAAE,UAAU,CAAC;IACjB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,OAAO,CAAC,OAAO,CAAC,CAyBnB"}

View File

@@ -0,0 +1,32 @@
import { COSEKEYS, isCOSECrv, isCOSEPublicKeyEC2, isCOSEPublicKeyOKP, isCOSEPublicKeyRSA, } from '../../cose.js';
import { verifyEC2 } from './verifyEC2.js';
import { verifyRSA } from './verifyRSA.js';
import { verifyOKP } from './verifyOKP.js';
import { unwrapEC2Signature } from './unwrapEC2Signature.js';
/**
* Verify signatures with their public key. Supports EC2 and RSA public keys.
*/
export function verify(opts) {
const { cosePublicKey, signature, data, shaHashOverride } = opts;
if (isCOSEPublicKeyEC2(cosePublicKey)) {
const crv = cosePublicKey.get(COSEKEYS.crv);
if (!isCOSECrv(crv)) {
throw new Error(`unknown COSE curve ${crv}`);
}
const unwrappedSignature = unwrapEC2Signature(signature, crv);
return verifyEC2({
cosePublicKey,
signature: unwrappedSignature,
data,
shaHashOverride,
});
}
else if (isCOSEPublicKeyRSA(cosePublicKey)) {
return verifyRSA({ cosePublicKey, signature, data, shaHashOverride });
}
else if (isCOSEPublicKeyOKP(cosePublicKey)) {
return verifyOKP({ cosePublicKey, signature, data });
}
const kty = cosePublicKey.get(COSEKEYS.kty);
throw new Error(`Signature verification with public key of kty ${kty} is not supported by this method`);
}

View File

@@ -0,0 +1,11 @@
import { COSEALG, COSEPublicKeyEC2 } from '../../cose.js';
/**
* Verify a signature using an EC2 public key
*/
export declare function verifyEC2(opts: {
cosePublicKey: COSEPublicKeyEC2;
signature: Uint8Array;
data: Uint8Array;
shaHashOverride?: COSEALG;
}): Promise<boolean>;
//# sourceMappingURL=verifyEC2.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"verifyEC2.d.ts","sourceRoot":"","sources":["../../../../src/helpers/iso/isoCrypto/verifyEC2.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAqB,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAO7E;;GAEG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,aAAa,EAAE,gBAAgB,CAAC;IAChC,SAAS,EAAE,UAAU,CAAC;IACtB,IAAI,EAAE,UAAU,CAAC;IACjB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,OAAO,CAAC,OAAO,CAAC,CA0EnB"}

View File

@@ -0,0 +1,73 @@
import { COSECRV, COSEKEYS } from '../../cose.js';
import { mapCoseAlgToWebCryptoAlg } from './mapCoseAlgToWebCryptoAlg.js';
import { importKey } from './importKey.js';
import { isoBase64URL } from '../index.js';
import { getWebCrypto } from './getWebCrypto.js';
/**
* Verify a signature using an EC2 public key
*/
export async function verifyEC2(opts) {
const { cosePublicKey, signature, data, shaHashOverride } = opts;
const WebCrypto = await getWebCrypto();
// Import the public key
const alg = cosePublicKey.get(COSEKEYS.alg);
const crv = cosePublicKey.get(COSEKEYS.crv);
const x = cosePublicKey.get(COSEKEYS.x);
const y = cosePublicKey.get(COSEKEYS.y);
if (!alg) {
throw new Error('Public key was missing alg (EC2)');
}
if (!crv) {
throw new Error('Public key was missing crv (EC2)');
}
if (!x) {
throw new Error('Public key was missing x (EC2)');
}
if (!y) {
throw new Error('Public key was missing y (EC2)');
}
let _crv;
if (crv === COSECRV.P256) {
_crv = 'P-256';
}
else if (crv === COSECRV.P384) {
_crv = 'P-384';
}
else if (crv === COSECRV.P521) {
_crv = 'P-521';
}
else {
throw new Error(`Unexpected COSE crv value of ${crv} (EC2)`);
}
const keyData = {
kty: 'EC',
crv: _crv,
x: isoBase64URL.fromBuffer(x),
y: isoBase64URL.fromBuffer(y),
ext: false,
};
const keyAlgorithm = {
/**
* Note to future self: you can't use `mapCoseAlgToWebCryptoKeyAlgName()` here because some
* leaf certs from actual devices specified an RSA SHA value for `alg` (e.g. `-257`) which
* would then map here to `'RSASSA-PKCS1-v1_5'`. We always want `'ECDSA'` here so we'll
* hard-code this.
*/
name: 'ECDSA',
namedCurve: _crv,
};
const key = await importKey({
keyData,
algorithm: keyAlgorithm,
});
// Determine which SHA algorithm to use for signature verification
let subtleAlg = mapCoseAlgToWebCryptoAlg(alg);
if (shaHashOverride) {
subtleAlg = mapCoseAlgToWebCryptoAlg(shaHashOverride);
}
const verifyAlgorithm = {
name: 'ECDSA',
hash: { name: subtleAlg },
};
return WebCrypto.subtle.verify(verifyAlgorithm, key, signature, data);
}

View File

@@ -0,0 +1,7 @@
import { COSEPublicKeyOKP } from '../../cose.js';
export declare function verifyOKP(opts: {
cosePublicKey: COSEPublicKeyOKP;
signature: Uint8Array;
data: Uint8Array;
}): Promise<boolean>;
//# sourceMappingURL=verifyOKP.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"verifyOKP.d.ts","sourceRoot":"","sources":["../../../../src/helpers/iso/isoCrypto/verifyOKP.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,gBAAgB,EAAa,MAAM,eAAe,CAAC;AAM/E,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,aAAa,EAAE,gBAAgB,CAAC;IAChC,SAAS,EAAE,UAAU,CAAC;IACtB,IAAI,EAAE,UAAU,CAAC;CAClB,GAAG,OAAO,CAAC,OAAO,CAAC,CAyDnB"}

Some files were not shown because too many files have changed in this diff Show More