@simplewebauthn/server
Advanced tools
Comparing version 0.6.1 to 0.7.0
@@ -8,3 +8,3 @@ import { AssertionCredentialJSON, AuthenticatorDevice } from '@simplewebauthn/typescript-types'; | ||
authenticator: AuthenticatorDevice; | ||
requireUserVerification?: boolean; | ||
fidoUserVerification?: UserVerificationRequirement; | ||
}; | ||
@@ -22,4 +22,5 @@ /** | ||
* @param authenticator An internal {@link AuthenticatorDevice} matching the credential's ID | ||
* @param requireUserVerification (Optional) Enforce user verification by the authenticator | ||
* (via PIN, fingerprint, etc...) | ||
* @param fidoUserVerification (Optional) The value specified for `userVerification` when calling | ||
* `generateAssertionOptions()`. Activates FIDO-specific user presence and verification checks. | ||
* Omitting this value defaults verification to a WebAuthn-specific user presence requirement. | ||
*/ | ||
@@ -26,0 +27,0 @@ export default function verifyAssertionResponse(options: Options): VerifiedAssertion; |
@@ -12,2 +12,3 @@ "use strict"; | ||
const parseAuthenticatorData_1 = __importDefault(require("../helpers/parseAuthenticatorData")); | ||
const isBase64URLString_1 = __importDefault(require("../helpers/isBase64URLString")); | ||
/** | ||
@@ -24,10 +25,29 @@ * Verify that the user has legitimately completed the login process | ||
* @param authenticator An internal {@link AuthenticatorDevice} matching the credential's ID | ||
* @param requireUserVerification (Optional) Enforce user verification by the authenticator | ||
* (via PIN, fingerprint, etc...) | ||
* @param fidoUserVerification (Optional) The value specified for `userVerification` when calling | ||
* `generateAssertionOptions()`. Activates FIDO-specific user presence and verification checks. | ||
* Omitting this value defaults verification to a WebAuthn-specific user presence requirement. | ||
*/ | ||
function verifyAssertionResponse(options) { | ||
const { credential, expectedChallenge, expectedOrigin, expectedRPID, authenticator, requireUserVerification = false, } = options; | ||
const { response } = credential; | ||
const { credential, expectedChallenge, expectedOrigin, expectedRPID, authenticator, fidoUserVerification, } = options; | ||
const { id, rawId, type: credentialType, response } = credential; | ||
// 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 (response === null || response === void 0 ? void 0 : response.clientDataJSON) !== 'string') { | ||
throw new Error('Credential response clientDataJSON was not a string'); | ||
} | ||
const clientDataJSON = decodeClientDataJSON_1.default(response.clientDataJSON); | ||
const { type, origin, challenge } = clientDataJSON; | ||
const { type, origin, challenge, tokenBinding } = clientDataJSON; | ||
// Make sure we're handling an assertion | ||
@@ -37,4 +57,6 @@ if (type !== 'webauthn.get') { | ||
} | ||
if (challenge !== expectedChallenge) { | ||
throw new Error(`Unexpected assertion challenge "${challenge}", expected "${expectedChallenge}"`); | ||
// Ensure the device provided the challenge we gave it | ||
const encodedExpectedChallenge = base64url_1.default.encode(expectedChallenge); | ||
if (challenge !== encodedExpectedChallenge) { | ||
throw new Error(`Unexpected assertion challenge "${challenge}", expected "${encodedExpectedChallenge}"`); | ||
} | ||
@@ -45,2 +67,19 @@ // Check that the origin is our site | ||
} | ||
if (!isBase64URLString_1.default(response.authenticatorData)) { | ||
throw new Error('Credential response authenticatorData was not a base64url string'); | ||
} | ||
if (!isBase64URLString_1.default(response.signature)) { | ||
throw new Error('Credential response signature was not a base64url string'); | ||
} | ||
if (response.userHandle && typeof response.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 = base64url_1.default.toBuffer(response.authenticatorData); | ||
@@ -54,9 +93,19 @@ const parsedAuthData = parseAuthenticatorData_1.default(authDataBuffer); | ||
} | ||
// Make sure someone was physically present | ||
if (!flags.up) { | ||
throw new Error('User not present during assertion'); | ||
// Enforce user verification if required | ||
if (fidoUserVerification) { | ||
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` | ||
} | ||
} | ||
// Enforce user verification if specified | ||
if (requireUserVerification && !flags.uv) { | ||
throw new Error('User verification required, but user could not be verified'); | ||
else { | ||
// WebAuthn only requires the user presence flag be true | ||
if (!flags.up) { | ||
throw new Error('User not present during assertion'); | ||
} | ||
} | ||
@@ -63,0 +112,0 @@ const clientDataHash = toHash_1.default(base64url_1.default.toBuffer(response.clientDataJSON)); |
@@ -16,3 +16,3 @@ import type { PublicKeyCredentialCreationOptionsJSON, Base64URLString } from '@simplewebauthn/typescript-types'; | ||
}; | ||
export declare const supportedCOSEAlgorithIdentifiers: COSEAlgorithmIdentifier[]; | ||
export declare const supportedCOSEAlgorithmIdentifiers: COSEAlgorithmIdentifier[]; | ||
/** | ||
@@ -19,0 +19,0 @@ * Prepare a value to pass into navigator.credentials.create(...) for authenticator "registration" |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.supportedCOSEAlgorithIdentifiers = void 0; | ||
exports.supportedCOSEAlgorithmIdentifiers = void 0; | ||
// Supported crypto algo identifiers | ||
// See https://w3c.github.io/webauthn/#sctn-alg-identifier | ||
exports.supportedCOSEAlgorithIdentifiers = [-7, -35, -36, -8]; | ||
exports.supportedCOSEAlgorithmIdentifiers = [ | ||
-7, | ||
-8, | ||
-36, | ||
-37, | ||
-38, | ||
-39, | ||
-257, | ||
-258, | ||
-259, | ||
-65535, | ||
]; | ||
/** | ||
@@ -40,3 +51,3 @@ * Prepare a value to pass into navigator.credentials.create(...) for authenticator "registration" | ||
}, | ||
pubKeyCredParams: exports.supportedCOSEAlgorithIdentifiers.map(id => ({ | ||
pubKeyCredParams: exports.supportedCOSEAlgorithmIdentifiers.map(id => ({ | ||
alg: id, | ||
@@ -43,0 +54,0 @@ type: 'public-key', |
@@ -7,2 +7,4 @@ /// <reference types="node" /> | ||
authData: Buffer; | ||
aaguid: Buffer; | ||
verifyTimestampMS?: boolean; | ||
}; | ||
@@ -12,3 +14,3 @@ /** | ||
*/ | ||
export default function verifyAttestationAndroidSafetyNet(options: Options): boolean; | ||
export default function verifyAttestationAndroidSafetyNet(options: Options): Promise<boolean>; | ||
export {}; |
@@ -10,12 +10,20 @@ "use strict"; | ||
const getCertificateInfo_1 = __importDefault(require("../../helpers/getCertificateInfo")); | ||
const validateCertificatePath_1 = __importDefault(require("../../helpers/validateCertificatePath")); | ||
const convertASN1toPEM_1 = __importDefault(require("../../helpers/convertASN1toPEM")); | ||
const metadataService_1 = __importDefault(require("../../metadata/metadataService")); | ||
const verifyAttestationWithMetadata_1 = __importDefault(require("../../metadata/verifyAttestationWithMetadata")); | ||
/** | ||
* Verify an attestation response with fmt 'android-safetynet' | ||
*/ | ||
function verifyAttestationAndroidSafetyNet(options) { | ||
const { attStmt, clientDataHash, authData } = options; | ||
if (!attStmt.response) { | ||
async function verifyAttestationAndroidSafetyNet(options) { | ||
const { attStmt, clientDataHash, authData, aaguid, verifyTimestampMS = true } = options; | ||
const { response, ver } = attStmt; | ||
if (!ver) { | ||
throw new Error('No ver value in attestation (SafetyNet)'); | ||
} | ||
if (!response) { | ||
throw new Error('No response was included in attStmt by authenticator (SafetyNet)'); | ||
} | ||
// Prepare to verify a JWT | ||
const jwt = attStmt.response.toString('utf8'); | ||
const jwt = response.toString('utf8'); | ||
const jwtParts = jwt.split('.'); | ||
@@ -28,3 +36,16 @@ const HEADER = JSON.parse(base64url_1.default.decode(jwtParts[0])); | ||
*/ | ||
const { nonce, ctsProfileMatch } = PAYLOAD; | ||
const { nonce, ctsProfileMatch, timestampMs } = PAYLOAD; | ||
if (verifyTimestampMS) { | ||
// Make sure timestamp is in the past | ||
let now = Date.now(); | ||
if (timestampMs > Date.now()) { | ||
throw new Error(`Payload timestamp "${timestampMs}" was later than "${now}" (SafetyNet)`); | ||
} | ||
// Consider a SafetyNet attestation valid within a minute of it being performed | ||
const timestampPlusDelay = timestampMs + 60 * 1000; | ||
now = Date.now(); | ||
if (timestampPlusDelay < now) { | ||
throw new Error(`Payload timestamp "${timestampPlusDelay}" has expired (SafetyNet)`); | ||
} | ||
} | ||
const nonceBase = Buffer.concat([authData, clientDataHash]); | ||
@@ -45,20 +66,31 @@ const nonceBuffer = toHash_1.default(nonceBase); | ||
*/ | ||
// Generate an array of certs constituting a full certificate chain | ||
const fullpathCert = HEADER.x5c.concat([GlobalSignRootCAR2]).map(cert => { | ||
let pem = ''; | ||
// Take a string of characters and chop them up into 64-char lines (just like a PEM cert) | ||
for (let i = 0; i < cert.length; i += 64) { | ||
pem += `${cert.slice(i, i + 64)}\n`; | ||
} | ||
return `-----BEGIN CERTIFICATE-----\n${pem}-----END CERTIFICATE-----`; | ||
}); | ||
const certificate = fullpathCert[0]; | ||
const commonCertInfo = getCertificateInfo_1.default(certificate); | ||
const { subject } = commonCertInfo; | ||
// TODO: Find out where this CN string is specified and if it might change | ||
const leafCert = convertASN1toPEM_1.default(HEADER.x5c[0]); | ||
const leafCertInfo = getCertificateInfo_1.default(leafCert); | ||
const { subject } = leafCertInfo; | ||
// Ensure the certificate was issued to this hostname | ||
// See https://developer.android.com/training/safetynet/attestation#verify-attestation-response | ||
if (subject.CN !== 'attest.android.com') { | ||
throw new Error('Certificate common name was not "attest.android.com" (SafetyNet)'); | ||
} | ||
// TODO: Re-investigate this if we decide to "use MDS or Metadata Statements" | ||
// validateCertificatePath(fullpathCert); | ||
const statement = await metadataService_1.default.getStatement(aaguid); | ||
if (statement) { | ||
try { | ||
// Convert from alg in JWT header to a number in the metadata | ||
const alg = HEADER.alg === 'RS256' ? -257 : -99999; | ||
await verifyAttestationWithMetadata_1.default(statement, alg, HEADER.x5c); | ||
} | ||
catch (err) { | ||
throw new Error(`${err.message} (SafetyNet)`); | ||
} | ||
} | ||
else { | ||
// Validate certificate path using a fixed global root cert | ||
const path = HEADER.x5c.concat([GlobalSignRootCAR2]).map(convertASN1toPEM_1.default); | ||
try { | ||
await validateCertificatePath_1.default(path); | ||
} | ||
catch (err) { | ||
throw new Error(`${err.message} (SafetyNet)`); | ||
} | ||
} | ||
/** | ||
@@ -72,3 +104,3 @@ * END Verify Header | ||
const signatureBuffer = base64url_1.default.toBuffer(SIGNATURE); | ||
const verified = verifySignature_1.default(signatureBuffer, signatureBaseBuffer, certificate); | ||
const verified = verifySignature_1.default(signatureBuffer, signatureBaseBuffer, leafCert); | ||
/** | ||
@@ -75,0 +107,0 @@ * END Verify Signature |
@@ -9,2 +9,3 @@ /// <reference types="node" /> | ||
credentialPublicKey: Buffer; | ||
aaguid: Buffer; | ||
}; | ||
@@ -11,0 +12,0 @@ /** |
@@ -13,3 +13,3 @@ "use strict"; | ||
function verifyAttestationFIDOU2F(options) { | ||
const { attStmt, clientDataHash, rpIdHash, credentialID, credentialPublicKey } = options; | ||
const { attStmt, clientDataHash, rpIdHash, credentialID, credentialPublicKey, aaguid = '', } = options; | ||
const reservedByte = Buffer.from([0x00]); | ||
@@ -31,2 +31,7 @@ const publicKey = convertCOSEtoPKCS_1.default(credentialPublicKey); | ||
} | ||
// FIDO spec says that aaguid _must_ equal 0x00 here to be legit | ||
const aaguidToHex = Number.parseInt(aaguid.toString('hex'), 16); | ||
if (aaguidToHex !== 0x00) { | ||
throw new Error(`AAGUID "${aaguidToHex}" was not expected value`); | ||
} | ||
const publicKeyCertPEM = convertASN1toPEM_1.default(x5c[0]); | ||
@@ -33,0 +38,0 @@ return verifySignature_1.default(sig, signatureBase, publicKeyCertPEM); |
@@ -8,2 +8,3 @@ /// <reference types="node" /> | ||
credentialPublicKey: Buffer; | ||
aaguid: Buffer; | ||
}; | ||
@@ -13,3 +14,3 @@ /** | ||
*/ | ||
export default function verifyAttestationPacked(options: Options): boolean; | ||
export default function verifyAttestationPacked(options: Options): Promise<boolean>; | ||
export {}; |
@@ -28,2 +28,3 @@ "use strict"; | ||
const convertCOSEtoPKCS_1 = __importStar(require("../../helpers/convertCOSEtoPKCS")); | ||
const constants_1 = require("../../helpers/constants"); | ||
const toHash_1 = __importDefault(require("../../helpers/toHash")); | ||
@@ -34,11 +35,16 @@ const convertASN1toPEM_1 = __importDefault(require("../../helpers/convertASN1toPEM")); | ||
const decodeCredentialPublicKey_1 = __importDefault(require("../../helpers/decodeCredentialPublicKey")); | ||
const metadataService_1 = __importDefault(require("../../metadata/metadataService")); | ||
const verifyAttestationWithMetadata_1 = __importDefault(require("../../metadata/verifyAttestationWithMetadata")); | ||
/** | ||
* Verify an attestation response with fmt 'packed' | ||
*/ | ||
function verifyAttestationPacked(options) { | ||
const { attStmt, clientDataHash, authData, credentialPublicKey } = options; | ||
const { sig, x5c } = attStmt; | ||
async function verifyAttestationPacked(options) { | ||
const { attStmt, clientDataHash, authData, credentialPublicKey, aaguid } = options; | ||
const { sig, x5c, alg } = attStmt; | ||
if (!sig) { | ||
throw new Error('No attestation signature provided in attestation statement (Packed)'); | ||
} | ||
if (typeof alg !== 'number') { | ||
throw new Error(`Attestation Statement alg "${alg}" is not a number (Packed)`); | ||
} | ||
const signatureBase = Buffer.concat([authData, clientDataHash]); | ||
@@ -49,23 +55,48 @@ let verified = false; | ||
const leafCert = convertASN1toPEM_1.default(x5c[0]); | ||
const leafCertInfo = getCertificateInfo_1.default(leafCert); | ||
const { subject, basicConstraintsCA, version } = leafCertInfo; | ||
const { subject, basicConstraintsCA, version, notBefore, notAfter } = getCertificateInfo_1.default(leafCert); | ||
const { OU, CN, O, C } = subject; | ||
if (OU !== 'Authenticator Attestation') { | ||
throw new Error('Batch certificate OU was not "Authenticator Attestation" (Packed|Full'); | ||
throw new Error('Certificate OU was not "Authenticator Attestation" (Packed|Full)'); | ||
} | ||
if (!CN) { | ||
throw new Error('Batch certificate CN was empty (Packed|Full'); | ||
throw new Error('Certificate CN was empty (Packed|Full)'); | ||
} | ||
if (!O) { | ||
throw new Error('Batch certificate CN was empty (Packed|Full'); | ||
throw new Error('Certificate O was empty (Packed|Full)'); | ||
} | ||
if (!C || C.length !== 2) { | ||
throw new Error('Batch certificate C was not two-character ISO 3166 code (Packed|Full'); | ||
throw new Error('Certificate C was not two-character ISO 3166 code (Packed|Full)'); | ||
} | ||
if (basicConstraintsCA) { | ||
throw new Error('Batch certificate basic constraints CA was not `false` (Packed|Full'); | ||
throw new Error('Certificate basic constraints CA was not `false` (Packed|Full)'); | ||
} | ||
if (version !== 3) { | ||
throw new Error('Batch certificate version was not `3` (ASN.1 value of 2) (Packed|Full'); | ||
throw new Error('Certificate version was not `3` (ASN.1 value of 2) (Packed|Full)'); | ||
} | ||
let now = new Date(); | ||
if (notBefore > now) { | ||
throw new Error(`Certificate not good before "${notBefore.toString()}" (Packed|Full)`); | ||
} | ||
now = new Date(); | ||
if (notAfter < now) { | ||
throw new Error(`Certificate not good after "${notAfter.toString()}" (Packed|Full)`); | ||
} | ||
// TODO: If certificate contains id-fido-gen-ce-aaguid(1.3.6.1.4.1.45724.1.1.4) extension, check | ||
// that it’s value is set to the same AAGUID as in authData. | ||
// If available, validate attestation alg and x5c with info in the metadata statement | ||
const statement = await metadataService_1.default.getStatement(aaguid); | ||
if (statement) { | ||
// The presence of x5c means this is a full attestation. Check to see if attestationTypes | ||
// includes packed attestations. | ||
if (statement.attestationTypes.indexOf(constants_1.FIDO_METADATA_ATTESTATION_TYPES.ATTESTATION_BASIC_FULL) < | ||
0) { | ||
throw new Error('Metadata does not indicate support for full attestations (Packed|Full)'); | ||
} | ||
try { | ||
await verifyAttestationWithMetadata_1.default(statement, alg, x5c); | ||
} | ||
catch (err) { | ||
throw new Error(`${err.message} (Packed|Full)`); | ||
} | ||
} | ||
verified = verifySignature_1.default(sig, signatureBase, leafCert); | ||
@@ -76,11 +107,7 @@ } | ||
const kty = cosePublicKey.get(convertCOSEtoPKCS_1.COSEKEYS.kty); | ||
const alg = cosePublicKey.get(convertCOSEtoPKCS_1.COSEKEYS.alg); | ||
if (!alg) { | ||
throw new Error('COSE public key was missing alg (Packed|Self)'); | ||
} | ||
if (!kty) { | ||
throw new Error('COSE public key was missing kty (Packed|Self)'); | ||
} | ||
const hashAlg = COSEALGHASH[alg]; | ||
if (kty === COSEKTY.EC2) { | ||
const hashAlg = convertCOSEtoPKCS_1.COSEALGHASH[alg]; | ||
if (kty === convertCOSEtoPKCS_1.COSEKTY.EC2) { | ||
const crv = cosePublicKey.get(convertCOSEtoPKCS_1.COSEKEYS.crv); | ||
@@ -100,7 +127,7 @@ if (!crv) { | ||
*/ | ||
const ec = new elliptic_1.default.ec(COSECRV[crv]); | ||
const ec = new elliptic_1.default.ec(convertCOSEtoPKCS_1.COSECRV[crv]); | ||
const key = ec.keyFromPublic(pkcsPublicKey); | ||
verified = key.verify(signatureBaseHash, sig); | ||
} | ||
else if (kty === COSEKTY.RSA) { | ||
else if (kty === convertCOSEtoPKCS_1.COSEKTY.RSA) { | ||
const n = cosePublicKey.get(convertCOSEtoPKCS_1.COSEKEYS.n); | ||
@@ -110,3 +137,3 @@ if (!n) { | ||
} | ||
const signingScheme = COSERSASCHEME[alg]; | ||
const signingScheme = convertCOSEtoPKCS_1.COSERSASCHEME[alg]; | ||
// TODO: Verify this works | ||
@@ -121,3 +148,3 @@ const key = new node_rsa_1.default(); | ||
} | ||
else if (kty === COSEKTY.OKP) { | ||
else if (kty === convertCOSEtoPKCS_1.COSEKTY.OKP) { | ||
const x = cosePublicKey.get(convertCOSEtoPKCS_1.COSEKEYS.x); | ||
@@ -137,40 +164,2 @@ if (!x) { | ||
exports.default = verifyAttestationPacked; | ||
var COSEKTY; | ||
(function (COSEKTY) { | ||
COSEKTY[COSEKTY["OKP"] = 1] = "OKP"; | ||
COSEKTY[COSEKTY["EC2"] = 2] = "EC2"; | ||
COSEKTY[COSEKTY["RSA"] = 3] = "RSA"; | ||
})(COSEKTY || (COSEKTY = {})); | ||
const COSERSASCHEME = { | ||
'-3': 'pss-sha256', | ||
'-39': 'pss-sha512', | ||
'-38': 'pss-sha384', | ||
'-65535': 'pkcs1-sha1', | ||
'-257': 'pkcs1-sha256', | ||
'-258': 'pkcs1-sha384', | ||
'-259': 'pkcs1-sha512', | ||
}; | ||
// See https://w3c.github.io/webauthn/#sctn-alg-identifier | ||
const COSECRV = { | ||
// alg: -7 | ||
1: 'p256', | ||
// alg: -35 | ||
2: 'p384', | ||
// alg: -36 | ||
3: 'p521', | ||
// alg: -8 | ||
6: 'ed25519', | ||
}; | ||
const COSEALGHASH = { | ||
'-257': 'sha256', | ||
'-258': 'sha384', | ||
'-259': 'sha512', | ||
'-65535': 'sha1', | ||
'-39': 'sha512', | ||
'-38': 'sha384', | ||
'-37': 'sha256', | ||
'-7': 'sha256', | ||
'-8': 'sha512', | ||
'-36': 'sha512', | ||
}; | ||
//# sourceMappingURL=verifyPacked.js.map |
@@ -7,3 +7,3 @@ import { AttestationCredentialJSON } from '@simplewebauthn/typescript-types'; | ||
expectedOrigin: string; | ||
expectedRPID: string; | ||
expectedRPID?: string; | ||
requireUserVerification?: boolean; | ||
@@ -24,3 +24,3 @@ }; | ||
*/ | ||
export default function verifyAttestationResponse(options: Options): VerifiedAttestation; | ||
export default function verifyAttestationResponse(options: Options): Promise<VerifiedAttestation>; | ||
/** | ||
@@ -27,0 +27,0 @@ * Result of attestation verification |
@@ -36,2 +36,4 @@ "use strict"; | ||
const verifyAndroidSafetyNet_1 = __importDefault(require("./verifications/verifyAndroidSafetyNet")); | ||
const verifyTPM_1 = __importDefault(require("./verifications/tpm/verifyTPM")); | ||
const verifyAndroidKey_1 = __importDefault(require("./verifications/verifyAndroidKey")); | ||
/** | ||
@@ -50,7 +52,19 @@ * Verify that the user has legitimately completed the registration process | ||
*/ | ||
function verifyAttestationResponse(options) { | ||
async function verifyAttestationResponse(options) { | ||
const { credential, expectedChallenge, expectedOrigin, expectedRPID, requireUserVerification = false, } = options; | ||
const { response } = credential; | ||
const { id, rawId, type: credentialType, response } = credential; | ||
// 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"`); | ||
} | ||
const clientDataJSON = decodeClientDataJSON_1.default(response.clientDataJSON); | ||
const { type, origin, challenge } = clientDataJSON; | ||
const { type, origin, challenge, tokenBinding } = clientDataJSON; | ||
// Make sure we're handling an attestation | ||
@@ -61,4 +75,5 @@ if (type !== 'webauthn.create') { | ||
// Ensure the device provided the challenge we gave it | ||
if (challenge !== expectedChallenge) { | ||
throw new Error(`Unexpected attestation challenge "${challenge}", expected "${expectedChallenge}"`); | ||
const encodedExpectedChallenge = base64url_1.default.encode(expectedChallenge); | ||
if (challenge !== encodedExpectedChallenge) { | ||
throw new Error(`Unexpected attestation challenge "${challenge}", expected "${encodedExpectedChallenge}"`); | ||
} | ||
@@ -69,10 +84,20 @@ // Check that the origin is our site | ||
} | ||
if (tokenBinding) { | ||
if (typeof tokenBinding !== 'object') { | ||
throw new Error(`Unexpected value for TokenBinding "${tokenBinding}"`); | ||
} | ||
if (['present', 'supported', 'not-supported'].indexOf(tokenBinding.status) < 0) { | ||
throw new Error(`Unexpected tokenBinding.status value of "${tokenBinding.status}"`); | ||
} | ||
} | ||
const attestationObject = decodeAttestationObject_1.default(response.attestationObject); | ||
const { fmt, authData, attStmt } = attestationObject; | ||
const parsedAuthData = parseAuthenticatorData_1.default(authData); | ||
const { rpIdHash, flags, credentialID, counter, credentialPublicKey } = parsedAuthData; | ||
const { aaguid, rpIdHash, flags, credentialID, counter, credentialPublicKey } = parsedAuthData; | ||
// Make sure the response's RP ID is ours | ||
const expectedRPIDHash = toHash_1.default(Buffer.from(expectedRPID, 'ascii')); | ||
if (!rpIdHash.equals(expectedRPIDHash)) { | ||
throw new Error(`Unexpected RP ID hash`); | ||
if (expectedRPID) { | ||
const expectedRPIDHash = toHash_1.default(Buffer.from(expectedRPID, 'ascii')); | ||
if (!rpIdHash.equals(expectedRPIDHash)) { | ||
throw new Error(`Unexpected RP ID hash`); | ||
} | ||
} | ||
@@ -93,10 +118,13 @@ // Make sure someone was physically present | ||
} | ||
if (!aaguid) { | ||
throw new Error('No AAGUID was present in attestation'); | ||
} | ||
const decodedPublicKey = decodeCredentialPublicKey_1.default(credentialPublicKey); | ||
const alg = decodedPublicKey.get(convertCOSEtoPKCS_1.COSEKEYS.alg); | ||
if (!alg) { | ||
throw new Error('Credential public key was missing alg'); | ||
if (typeof alg !== 'number') { | ||
throw new Error('Credential public key was missing numeric alg'); | ||
} | ||
// Make sure the key algorithm is one we specified within the attestation options | ||
if (!generateAttestationOptions_1.supportedCOSEAlgorithIdentifiers.includes(alg)) { | ||
const supported = generateAttestationOptions_1.supportedCOSEAlgorithIdentifiers.join(', '); | ||
if (!generateAttestationOptions_1.supportedCOSEAlgorithmIdentifiers.includes(alg)) { | ||
const supported = generateAttestationOptions_1.supportedCOSEAlgorithmIdentifiers.join(', '); | ||
throw new Error(`Unexpected public key alg "${alg}", expected one of "${supported}"`); | ||
@@ -116,6 +144,7 @@ } | ||
rpIdHash, | ||
aaguid, | ||
}); | ||
} | ||
else if (fmt === decodeAttestationObject_1.ATTESTATION_FORMATS.PACKED) { | ||
verified = verifyPacked_1.default({ | ||
verified = await verifyPacked_1.default({ | ||
attStmt, | ||
@@ -125,12 +154,35 @@ authData, | ||
credentialPublicKey, | ||
aaguid, | ||
}); | ||
} | ||
else if (fmt === decodeAttestationObject_1.ATTESTATION_FORMATS.ANDROID_SAFETYNET) { | ||
verified = verifyAndroidSafetyNet_1.default({ | ||
verified = await verifyAndroidSafetyNet_1.default({ | ||
attStmt, | ||
authData, | ||
clientDataHash, | ||
aaguid, | ||
}); | ||
} | ||
else if (fmt === decodeAttestationObject_1.ATTESTATION_FORMATS.ANDROID_KEY) { | ||
verified = await verifyAndroidKey_1.default({ | ||
attStmt, | ||
authData, | ||
clientDataHash, | ||
credentialPublicKey, | ||
aaguid, | ||
}); | ||
} | ||
else if (fmt === decodeAttestationObject_1.ATTESTATION_FORMATS.TPM) { | ||
verified = await verifyTPM_1.default({ | ||
aaguid, | ||
attStmt, | ||
authData, | ||
credentialPublicKey, | ||
clientDataHash, | ||
}); | ||
} | ||
else if (fmt === decodeAttestationObject_1.ATTESTATION_FORMATS.NONE) { | ||
if (Object.keys(attStmt).length > 0) { | ||
throw new Error('None attestation had unexpected attestation statement'); | ||
} | ||
// This is the weaker of the attestations, so there's nothing else to really check | ||
@@ -137,0 +189,0 @@ verified = true; |
/// <reference types="node" /> | ||
import type { Base64URLString } from '@simplewebauthn/typescript-types'; | ||
/** | ||
@@ -8,2 +9,2 @@ * Convert binary certificate or public key to an OpenSSL-compatible PEM text format. | ||
*/ | ||
export default function convertASN1toPEM(pkBuffer: Buffer): string; | ||
export default function convertASN1toPEM(pkBuffer: Buffer | Base64URLString): string; |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const base64url_1 = __importDefault(require("base64url")); | ||
/** | ||
@@ -10,3 +14,9 @@ * Convert binary certificate or public key to an OpenSSL-compatible PEM text format. | ||
function convertASN1toPEM(pkBuffer) { | ||
let buffer = pkBuffer; | ||
let buffer; | ||
if (typeof pkBuffer === 'string') { | ||
buffer = base64url_1.default.toBuffer(pkBuffer); | ||
} | ||
else { | ||
buffer = pkBuffer; | ||
} | ||
let type; | ||
@@ -13,0 +23,0 @@ if (buffer.length === 65 && buffer[0] === 0x04) { |
/// <reference types="node" /> | ||
import type { SigningSchemeHash } from 'node-rsa'; | ||
/** | ||
@@ -19,1 +20,15 @@ * Takes COSE-encoded public key and converts it to PKCS key | ||
} | ||
export declare enum COSEKTY { | ||
OKP = 1, | ||
EC2 = 2, | ||
RSA = 3 | ||
} | ||
export declare const COSERSASCHEME: { | ||
[key: string]: SigningSchemeHash; | ||
}; | ||
export declare const COSECRV: { | ||
[key: number]: string; | ||
}; | ||
export declare const COSEALGHASH: { | ||
[key: string]: string; | ||
}; |
@@ -6,3 +6,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.COSEKEYS = void 0; | ||
exports.COSEALGHASH = exports.COSECRV = exports.COSERSASCHEME = exports.COSEKTY = exports.COSEKEYS = void 0; | ||
const cbor_1 = __importDefault(require("cbor")); | ||
@@ -39,6 +39,6 @@ /** | ||
} | ||
if (!y) { | ||
throw new Error('COSE public key was missing y'); | ||
if (y) { | ||
return Buffer.concat([tag, x, y]); | ||
} | ||
return Buffer.concat([tag, x, y]); | ||
return Buffer.concat([tag, x]); | ||
} | ||
@@ -56,2 +56,40 @@ exports.default = convertCOSEtoPKCS; | ||
})(COSEKEYS = exports.COSEKEYS || (exports.COSEKEYS = {})); | ||
var COSEKTY; | ||
(function (COSEKTY) { | ||
COSEKTY[COSEKTY["OKP"] = 1] = "OKP"; | ||
COSEKTY[COSEKTY["EC2"] = 2] = "EC2"; | ||
COSEKTY[COSEKTY["RSA"] = 3] = "RSA"; | ||
})(COSEKTY = exports.COSEKTY || (exports.COSEKTY = {})); | ||
exports.COSERSASCHEME = { | ||
'-3': 'pss-sha256', | ||
'-39': 'pss-sha512', | ||
'-38': 'pss-sha384', | ||
'-65535': 'pkcs1-sha1', | ||
'-257': 'pkcs1-sha256', | ||
'-258': 'pkcs1-sha384', | ||
'-259': 'pkcs1-sha512', | ||
}; | ||
// See https://w3c.github.io/webauthn/#sctn-alg-identifier | ||
exports.COSECRV = { | ||
// alg: -7 | ||
1: 'p256', | ||
// alg: -35 | ||
2: 'p384', | ||
// alg: -36 | ||
3: 'p521', | ||
// alg: -8 | ||
6: 'ed25519', | ||
}; | ||
exports.COSEALGHASH = { | ||
'-257': 'sha256', | ||
'-258': 'sha384', | ||
'-259': 'sha512', | ||
'-65535': 'sha1', | ||
'-39': 'sha512', | ||
'-38': 'sha384', | ||
'-37': 'sha256', | ||
'-7': 'sha256', | ||
'-8': 'sha512', | ||
'-36': 'sha512', | ||
}; | ||
//# sourceMappingURL=convertCOSEtoPKCS.js.map |
@@ -12,2 +12,4 @@ /// <reference types="node" /> | ||
ANDROID_SAFETYNET = "android-safetynet", | ||
ANDROID_KEY = "android-key", | ||
TPM = "tpm", | ||
NONE = "none" | ||
@@ -24,2 +26,6 @@ } | ||
response?: Buffer; | ||
alg?: number; | ||
ver?: string; | ||
certInfo?: Buffer; | ||
pubArea?: Buffer; | ||
}; |
@@ -25,4 +25,6 @@ "use strict"; | ||
ATTESTATION_FORMATS["ANDROID_SAFETYNET"] = "android-safetynet"; | ||
ATTESTATION_FORMATS["ANDROID_KEY"] = "android-key"; | ||
ATTESTATION_FORMATS["TPM"] = "tpm"; | ||
ATTESTATION_FORMATS["NONE"] = "none"; | ||
})(ATTESTATION_FORMATS = exports.ATTESTATION_FORMATS || (exports.ATTESTATION_FORMATS = {})); | ||
//# sourceMappingURL=decodeAttestationObject.js.map |
@@ -9,3 +9,8 @@ /** | ||
origin: string; | ||
crossOrigin?: boolean; | ||
tokenBinding?: { | ||
id?: string; | ||
status: 'present' | 'supported' | 'not-supported'; | ||
}; | ||
}; | ||
export {}; |
@@ -13,5 +13,2 @@ "use strict"; | ||
const clientData = JSON.parse(toString); | ||
// `challenge` will be Base64URL-encoded here. Decode it for easier comparisons with what is | ||
// provided as the expected value | ||
clientData.challenge = base64url_1.default.decode(clientData.challenge); | ||
return clientData; | ||
@@ -18,0 +15,0 @@ } |
export declare type CertificateInfo = { | ||
issuer: { | ||
[key: string]: string; | ||
}; | ||
subject: { | ||
@@ -7,2 +10,4 @@ [key: string]: string; | ||
basicConstraintsCA: boolean; | ||
notBefore: Date; | ||
notAfter: Date; | ||
}; | ||
@@ -9,0 +14,0 @@ /** |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const jsrsasign_1 = __importDefault(require("jsrsasign")); | ||
const jsrsasign_1 = require("jsrsasign"); | ||
/** | ||
@@ -13,17 +10,41 @@ * Extract PEM certificate info | ||
function getCertificateInfo(pemCertificate) { | ||
const subjectCert = new jsrsasign_1.default.X509(); | ||
var _a; | ||
const subjectCert = new jsrsasign_1.X509(); | ||
subjectCert.readCertPEM(pemCertificate); | ||
const subjectString = subjectCert.getSubjectString(); | ||
const subjectParts = subjectString.slice(1).split('/'); | ||
// Break apart the Issuer | ||
const issuerString = subjectCert.getIssuerString(); | ||
const issuerParts = issuerString.slice(1).split('/'); | ||
const issuer = {}; | ||
issuerParts.forEach(field => { | ||
const [key, val] = field.split('='); | ||
issuer[key] = val; | ||
}); | ||
// Break apart the Subject | ||
let subjectRaw = '/'; | ||
try { | ||
subjectRaw = subjectCert.getSubjectString(); | ||
} | ||
catch (err) { | ||
// Don't throw on an error that indicates an empty subject | ||
if (err !== 'malformed RDN') { | ||
throw err; | ||
} | ||
} | ||
const subjectParts = subjectRaw.slice(1).split('/'); | ||
const subject = {}; | ||
subjectParts.forEach(field => { | ||
const [key, val] = field.split('='); | ||
subject[key] = val; | ||
if (field) { | ||
const [key, val] = field.split('='); | ||
subject[key] = val; | ||
} | ||
}); | ||
const { version } = subjectCert; | ||
const basicConstraintsCA = !!subjectCert.getExtBasicConstraints().cA; | ||
const basicConstraintsCA = !!((_a = subjectCert.getExtBasicConstraints()) === null || _a === void 0 ? void 0 : _a.cA); | ||
return { | ||
issuer, | ||
subject, | ||
version, | ||
basicConstraintsCA, | ||
notBefore: jsrsasign_1.zulutodate(subjectCert.getNotBefore()), | ||
notAfter: jsrsasign_1.zulutodate(subjectCert.getNotAfter()), | ||
}; | ||
@@ -30,0 +51,0 @@ } |
@@ -21,2 +21,3 @@ /// <reference types="node" /> | ||
credentialPublicKey?: Buffer; | ||
extensionsDataBuffer?: Buffer; | ||
}; |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const cbor_1 = __importDefault(require("cbor")); | ||
/** | ||
@@ -7,2 +11,5 @@ * Make sense of the authData buffer contained in an Attestation | ||
function parseAuthenticatorData(authData) { | ||
if (authData.byteLength < 37) { | ||
throw new Error(`Authenticator data was ${authData.byteLength} bytes, expected at least 37 bytes`); | ||
} | ||
let intBuffer = authData; | ||
@@ -35,4 +42,18 @@ const rpIdHash = intBuffer.slice(0, 32); | ||
intBuffer = intBuffer.slice(credIDLen); | ||
credentialPublicKey = intBuffer; | ||
// Decode the next CBOR item in the buffer, then re-encode it back to a Buffer | ||
const firstDecoded = cbor_1.default.decodeFirstSync(intBuffer); | ||
const firstEncoded = cbor_1.default.encode(firstDecoded); | ||
credentialPublicKey = firstEncoded; | ||
intBuffer = intBuffer.slice(firstEncoded.byteLength); | ||
} | ||
let extensionsDataBuffer = undefined; | ||
if (flags.ed) { | ||
const firstDecoded = cbor_1.default.decodeFirstSync(intBuffer); | ||
const firstEncoded = cbor_1.default.encode(firstDecoded); | ||
extensionsDataBuffer = firstEncoded; | ||
intBuffer = intBuffer.slice(firstEncoded.byteLength); | ||
} | ||
if (intBuffer.byteLength > 0) { | ||
throw new Error('Leftover bytes detected while parsing authenticator data'); | ||
} | ||
return { | ||
@@ -47,2 +68,3 @@ rpIdHash, | ||
credentialPublicKey, | ||
extensionsDataBuffer, | ||
}; | ||
@@ -49,0 +71,0 @@ } |
@@ -7,2 +7,2 @@ /// <reference types="node" /> | ||
*/ | ||
export default function toHash(data: Buffer, algo?: string): Buffer; | ||
export default function toHash(data: Buffer | string, algo?: string): Buffer; |
@@ -8,3 +8,4 @@ /// <reference types="node" /> | ||
* @param publicKey Authenticator's public key as a PEM certificate | ||
* @param algo Which algorithm to use to verify the signature (default: `'sha256'`) | ||
*/ | ||
export default function verifySignature(signature: Buffer, signatureBase: Buffer, publicKey: string): boolean; | ||
export default function verifySignature(signature: Buffer, signatureBase: Buffer, publicKey: string, algo?: string): boolean; |
@@ -13,7 +13,8 @@ "use strict"; | ||
* @param publicKey Authenticator's public key as a PEM certificate | ||
* @param algo Which algorithm to use to verify the signature (default: `'sha256'`) | ||
*/ | ||
function verifySignature(signature, signatureBase, publicKey) { | ||
return crypto_1.default.createVerify('SHA256').update(signatureBase).verify(publicKey, signature); | ||
function verifySignature(signature, signatureBase, publicKey, algo = 'sha256') { | ||
return crypto_1.default.createVerify(algo).update(signatureBase).verify(publicKey, signature); | ||
} | ||
exports.default = verifySignature; | ||
//# sourceMappingURL=verifySignature.js.map |
@@ -10,2 +10,3 @@ /** | ||
import verifyAssertionResponse from "./assertion/verifyAssertionResponse"; | ||
export { generateAttestationOptions, verifyAttestationResponse, generateAssertionOptions, verifyAssertionResponse, }; | ||
import MetadataService from "./metadata/metadataService"; | ||
export { generateAttestationOptions, verifyAttestationResponse, generateAssertionOptions, verifyAssertionResponse, MetadataService, }; |
@@ -6,3 +6,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.verifyAssertionResponse = exports.generateAssertionOptions = exports.verifyAttestationResponse = exports.generateAttestationOptions = void 0; | ||
exports.MetadataService = exports.verifyAssertionResponse = exports.generateAssertionOptions = exports.verifyAttestationResponse = exports.generateAttestationOptions = void 0; | ||
/** | ||
@@ -21,2 +21,4 @@ * @packageDocumentation | ||
exports.verifyAssertionResponse = verifyAssertionResponse_1.default; | ||
const metadataService_1 = __importDefault(require("./metadata/metadataService")); | ||
exports.MetadataService = metadataService_1.default; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@simplewebauthn/server", | ||
"version": "0.6.1", | ||
"version": "0.7.0", | ||
"description": "SimpleWebAuthn for Servers", | ||
@@ -29,10 +29,17 @@ "main": "dist/index.js", | ||
"dependencies": { | ||
"@simplewebauthn/typescript-types": "^0.6.0", | ||
"@peculiar/asn1-android": "^2.0.7-alpha.0", | ||
"@peculiar/asn1-schema": "^2.0.5", | ||
"@peculiar/asn1-x509": "^2.0.5", | ||
"@simplewebauthn/typescript-types": "^0.7.0", | ||
"base64url": "^3.0.1", | ||
"cbor": "^5.0.2", | ||
"elliptic": "^6.5.2", | ||
"jsrsasign": "^8.0.15", | ||
"jsrsasign": "^8.0.20", | ||
"node-fetch": "^2.6.0", | ||
"node-rsa": "^1.0.8" | ||
}, | ||
"gitHead": "e00cbfb8218eaf54e2c4bd9b4a73efd57eacb8f9" | ||
"gitHead": "629ca7955282b76f8dd4acbfaa912db037ff8928", | ||
"devDependencies": { | ||
"@types/node-fetch": "^2.5.7" | ||
} | ||
} |
@@ -38,12 +38,9 @@ <!-- omit in toc --> | ||
SimpleWebAuthn can verify the following attestation formats: | ||
SimpleWebAuthn supports [all six WebAuthn attestation formats](https://w3c.github.io/webauthn/#sctn-defined-attestation-formats), including: | ||
- `fido-u2f` | ||
- `packed` | ||
- Supported Certificates | ||
- `X5C` | ||
- `COSE - EC2` | ||
- `COSE - RSA` (code is present but needs further testing) | ||
- `COSE - OKP` (code is present but needs further testing) | ||
- `android-safetynet` | ||
- `none` | ||
- **Packed** | ||
- **TPM** | ||
- **Android Key** | ||
- **Android SafetyNet** | ||
- **FIDO U2F** | ||
- **None** |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
182418
93
2875
10
1
46
+ Added@peculiar/asn1-schema@^2.0.5
+ Added@peculiar/asn1-x509@^2.0.5
+ Addednode-fetch@^2.6.0
+ Added@peculiar/asn1-android@2.3.13(transitive)
+ Added@peculiar/asn1-schema@2.3.13(transitive)
+ Added@peculiar/asn1-x509@2.3.13(transitive)
+ Added@simplewebauthn/typescript-types@0.7.1(transitive)
+ Addedasn1js@3.0.5(transitive)
+ Addedipaddr.js@2.2.0(transitive)
+ Addednode-fetch@2.7.0(transitive)
+ Addedpvtsutils@1.3.6(transitive)
+ Addedpvutils@1.1.3(transitive)
+ Addedtr46@0.0.3(transitive)
+ Addedtslib@2.8.1(transitive)
+ Addedwebidl-conversions@3.0.1(transitive)
+ Addedwhatwg-url@5.0.0(transitive)
- Removed@simplewebauthn/typescript-types@0.6.0(transitive)
Updatedjsrsasign@^8.0.20