Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
277 changes: 201 additions & 76 deletions src/background.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
import { b64DecodeBytes, b64EncodeBytes } from "./conversion";
import { SignedCertificateTimestamp } from "@peculiar/asn1-cert-transparency";
import { base64ToBytes, bytesToBase64 } from "./conversion";
import { CTLogClient } from "./ct_log_client";
import { CtLogStore } from "./ct_log_store";
import { leafHashForPreCert, sctsFromCertDer } from "./ct_parsing";
import { validateProof } from "./ct_proof_validation";
import { CtLog, CtSignedTreeHead } from "./ct_log_types";
import {
logEntryBytesForPreCert,
sctsFromCertDer,
sthFromBytes,
} from "./ct_parsing";
import { sha256 } from "./hashing";
import { PrismClient } from "./prism_client";
import { prismAccountToBincode } from "./prism_serialization";
import { validateCtProof, validatePrismProof } from "./proof_validation";
import { DomainVerificationStore } from "./verification_store";

async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function run_node() {
// TODO: Replace with actually running a prism light node
let i = 0;
while (true) {
await sleep(5000);
i += 1;
console.log(`Node is still running ... ${i}`);
}
}

run_node();

browser.webRequest.onHeadersReceived.addListener(
async function (details) {
if (details.url === CtLogStore.LOG_LIST_URL) {
Expand All @@ -29,78 +22,68 @@ browser.webRequest.onHeadersReceived.addListener(
}

const domain = new URL(details.url).origin;
const certs = await extractCertificates(details);

try {
const securityInfo = await browser.webRequest.getSecurityInfo(
details.requestId,
{ certificateChain: true, rawDER: true },
);

if (securityInfo.state !== "secure" && securityInfo.state !== "weak") {
// Non-HTTPS requests can't be verified
return;
}

if (securityInfo.certificates.length < 2) {
// 0 = No certificate at all - error
// 1 = No issuer (e.g. self signed) - can't query CT log
return;
}

const certDer = new Uint8Array(securityInfo.certificates[0].rawDER);
const issuerDer = new Uint8Array(securityInfo.certificates[1].rawDER);

const ctLogStore = await CtLogStore.getInstance();
// Abort early if we were not able to extract certificate data from the web request
if (!certs) return;

const domainVerificationStore =
await DomainVerificationStore.getInstance();
await domainVerificationStore.clearVerificationForDomain(domain);
// Initialize stores
const ctLogStore = await CtLogStore.getInstance();
const domainVerificationStore = await DomainVerificationStore.getInstance();

const scts = sctsFromCertDer(certDer);
// Reset all previous verifications for the domain of the web request
await domainVerificationStore.clearVerificationForDomain(domain);

for (const sct of scts) {
const b64LogId = b64EncodeBytes(new Uint8Array(sct.logId));
const log = ctLogStore.getLogById(b64LogId);

if (log === undefined) {
console.log("CT Log", b64LogId, "not found");
return;
}
console.log("Cert in", log.url);
const leafHash = await leafHashForPreCert(
certDer,
issuerDer,
sct.timestamp,
new Uint8Array(sct.extensions),
);
const b64LeafHash = b64EncodeBytes(leafHash);
console.log(log.description, "B64 Leaf Hash:", b64LeafHash);
// Extract Signed Certificate Timestamps (SCTs) from the raw certificate.
// Each SCT contains information about when a certificate transparency log
// observed and logged the certificate, signed by the log's private key
const scts = sctsFromCertDer(certs.certDer);

const ctClient = new CTLogClient(log.url);
// Go through all SCTs we found in the certificate, and verify them against their
// corresponding CT logs using proof validation
for (const sct of scts) {
const b64LogId = bytesToBase64(new Uint8Array(sct.logId));
const log = ctLogStore.getLogById(b64LogId);

// TODO: Acquire that from prism instead
const logSth = await ctClient.getSignedTreeHead();
if (log === undefined) {
console.warn("CT Log", b64LogId, "not found in official list");
continue;
}

const proof = await ctClient.getProofByHash(
b64LeafHash,
logSth.tree_size,
try {
// Fetch the latest Signed Tree Head (STH) of the CT Log mentioned in the SCT.
// An STH is a cryptographic commitment to the current state of a log,
// signed by the log operator, that includes a root hash and timestamp.
const sth = await queryLogSth(log);

// After ensuring the STH is valid, we can now check whether
// an entry with the cert's content really exists in the CT Log
const isCtLogEntryValid = await checkCertValidity(
certs.certDer,
certs.issuerDer,
sct,
log,
sth,
);

const expectedRootHash = b64DecodeBytes(logSth.sha256_root_hash);
const verificationResult = await validateProof(
proof,
leafHash,
expectedRootHash,
// In the end, we store the validation result for the corresponding
// (domain, log) pair.
await domainVerificationStore.reportLogVerification(
domain,
log.description,
isCtLogEntryValid,
);

} catch (error) {
console.error(error);
// In the end, we store the validation result for the corresponding
// (domain, log) pair.
await domainVerificationStore.reportLogVerification(
domain,
log.description,
verificationResult,
false,
);
return;
}
} catch (error) {
console.error("Error validating cert:", error);
}

return {};
Expand All @@ -116,3 +99,145 @@ browser.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
return await verificationStore.verificationForDomain(domain);
}
});

run_node();

const prism = new PrismClient("http://127.0.0.1:50524");

/**
* Utility function to pause execution for a specified number of milliseconds
* @param ms Number of milliseconds to sleep
* @returns Promise that resolves after the specified delay
*/
async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
* Runs a simulated prism light node
* Currently just a placeholder that logs periodic messages
* TODO: Replace with actual prism light node implementation
*/
async function run_node() {
let i = 0;
while (true) {
await sleep(60000);
i += 1;
console.log(`Node is still running ...`);
}
}

/**
* Extracts certificate and issuer certificate from a web request
* @param details Web request details containing security information
* @returns Object containing certificate DER and issuer DER, or null if certificates cannot be extracted
*/
async function extractCertificates(details) {
const securityInfo = await browser.webRequest.getSecurityInfo(
details.requestId,
{ certificateChain: true, rawDER: true },
);

if (securityInfo.state !== "secure" && securityInfo.state !== "weak") {
// Non-HTTPS requests can't be verified
return null;
}

if (securityInfo.certificates.length < 2) {
// 0 = No certificate at all - error
// 1 = No issuer (e.g. self signed) - can't query CT log
return null;
}

return {
certDer: new Uint8Array(securityInfo.certificates[0].rawDER),
issuerDer: new Uint8Array(securityInfo.certificates[1].rawDER),
};
}

/**
* Queries and validates a Certificate Transparency log's Signed Tree Head from prism.
*
* We use prism accounts instead of asking the log directly because the log can be dishonest
* and lead you to believe a certificate is valid and has been included, even if it hasn't.
* By posting the STHs to a prism account, we have a verifiable append-only ledger that all users
* can access in a trust minimized way to get the same view of the STH. This bypasses the
* need for gossip-based methods and heavy clients.
*
* @param log The CtLog from which the STH shall be queried
* @returns Parsed Signed Tree Head from the log
* @throws Error if log not found in Prism or proof validation fails
*/
async function queryLogSth(log: CtLog) {
// Here, we fetch the Account for a specific CT Log from prism
const accountRes = await prism.getAccount(log.log_id);

if (accountRes.account === undefined || accountRes.proof.leaf === undefined) {
throw new Error(`CT Log ${log.log_id} not found in prism`);
}

if (accountRes.account.signed_data.length != 1) {
throw new Error(
`Incorrect number of prism entries (${accountRes.account.signed_data.length}) for Log ${log.log_id}`,
);
}

// In order to validate the aquired account info, we need the latest cryptographic commitment
// from prism.
// TODO: query commitment from light node instead of via REST
const expectedPrismRootHashHex = (await prism.getCommitment()).commitment;
const prismProof = accountRes.proof;
// To ensure that prism gave us the correct account data (and thus STH)
// we need to serialize the account like prism does.
const prismValue = prismAccountToBincode(accountRes.account);

// We are then able to validate the Merkle Proof provided by prism.
// If successfully verified, we can be sure that the STH we fetched,
// is the one agreed upon by all prism nodes.
const isPrismProofValid = await validatePrismProof(
prismProof,
log.log_id,
prismValue,
expectedPrismRootHashHex,
);

if (!isPrismProofValid) {
throw new Error(`Invalid Prism proof for log ${log.log_id}`);
}

return sthFromBytes(base64ToBytes(accountRes.account.signed_data[0].data));
}

/**
* Verifies a Certificate Transparency log proof for a certificate.
*
* @param certDer Certificate in DER format
* @param issuerDer Issuer certificate in DER format
* @param sct Signed Certificate Timestamp
* @param log CT log information
* @param sth Signed Tree Head from the log
* @returns Boolean indicating if the proof is valid
*/
async function checkCertValidity(
certDer: Uint8Array,
issuerDer: Uint8Array,
sct: SignedCertificateTimestamp,
log: CtLog,
sth: CtSignedTreeHead,
) {
const logEntryBytes = await logEntryBytesForPreCert(
certDer,
issuerDer,
sct.timestamp,
new Uint8Array(sct.extensions),
);
const leafHash = await sha256(logEntryBytes);

const ctClient = new CTLogClient(log.url);
const ctProof = await ctClient.getProofByHash(
bytesToBase64(leafHash),
sth.treeSize,
);

return await validateCtProof(ctProof, leafHash, sth.rootHash);
}
45 changes: 45 additions & 0 deletions src/byte_arrays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Concatenates multiple Uint8Arrays into a single Uint8Array.
*
* @param arrays - Variable number of Uint8Array arguments to concatenate
* @returns A new Uint8Array containing all input arrays concatenated in order
* @example
* ```ts
* const arr1 = new Uint8Array([1, 2]);
* const arr2 = new Uint8Array([3, 4]);
* const result = concatenateArrays(arr1, arr2);
* // result is Uint8Array([1, 2, 3, 4])
* ```
*/
export function concatenateArrays(...arrays: Uint8Array[]): Uint8Array {
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;

for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}

return result;
}

/**
* Compares two Uint8Arrays for equality by checking length and values.
*
* @param arr1 - First Uint8Array to compare
* @param arr2 - Second Uint8Array to compare
* @returns true if arrays are equal in length and values, false otherwise
* @example
* ```ts
* const arr1 = new Uint8Array([1, 2]);
* const arr2 = new Uint8Array([1, 2]);
* const equal = areArraysEqual(arr1, arr2); // true
* ```
*/
export function areArraysEqual(arr1: Uint8Array, arr2: Uint8Array): boolean {
if (arr1.length !== arr2.length) {
return false;
}
return arr1.every((value, index) => value === arr2[index]);
}
Comment on lines +40 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Consider using constant-time comparison for cryptographic operations

The current array comparison using every might be vulnerable to timing attacks if used for comparing sensitive cryptographic data.

 export function areArraysEqual(arr1: Uint8Array, arr2: Uint8Array): boolean {
   if (arr1.length !== arr2.length) {
     return false;
   }
-  return arr1.every((value, index) => value === arr2[index]);
+  // Constant-time comparison
+  let result = 0;
+  for (let i = 0; i < arr1.length; i++) {
+    result |= arr1[i] ^ arr2[i];
+  }
+  return result === 0;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function areArraysEqual(arr1: Uint8Array, arr2: Uint8Array): boolean {
if (arr1.length !== arr2.length) {
return false;
}
return arr1.every((value, index) => value === arr2[index]);
}
export function areArraysEqual(arr1: Uint8Array, arr2: Uint8Array): boolean {
if (arr1.length !== arr2.length) {
return false;
}
// Constant-time comparison
let result = 0;
for (let i = 0; i < arr1.length; i++) {
result |= arr1[i] ^ arr2[i];
}
return result === 0;
}

Loading