Add verifiable GVL revision evidence packages
Dieser Commit ist enthalten in:
@@ -0,0 +1,462 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT =
|
||||||
|
"vendorget-gvl-evidence-export";
|
||||||
|
const VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION = 1;
|
||||||
|
const VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT =
|
||||||
|
"vendorget-gvl-revision-evidence";
|
||||||
|
const VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION = 1;
|
||||||
|
const VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND = "iab-gvl-revision";
|
||||||
|
|
||||||
|
const VENDORGET_GVL_EVIDENCE_STORE_NAMES = [
|
||||||
|
VENDORGET_STORE_NAMES.gvlRawEvidence,
|
||||||
|
VENDORGET_STORE_NAMES.gvlSnapshots,
|
||||||
|
VENDORGET_STORE_NAMES.gvlVendors,
|
||||||
|
VENDORGET_STORE_NAMES.gvlPurposes,
|
||||||
|
VENDORGET_STORE_NAMES.gvlSpecialPurposes,
|
||||||
|
VENDORGET_STORE_NAMES.gvlFeatures,
|
||||||
|
VENDORGET_STORE_NAMES.gvlSpecialFeatures,
|
||||||
|
VENDORGET_STORE_NAMES.gvlDataCategories,
|
||||||
|
VENDORGET_STORE_NAMES.gvlVendorRelationships
|
||||||
|
];
|
||||||
|
|
||||||
|
const VENDORGET_GVL_EVIDENCE_ARRAY_NAMES = [
|
||||||
|
"gvl_raw_evidence",
|
||||||
|
"gvl_snapshots",
|
||||||
|
"gvl_vendors",
|
||||||
|
"gvl_purposes",
|
||||||
|
"gvl_special_purposes",
|
||||||
|
"gvl_features",
|
||||||
|
"gvl_special_features",
|
||||||
|
"gvl_data_categories",
|
||||||
|
"gvl_vendor_relationships"
|
||||||
|
];
|
||||||
|
|
||||||
|
const VENDORGET_GVL_NORMALIZED_STORE_NAMES = [
|
||||||
|
VENDORGET_STORE_NAMES.gvlVendors,
|
||||||
|
VENDORGET_STORE_NAMES.gvlPurposes,
|
||||||
|
VENDORGET_STORE_NAMES.gvlSpecialPurposes,
|
||||||
|
VENDORGET_STORE_NAMES.gvlFeatures,
|
||||||
|
VENDORGET_STORE_NAMES.gvlSpecialFeatures,
|
||||||
|
VENDORGET_STORE_NAMES.gvlDataCategories,
|
||||||
|
VENDORGET_STORE_NAMES.gvlVendorRelationships
|
||||||
|
];
|
||||||
|
|
||||||
|
async function exportVendorGetGvlEvidenceJson() {
|
||||||
|
const db = await openVendorGetDb();
|
||||||
|
const exportedStores = await readGvlEvidenceStores(db);
|
||||||
|
|
||||||
|
return {
|
||||||
|
metadata: {
|
||||||
|
exportFormat: VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT,
|
||||||
|
exportFormatVersion: VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION,
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
dbName: db.name,
|
||||||
|
dbVersion: db.version
|
||||||
|
},
|
||||||
|
...exportedStores
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportVendorGetGvlRevisionEvidenceJson(snapshotSha256) {
|
||||||
|
if (!snapshotSha256) {
|
||||||
|
throw new Error("missing_snapshot_sha256");
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await openVendorGetDb();
|
||||||
|
const snapshot = await getGvlEvidenceRecordByKey(
|
||||||
|
db,
|
||||||
|
VENDORGET_STORE_NAMES.gvlSnapshots,
|
||||||
|
snapshotSha256
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!snapshot) {
|
||||||
|
throw new Error("gvl_snapshot_not_found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawGvlSha256 = snapshot.rawGvlSha256 ?? null;
|
||||||
|
const rawEvidence = rawGvlSha256
|
||||||
|
? await getGvlEvidenceRecordByKey(
|
||||||
|
db,
|
||||||
|
VENDORGET_STORE_NAMES.gvlRawEvidence,
|
||||||
|
rawGvlSha256
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
const normalized = await readGvlRevisionNormalizedRecords(db, snapshotSha256);
|
||||||
|
const exportedAt = new Date();
|
||||||
|
const exportedAtUtcCompact = formatGvlEvidenceUtcCompact(exportedAt);
|
||||||
|
const payload = {
|
||||||
|
metadata: {
|
||||||
|
exportFormat: VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT,
|
||||||
|
exportFormatVersion: VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION,
|
||||||
|
exportedAt: exportedAt.toISOString(),
|
||||||
|
exportedAtUtcCompact,
|
||||||
|
dbName: db.name,
|
||||||
|
dbVersion: db.version,
|
||||||
|
vendorListVersion: snapshot.vendorListVersion ?? null,
|
||||||
|
snapshotSha256: snapshot.sha256 ?? snapshot.snapshotSha256 ?? null,
|
||||||
|
rawGvlSha256,
|
||||||
|
contentKind: VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND
|
||||||
|
},
|
||||||
|
rawEvidence,
|
||||||
|
snapshot,
|
||||||
|
normalized
|
||||||
|
};
|
||||||
|
const exportPayloadSha256 = await calculateGvlEvidencePayloadSha256(payload);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...payload,
|
||||||
|
metadata: {
|
||||||
|
...payload.metadata,
|
||||||
|
exportPayloadSha256
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyVendorGetGvlRevisionEvidenceJson(exportContainer) {
|
||||||
|
const errors = [];
|
||||||
|
const metadata = exportContainer?.metadata ?? {};
|
||||||
|
const normalized = exportContainer?.normalized ?? {};
|
||||||
|
const rawBody = exportContainer?.rawEvidence?.rawBody ?? null;
|
||||||
|
const snapshot = exportContainer?.snapshot ?? null;
|
||||||
|
const snapshotSha256 = metadata.snapshotSha256 ?? null;
|
||||||
|
const rawGvlSha256 = metadata.rawGvlSha256 ?? null;
|
||||||
|
|
||||||
|
if (!exportContainer || typeof exportContainer !== "object") {
|
||||||
|
errors.push("invalid_export_container");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.exportFormat !== VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT) {
|
||||||
|
errors.push("invalid_export_format");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
metadata.exportFormatVersion !==
|
||||||
|
VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION
|
||||||
|
) {
|
||||||
|
errors.push("unsupported_export_format_version");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.contentKind !== VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND) {
|
||||||
|
errors.push("invalid_content_kind");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.vendorListVersion === null || metadata.vendorListVersion === undefined) {
|
||||||
|
errors.push("missing_vendor_list_version");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!snapshotSha256) {
|
||||||
|
errors.push("missing_snapshot_sha256");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawGvlSha256) {
|
||||||
|
errors.push("missing_raw_gvl_sha256");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof rawBody !== "string") {
|
||||||
|
errors.push("missing_raw_body");
|
||||||
|
} else if ((await VendorGetGvlService.calculateRawGvlSha256(rawBody)) !== rawGvlSha256) {
|
||||||
|
errors.push("raw_body_sha256_mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshotRecordSha256 = snapshot?.sha256 ?? snapshot?.snapshotSha256 ?? null;
|
||||||
|
|
||||||
|
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
|
||||||
|
errors.push("missing_snapshot");
|
||||||
|
} else if (snapshotRecordSha256 !== snapshotSha256) {
|
||||||
|
errors.push("snapshot_sha256_mismatch");
|
||||||
|
} else if (
|
||||||
|
(await VendorGetGvlService.calculateGvlSnapshotSha256(snapshot.rawJson)) !==
|
||||||
|
snapshotSha256
|
||||||
|
) {
|
||||||
|
errors.push("snapshot_sha256_mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const storeName of VENDORGET_GVL_NORMALIZED_STORE_NAMES) {
|
||||||
|
if (!Array.isArray(normalized[storeName])) {
|
||||||
|
errors.push(`missing_normalized_store_${storeName}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const record of normalized[storeName]) {
|
||||||
|
if (record?.snapshotSha256 !== snapshotSha256) {
|
||||||
|
errors.push(`normalized_record_snapshot_mismatch_${storeName}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!metadata.exportPayloadSha256) {
|
||||||
|
errors.push("missing_export_payload_sha256");
|
||||||
|
} else {
|
||||||
|
const recalculatedPayloadSha256 =
|
||||||
|
await calculateGvlEvidencePayloadSha256WithoutEmbeddedHash(exportContainer);
|
||||||
|
|
||||||
|
if (recalculatedPayloadSha256 !== metadata.exportPayloadSha256) {
|
||||||
|
errors.push("export_payload_sha256_mismatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
vendorListVersion: metadata.vendorListVersion ?? null,
|
||||||
|
snapshotSha256,
|
||||||
|
rawGvlSha256,
|
||||||
|
exportPayloadSha256: metadata.exportPayloadSha256 ?? null,
|
||||||
|
normalizedCounts: countGvlRevisionNormalizedRecords(normalized)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGvlEvidenceRecordByKey(db, storeName, key) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction([storeName], "readonly");
|
||||||
|
const getRequest = tx.objectStore(storeName).get(key);
|
||||||
|
let record = null;
|
||||||
|
|
||||||
|
getRequest.onerror = () => reject(getRequest.error);
|
||||||
|
getRequest.onsuccess = () => {
|
||||||
|
record = getRequest.result ?? null;
|
||||||
|
};
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(record);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readGvlRevisionNormalizedRecords(db, snapshotSha256) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const normalized = {};
|
||||||
|
const tx = db.transaction(VENDORGET_GVL_NORMALIZED_STORE_NAMES, "readonly");
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(normalized);
|
||||||
|
|
||||||
|
for (const storeName of VENDORGET_GVL_NORMALIZED_STORE_NAMES) {
|
||||||
|
const records = [];
|
||||||
|
const cursorRequest = tx
|
||||||
|
.objectStore(storeName)
|
||||||
|
.index("snapshotSha256")
|
||||||
|
.openCursor(IDBKeyRange.only(snapshotSha256));
|
||||||
|
|
||||||
|
normalized[storeName] = records;
|
||||||
|
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||||
|
cursorRequest.onsuccess = () => {
|
||||||
|
const cursor = cursorRequest.result;
|
||||||
|
|
||||||
|
if (!cursor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
records.push(cursor.value);
|
||||||
|
cursor.continue();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calculateGvlEvidencePayloadSha256(payload) {
|
||||||
|
return sha256Hex(stableStringify(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calculateGvlEvidencePayloadSha256WithoutEmbeddedHash(
|
||||||
|
exportContainer
|
||||||
|
) {
|
||||||
|
const metadata = {
|
||||||
|
...(exportContainer?.metadata ?? {})
|
||||||
|
};
|
||||||
|
|
||||||
|
delete metadata.exportPayloadSha256;
|
||||||
|
|
||||||
|
return calculateGvlEvidencePayloadSha256({
|
||||||
|
...exportContainer,
|
||||||
|
metadata
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function countGvlRevisionNormalizedRecords(normalized) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
VENDORGET_GVL_NORMALIZED_STORE_NAMES.map((storeName) => [
|
||||||
|
storeName,
|
||||||
|
Array.isArray(normalized?.[storeName]) ? normalized[storeName].length : 0
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGvlEvidenceUtcCompact(date) {
|
||||||
|
return [
|
||||||
|
date.getUTCFullYear(),
|
||||||
|
padGvlEvidenceDatePart(date.getUTCMonth() + 1),
|
||||||
|
padGvlEvidenceDatePart(date.getUTCDate()),
|
||||||
|
"T",
|
||||||
|
padGvlEvidenceDatePart(date.getUTCHours()),
|
||||||
|
padGvlEvidenceDatePart(date.getUTCMinutes()),
|
||||||
|
padGvlEvidenceDatePart(date.getUTCSeconds()),
|
||||||
|
"Z"
|
||||||
|
].join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function padGvlEvidenceDatePart(value) {
|
||||||
|
return String(value).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readGvlEvidenceStores(db) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const exportContainer = {};
|
||||||
|
const tx = db.transaction(VENDORGET_GVL_EVIDENCE_STORE_NAMES, "readonly");
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => resolve(exportContainer);
|
||||||
|
|
||||||
|
for (const storeName of VENDORGET_GVL_EVIDENCE_STORE_NAMES) {
|
||||||
|
const records = [];
|
||||||
|
const cursorRequest = tx.objectStore(storeName).openCursor();
|
||||||
|
|
||||||
|
exportContainer[storeName] = records;
|
||||||
|
|
||||||
|
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||||
|
cursorRequest.onsuccess = () => {
|
||||||
|
const cursor = cursorRequest.result;
|
||||||
|
|
||||||
|
if (!cursor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
records.push(cursor.value);
|
||||||
|
cursor.continue();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importVendorGetGvlEvidenceJson(importContainer) {
|
||||||
|
validateVendorGetGvlEvidenceImport(importContainer);
|
||||||
|
|
||||||
|
const db = await openVendorGetDb();
|
||||||
|
|
||||||
|
return importGvlEvidenceStores(db, importContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateVendorGetGvlEvidenceImport(importContainer) {
|
||||||
|
if (!importContainer || typeof importContainer !== "object") {
|
||||||
|
throw new Error("invalid_gvl_evidence_export");
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = importContainer.metadata ?? {};
|
||||||
|
|
||||||
|
if (metadata.exportFormat !== VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT) {
|
||||||
|
throw new Error("invalid_gvl_evidence_export_format");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
metadata.exportFormatVersion !==
|
||||||
|
VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION
|
||||||
|
) {
|
||||||
|
throw new Error("unsupported_gvl_evidence_export_version");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const arrayName of VENDORGET_GVL_EVIDENCE_ARRAY_NAMES) {
|
||||||
|
if (!Array.isArray(importContainer[arrayName])) {
|
||||||
|
throw new Error(`missing_gvl_evidence_store_${arrayName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function importGvlEvidenceStores(db, importContainer) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const counts = buildEmptyGvlEvidenceImportCounts();
|
||||||
|
const seenKeysByStore = new Map();
|
||||||
|
const tx = db.transaction(VENDORGET_GVL_EVIDENCE_STORE_NAMES, "readwrite");
|
||||||
|
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
tx.onabort = () => reject(tx.error);
|
||||||
|
tx.oncomplete = () => {
|
||||||
|
resolve({
|
||||||
|
importedAt: new Date().toISOString(),
|
||||||
|
counts
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const storeName of VENDORGET_GVL_EVIDENCE_STORE_NAMES) {
|
||||||
|
importGvlEvidenceStoreRecords(
|
||||||
|
tx.objectStore(storeName),
|
||||||
|
importContainer[storeName],
|
||||||
|
counts[storeName],
|
||||||
|
getSeenKeysForStore(seenKeysByStore, storeName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEmptyGvlEvidenceImportCounts() {
|
||||||
|
return Object.fromEntries(
|
||||||
|
VENDORGET_GVL_EVIDENCE_STORE_NAMES.map((storeName) => [
|
||||||
|
storeName,
|
||||||
|
{
|
||||||
|
read: 0,
|
||||||
|
inserted: 0,
|
||||||
|
skippedExisting: 0,
|
||||||
|
skippedInvalid: 0
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeenKeysForStore(seenKeysByStore, storeName) {
|
||||||
|
if (!seenKeysByStore.has(storeName)) {
|
||||||
|
seenKeysByStore.set(storeName, new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
return seenKeysByStore.get(storeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importGvlEvidenceStoreRecords(objectStore, records, counts, seenKeys) {
|
||||||
|
for (const record of records) {
|
||||||
|
counts.read += 1;
|
||||||
|
|
||||||
|
const key = getGvlEvidenceRecordKey(objectStore, record);
|
||||||
|
const keySignature = JSON.stringify(key);
|
||||||
|
|
||||||
|
if (key === null || seenKeys.has(keySignature)) {
|
||||||
|
counts.skippedInvalid += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenKeys.add(keySignature);
|
||||||
|
|
||||||
|
const getRequest = objectStore.get(key);
|
||||||
|
|
||||||
|
getRequest.onsuccess = () => {
|
||||||
|
if (getRequest.result !== undefined) {
|
||||||
|
counts.skippedExisting += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addRequest = objectStore.add(record);
|
||||||
|
|
||||||
|
addRequest.onsuccess = () => {
|
||||||
|
counts.inserted += 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGvlEvidenceRecordKey(objectStore, record) {
|
||||||
|
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectStore.name === VENDORGET_STORE_NAMES.gvlSnapshots) {
|
||||||
|
return record.sha256 ?? record.snapshotSha256 ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyPath = objectStore.keyPath;
|
||||||
|
|
||||||
|
if (typeof keyPath !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return record[keyPath] ?? null;
|
||||||
|
}
|
||||||
In neuem Issue referenzieren
Einen Benutzer sperren