mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
112 lines
4.5 KiB
112 lines
4.5 KiB
import { toHash } from '../../helpers/toHash.js';
|
|
import { verifySignature } from '../../helpers/verifySignature.js';
|
|
import { getCertificateInfo } from '../../helpers/getCertificateInfo.js';
|
|
import { validateCertificatePath } from '../../helpers/validateCertificatePath.js';
|
|
import { convertCertBufferToPEM } from '../../helpers/convertCertBufferToPEM.js';
|
|
import { isoBase64URL, isoUint8Array } from '../../helpers/iso/index.js';
|
|
import { MetadataService } from '../../services/metadataService.js';
|
|
import { verifyAttestationWithMetadata } from '../../metadata/verifyAttestationWithMetadata.js';
|
|
/**
|
|
* Verify an attestation response with fmt 'android-safetynet'
|
|
*/
|
|
export async function verifyAttestationAndroidSafetyNet(options) {
|
|
const { attStmt, clientDataHash, authData, aaguid, rootCertificates, verifyTimestampMS = true, credentialPublicKey, attestationSafetyNetEnforceCTSCheck, } = options;
|
|
const alg = attStmt.get('alg');
|
|
const response = attStmt.get('response');
|
|
const ver = attStmt.get('ver');
|
|
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 = isoUint8Array.toUTF8String(response);
|
|
const jwtParts = jwt.split('.');
|
|
const HEADER = JSON.parse(isoBase64URL.toUTF8String(jwtParts[0]));
|
|
const PAYLOAD = JSON.parse(isoBase64URL.toUTF8String(jwtParts[1]));
|
|
const SIGNATURE = jwtParts[2];
|
|
/**
|
|
* START Verify 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 = isoUint8Array.concat([authData, clientDataHash]);
|
|
const nonceBuffer = await toHash(nonceBase);
|
|
const expectedNonce = isoBase64URL.fromBuffer(nonceBuffer, 'base64');
|
|
if (nonce !== expectedNonce) {
|
|
throw new Error('Could not verify payload nonce (SafetyNet)');
|
|
}
|
|
if (attestationSafetyNetEnforceCTSCheck && !ctsProfileMatch) {
|
|
throw new Error('Could not verify device integrity (SafetyNet)');
|
|
}
|
|
/**
|
|
* END Verify PAYLOAD
|
|
*/
|
|
/**
|
|
* START Verify Header
|
|
*/
|
|
// `HEADER.x5c[0]` is definitely a base64 string
|
|
const leafCertBuffer = isoBase64URL.toBuffer(HEADER.x5c[0], 'base64');
|
|
const leafCertInfo = getCertificateInfo(leafCertBuffer);
|
|
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)');
|
|
}
|
|
const statement = await MetadataService.getStatement(aaguid);
|
|
if (statement) {
|
|
try {
|
|
await verifyAttestationWithMetadata({
|
|
statement,
|
|
credentialPublicKey,
|
|
x5c: HEADER.x5c,
|
|
attestationStatementAlg: alg,
|
|
});
|
|
}
|
|
catch (err) {
|
|
const _err = err;
|
|
throw new Error(`${_err.message} (SafetyNet)`);
|
|
}
|
|
}
|
|
else {
|
|
try {
|
|
// Try validating the certificate path using the root certificates set via SettingsService
|
|
await validateCertificatePath(HEADER.x5c.map(convertCertBufferToPEM), rootCertificates);
|
|
}
|
|
catch (err) {
|
|
const _err = err;
|
|
throw new Error(`${_err.message} (SafetyNet)`);
|
|
}
|
|
}
|
|
/**
|
|
* END Verify Header
|
|
*/
|
|
/**
|
|
* START Verify Signature
|
|
*/
|
|
const signatureBaseBuffer = isoUint8Array.fromUTF8String(`${jwtParts[0]}.${jwtParts[1]}`);
|
|
const signatureBuffer = isoBase64URL.toBuffer(SIGNATURE);
|
|
const verified = await verifySignature({
|
|
signature: signatureBuffer,
|
|
data: signatureBaseBuffer,
|
|
x509Certificate: leafCertBuffer,
|
|
});
|
|
/**
|
|
* END Verify Signature
|
|
*/
|
|
return verified;
|
|
}
|
|
|