summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/dap
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/telemetry/dap
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/telemetry/dap')
-rw-r--r--toolkit/components/telemetry/dap/DAPTelemetry.cpp295
-rw-r--r--toolkit/components/telemetry/dap/DAPTelemetry.h25
-rw-r--r--toolkit/components/telemetry/dap/DAPTelemetryBindings.h11
-rw-r--r--toolkit/components/telemetry/dap/DAPTelemetrySender.sys.mjs199
-rw-r--r--toolkit/components/telemetry/dap/components.conf10
-rw-r--r--toolkit/components/telemetry/dap/ffi-gtest/Cargo.toml20
-rw-r--r--toolkit/components/telemetry/dap/ffi-gtest/PrgAes128_tests.json1
-rw-r--r--toolkit/components/telemetry/dap/ffi-gtest/TestDAPTelemetry.cpp23
-rw-r--r--toolkit/components/telemetry/dap/ffi-gtest/moz.build7
-rw-r--r--toolkit/components/telemetry/dap/ffi-gtest/test.rs230
-rw-r--r--toolkit/components/telemetry/dap/ffi/Cargo.toml13
-rw-r--r--toolkit/components/telemetry/dap/ffi/cbindgen.toml11
-rw-r--r--toolkit/components/telemetry/dap/ffi/src/lib.rs197
-rw-r--r--toolkit/components/telemetry/dap/ffi/src/prg.rs93
-rw-r--r--toolkit/components/telemetry/dap/ffi/src/types.rs338
-rw-r--r--toolkit/components/telemetry/dap/metrics.yaml46
-rw-r--r--toolkit/components/telemetry/dap/nsIDAPTelemetry.idl29
-rw-r--r--toolkit/components/telemetry/dap/tests/xpcshell/test_dap.js195
-rw-r--r--toolkit/components/telemetry/dap/tests/xpcshell/xpcshell.ini2
19 files changed, 1745 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/dap/DAPTelemetry.cpp b/toolkit/components/telemetry/dap/DAPTelemetry.cpp
new file mode 100644
index 0000000000..067783cd8c
--- /dev/null
+++ b/toolkit/components/telemetry/dap/DAPTelemetry.cpp
@@ -0,0 +1,295 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DAPTelemetryBindings.h"
+
+#include "mozilla/Logging.h"
+#include "nsPromiseFlatString.h"
+
+#include "nss.h"
+#include "nsNSSComponent.h"
+#include "secmodt.h"
+#include "pk11pub.h"
+#include "ScopedNSSTypes.h"
+
+static mozilla::LazyLogModule sLogger("DAPTelemetry");
+#undef LOG
+#define LOG(...) MOZ_LOG(sLogger, mozilla::LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(DAPTelemetry, nsIDAPTelemetry)
+
+// This function was copied from pk11_hpke_unittest.cc
+// And modified to take a Span.
+static std::vector<uint8_t> Pkcs8(Span<const uint8_t> sk,
+ Span<const uint8_t> pk) {
+ // Only X25519 format.
+ std::vector<uint8_t> v(105);
+ v.assign({
+ 0x30, 0x67, 0x02, 0x01, 0x00, 0x30, 0x14, 0x06, 0x07, 0x2a, 0x86, 0x48,
+ 0xce, 0x3d, 0x02, 0x01, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xda,
+ 0x47, 0x0f, 0x01, 0x04, 0x4c, 0x30, 0x4a, 0x02, 0x01, 0x01, 0x04, 0x20,
+ });
+ v.insert(v.end(), sk.begin(), sk.end());
+ v.insert(v.end(), {
+ 0xa1,
+ 0x23,
+ 0x03,
+ 0x21,
+ 0x00,
+ });
+ v.insert(v.end(), pk.begin(), pk.end());
+ return v;
+}
+
+// This function was copied from cpputil.h
+static unsigned char* toUcharPtr(const uint8_t* v) {
+ return const_cast<unsigned char*>(static_cast<const unsigned char*>(v));
+}
+
+/// If successful this returns a pointer to a HpkeContext which must be
+/// released using dapDestroyHpkeContext or PK11_HPKE_DestroyContext.
+HpkeContext* dapSetupHpkeContextInternal(
+ const uint8_t* aKey, uint32_t aKeyLength, const uint8_t* aInfo,
+ uint32_t aInfoLength, SECKEYPublicKey* aPkE, SECKEYPrivateKey* aSkE,
+ nsTArray<uint8_t>* aOutputEncapsulatedKey) {
+ SECStatus status = PK11_HPKE_ValidateParameters(
+ HpkeDhKemX25519Sha256, HpkeKdfHkdfSha256, HpkeAeadAes128Gcm);
+ if (status != SECSuccess) {
+ MOZ_LOG(sLogger, mozilla::LogLevel::Error,
+ ("Invalid HKPE parameters found."));
+ return nullptr;
+ }
+
+ UniqueHpkeContext context(
+ PK11_HPKE_NewContext(HpkeDhKemX25519Sha256, HpkeKdfHkdfSha256,
+ HpkeAeadAes128Gcm, nullptr, nullptr));
+
+ SECKEYPublicKey* pkR_raw = nullptr;
+ status = PK11_HPKE_Deserialize(context.get(), aKey, aKeyLength, &pkR_raw);
+ UniqueSECKEYPublicKey pkR(pkR_raw);
+ pkR_raw = nullptr;
+ if (status != SECSuccess) {
+ MOZ_LOG(sLogger, mozilla::LogLevel::Error,
+ ("Failed to deserialize HPKE encryption key."));
+ return nullptr;
+ }
+
+ const SECItem hpkeInfo = {siBuffer, toUcharPtr(aInfo), aInfoLength};
+
+ status = PK11_HPKE_SetupS(context.get(), aPkE, aSkE, pkR.get(), &hpkeInfo);
+ if (status != SECSuccess) {
+ MOZ_LOG(sLogger, mozilla::LogLevel::Error, ("HPKE setup failed."));
+ return nullptr;
+ }
+
+ const SECItem* hpkeEncapKey = PK11_HPKE_GetEncapPubKey(context.get());
+ if (!hpkeEncapKey) {
+ MOZ_LOG(sLogger, mozilla::LogLevel::Error,
+ ("Failed to get HPKE encapsulated public key."));
+ return nullptr;
+ }
+
+ aOutputEncapsulatedKey->AppendElements(hpkeEncapKey->data, hpkeEncapKey->len);
+
+ return context.release();
+}
+
+extern "C" {
+/// If successful this returns a pointer to a PK11Context which must be
+/// released using dapReleaseCmac.
+void* dapStartCmac(uint8_t* aSeed) {
+ MOZ_RELEASE_ASSERT(EnsureNSSInitializedChromeOrContent(),
+ "Could not initialize NSS.");
+
+ UniquePK11SlotInfo slot(PK11_GetBestSlot(CKM_AES_CMAC, nullptr));
+ MOZ_RELEASE_ASSERT(slot, "DAPTelemetry: dapStartCmac(): Failed to get slot.");
+
+ SECItem keyItem = {siBuffer, toUcharPtr(aSeed), 16};
+ UniquePK11SymKey key(PK11_ImportSymKey(slot.get(), CKM_AES_CMAC,
+ PK11_OriginUnwrap, CKA_SIGN, &keyItem,
+ nullptr));
+ MOZ_RELEASE_ASSERT(key,
+ "DAPTelemetry: dapStartCmac(): Failed to import key.");
+
+ UniqueSECItem param(PK11_ParamFromIV(CKM_AES_CMAC, nullptr));
+ MOZ_RELEASE_ASSERT(
+ param, "DAPTelemetry: dapStartCmac(): Failed to create parameters.");
+
+ PK11Context* ctx = PK11_CreateContextBySymKey(CKM_AES_CMAC, CKA_SIGN,
+ key.get(), param.get());
+ MOZ_RELEASE_ASSERT(ctx,
+ "DAPTelemetry: dapStartCmac(): Failed to create context.");
+
+ return ctx;
+}
+
+void dapUpdateCmac(void* aContext, const uint8_t* aData, uint32_t aDataLen) {
+ SECStatus res =
+ PK11_DigestOp(static_cast<PK11Context*>(aContext), aData, aDataLen);
+ MOZ_RELEASE_ASSERT(
+ res == SECSuccess,
+ "DAPTelemetry: dapUpdateCmac(): Mac digest update failed.");
+}
+
+void dapFinalizeCmac(void* aContext, uint8_t* aMac) {
+ unsigned int maclen = 0;
+
+ SECStatus res =
+ PK11_DigestFinal(static_cast<PK11Context*>(aContext), aMac, &maclen, 16);
+ MOZ_RELEASE_ASSERT(
+ res == SECSuccess,
+ "DAPTelemetry: dapFinalizeCmac(): PK11_DigestFinal failed.");
+ MOZ_RELEASE_ASSERT(
+ maclen == 16,
+ "DAPTelemetry: dapFinalizeCmac(): PK11_DigestFinal returned "
+ "too few MAC bytes.");
+}
+
+void dapReleaseCmac(void* aContext) {
+ PK11_DestroyContext(static_cast<PK11Context*>(aContext), true);
+}
+
+/// If successful this returns a pointer to a PK11Context which must be
+/// released using dapReleaseCtrCtx.
+void* dapStartAesCtr(const uint8_t* aKey) {
+ MOZ_RELEASE_ASSERT(EnsureNSSInitializedChromeOrContent(),
+ "Could not initialize NSS.");
+
+ UniquePK11SlotInfo slot(PK11_GetBestSlot(CKM_AES_CTR, nullptr));
+ MOZ_RELEASE_ASSERT(aKey,
+ "DAPTelemetry: dapStartAesCtr(): Failed to get slot.");
+
+ SECItem ctrKeyItem = {siBuffer, toUcharPtr(aKey), 16};
+ UniquePK11SymKey ctrKey(PK11_ImportSymKey(slot.get(), CKM_AES_CTR,
+ PK11_OriginUnwrap, CKA_ENCRYPT,
+ &ctrKeyItem, nullptr));
+ MOZ_RELEASE_ASSERT(ctrKey,
+ "DAPTelemetry: dapStartAesCtr(): Failed to create key.");
+
+ SECItem ctrParam = {siBuffer, nullptr, 0};
+ CK_AES_CTR_PARAMS ctrParamInner;
+ ctrParamInner.ulCounterBits = 128;
+ memset(&ctrParamInner.cb, 0, 16);
+ ctrParam.type = siBuffer;
+ ctrParam.data = reinterpret_cast<unsigned char*>(&ctrParamInner);
+ ctrParam.len = static_cast<unsigned int>(sizeof(ctrParamInner));
+
+ PK11Context* ctrCtx = PK11_CreateContextBySymKey(CKM_AES_CTR, CKA_ENCRYPT,
+ ctrKey.get(), &ctrParam);
+ MOZ_RELEASE_ASSERT(
+ ctrCtx, "DAPTelemetry: dapStartAesCtr(): Failed to create context.");
+
+ return ctrCtx;
+}
+
+void dapCtrFillBuffer(void* aContext, uint8_t* aBuffer, int aBufferSize) {
+ int ctlen = 0;
+ memset(aBuffer, 0, aBufferSize);
+ SECStatus res = PK11_CipherOp(static_cast<PK11Context*>(aContext), aBuffer,
+ &ctlen, aBufferSize, aBuffer, aBufferSize);
+ MOZ_RELEASE_ASSERT(res == SECSuccess,
+ "DAPTelemetry: dapCtrFillBuffer(...): Encryption failed.");
+}
+
+void dapReleaseCtrCtx(void* aContext) {
+ PK11_DestroyContext(static_cast<PK11Context*>(aContext), true);
+}
+
+/// Takes additional ephemeral keys to make everything deterministic for test
+/// vectors.
+/// If successful this returns a pointer to a HpkeContext which must be
+/// released using dapDestroyHpkeContext or PK11_HPKE_DestroyContext.
+HpkeContext* dapSetupHpkeContextForTesting(
+ const uint8_t* aKey, uint32_t aKeyLength, const uint8_t* aInfo,
+ uint32_t aInfoLength, const uint8_t* aPkEm, uint32_t aPkEmLength,
+ const uint8_t* aSkEm, uint32_t aSkEmLength,
+ nsTArray<uint8_t>* aOutputEncapsulatedKey) {
+ Span<const uint8_t> sk_e(aSkEm, aSkEm + aSkEmLength);
+ Span<const uint8_t> pk_e(aPkEm, aPkEm + aPkEmLength);
+ std::vector<uint8_t> pkcs8_e = Pkcs8(sk_e, pk_e);
+
+ MOZ_RELEASE_ASSERT(EnsureNSSInitializedChromeOrContent(),
+ "Could not initialize NSS.");
+
+ UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+ MOZ_RELEASE_ASSERT(slot, "Failed to get slot.");
+
+ SECItem keys_e = {siBuffer, toUcharPtr(pkcs8_e.data()),
+ static_cast<unsigned int>(pkcs8_e.size())};
+ SECKEYPrivateKey* internal_skE_raw = nullptr;
+ SECStatus rv = PK11_ImportDERPrivateKeyInfoAndReturnKey(
+ slot.get(), &keys_e, nullptr, nullptr, false, false, KU_ALL,
+ &internal_skE_raw, nullptr);
+ UniqueSECKEYPrivateKey internal_skE(internal_skE_raw);
+ internal_skE_raw = nullptr;
+ MOZ_RELEASE_ASSERT(rv == SECSuccess, "Failed to import skE/pkE.");
+
+ UniqueSECKEYPublicKey internal_pkE(
+ SECKEY_ConvertToPublicKey(internal_skE.get()));
+
+ UniqueHpkeContext result(dapSetupHpkeContextInternal(
+ aKey, aKeyLength, aInfo, aInfoLength, internal_pkE.get(),
+ internal_skE.get(), aOutputEncapsulatedKey));
+
+ return result.release();
+}
+
+void dapDestroyHpkeContext(HpkeContext* aContext) {
+ PK11_HPKE_DestroyContext(aContext, true);
+}
+
+bool dapHpkeEncrypt(HpkeContext* aContext, const uint8_t* aAad,
+ uint32_t aAadLength, const uint8_t* aPlaintext,
+ uint32_t aPlaintextLength,
+ nsTArray<uint8_t>* aOutputShare) {
+ SECItem aad_si = {siBuffer, toUcharPtr(aAad), aAadLength};
+ SECItem plaintext_si = {siBuffer, toUcharPtr(aPlaintext), aPlaintextLength};
+ SECItem* ct = nullptr;
+ SECStatus rv = PK11_HPKE_Seal(aContext, &aad_si, &plaintext_si, &ct);
+ if (rv != SECSuccess) {
+ return false;
+ }
+
+ aOutputShare->AppendElements(ct->data, ct->len);
+
+ return true;
+}
+
+bool dapHpkeEncryptOneshot(const uint8_t* aKey, uint32_t aKeyLength,
+ const uint8_t* aInfo, uint32_t aInfoLength,
+ const uint8_t* aAad, uint32_t aAadLength,
+ const uint8_t* aPlaintext, uint32_t aPlaintextLength,
+ nsTArray<uint8_t>* aOutputEncapsulatedKey,
+ nsTArray<uint8_t>* aOutputShare) {
+ MOZ_RELEASE_ASSERT(EnsureNSSInitializedChromeOrContent(),
+ "Could not initialize NSS.");
+ UniqueHpkeContext context(
+ dapSetupHpkeContextInternal(aKey, aKeyLength, aInfo, aInfoLength, nullptr,
+ nullptr, aOutputEncapsulatedKey));
+ if (!context) {
+ return false;
+ }
+
+ return dapHpkeEncrypt(context.get(), aAad, aAadLength, aPlaintext,
+ aPlaintextLength, aOutputShare);
+}
+}
+
+NS_IMETHODIMP DAPTelemetry::GetReport(
+ const nsTArray<uint8_t>& aLeaderHpkeConfig,
+ const nsTArray<uint8_t>& aHelperHpkeConfig, uint8_t aMeasurement,
+ const nsTArray<uint8_t>& aTaskID, const uint64_t aTimePrecision,
+ nsTArray<uint8_t>& aOutReport) {
+ MOZ_RELEASE_ASSERT(aTaskID.Length() == 32, "TaskID must have 32 bytes.");
+ if (!dapGetReport(&aLeaderHpkeConfig, &aHelperHpkeConfig, aMeasurement,
+ &aTaskID, aTimePrecision, &aOutReport)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/telemetry/dap/DAPTelemetry.h b/toolkit/components/telemetry/dap/DAPTelemetry.h
new file mode 100644
index 0000000000..b6a0c8c17f
--- /dev/null
+++ b/toolkit/components/telemetry/dap/DAPTelemetry.h
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_nsIDAPTelemetry_h__
+#define mozilla_nsIDAPTelemetry_h__
+
+#include "nsIDAPTelemetry.h"
+
+namespace mozilla {
+
+class DAPTelemetry final : public nsIDAPTelemetry {
+ NS_DECL_ISUPPORTS
+
+ NS_DECL_NSIDAPTELEMETRY
+
+ public:
+ DAPTelemetry() = default;
+
+ private:
+ ~DAPTelemetry() = default;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/telemetry/dap/DAPTelemetryBindings.h b/toolkit/components/telemetry/dap/DAPTelemetryBindings.h
new file mode 100644
index 0000000000..bf62d7c0bc
--- /dev/null
+++ b/toolkit/components/telemetry/dap/DAPTelemetryBindings.h
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DAPTelemetryBindings_h
+#define DAPTelemetryBindings_h
+
+#include "DAPTelemetry.h"
+#include "mozilla/dap_ffi_generated.h"
+
+#endif // DAPTelemetryBindings_h
diff --git a/toolkit/components/telemetry/dap/DAPTelemetrySender.sys.mjs b/toolkit/components/telemetry/dap/DAPTelemetrySender.sys.mjs
new file mode 100644
index 0000000000..0ce983f4a0
--- /dev/null
+++ b/toolkit/components/telemetry/dap/DAPTelemetrySender.sys.mjs
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+let lazy = {};
+
+XPCOMUtils.defineLazyGetter(lazy, "logConsole", function() {
+ return console.createInstance({
+ prefix: "DAPTelemetrySender",
+ maxLogLevelPref: "toolkit.telemetry.dap.logLevel",
+ });
+});
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm",
+});
+
+const PREF_LEADER = "toolkit.telemetry.dap_leader";
+const PREF_HELPER = "toolkit.telemetry.dap_helper";
+
+XPCOMUtils.defineLazyPreferenceGetter(lazy, "LEADER", PREF_LEADER, undefined);
+XPCOMUtils.defineLazyPreferenceGetter(lazy, "HELPER", PREF_HELPER, undefined);
+
+/**
+ * The purpose of this singleton is to handle sending of DAP telemetry data.
+ * The current DAP draft standard is available here:
+ * https://github.com/ietf-wg-ppm/draft-ietf-ppm-dap
+ *
+ * The specific purpose of this singleton is to make the necessary calls to fetch to do networking.
+ */
+
+export const DAPTelemetrySender = new (class {
+ startup() {
+ lazy.logConsole.info("Performing DAP startup");
+
+ if (lazy.NimbusFeatures.dapTelemetry.getVariable("task1Enabled")) {
+ // For now we are sending a constant value because it simplifies verification.
+ let measurement = 3;
+ this.sendVerificationTaskReport(measurement);
+ }
+ }
+
+ /**
+ * For testing: sends a hard coded report for a hard coded task.
+ *
+ * @param {number} measurement
+ */
+ async sendVerificationTaskReport(measurement) {
+ lazy.logConsole.info("Trying to send verification task.");
+
+ // For now there is only a single task which is hardcoded here.
+ /**
+ * @typedef {object} Task
+ * @property {string} id_hexstring - The task's ID hex encoded.
+ * @property {string} id_base64 - The same ID base 64 encoded.
+ * @property {string} leader_endpoint - Base URL for the leader.
+ * @property {string} helper_endpoint - Base URL for the helper.
+ * @property {number} time_precision - Timestamps (in s) are rounded to the nearest multiple of this.
+ */
+ const task = {
+ // Note that this does not exactly match the task definition from the standard linked-to above.
+ id_hexstring:
+ "4ad95d3b67332ff89a505da296315b88b88d4f1c5535d3c780fbae1162c79ec9",
+ id_base64: "StldO2czL_iaUF2iljFbiLiNTxxVNdPHgPuuEWLHnsk",
+ leader_endpoint: null,
+ helper_endpoint: null,
+ time_precision: 600,
+ };
+
+ task.leader_endpoint = lazy.LEADER;
+ if (!task.leader_endpoint) {
+ lazy.logConsole.error('Preference "' + PREF_LEADER + '" not set');
+ return;
+ }
+
+ task.helper_endpoint = lazy.HELPER;
+ if (!task.helper_endpoint) {
+ lazy.logConsole.error('Preference "' + PREF_HELPER + '" not set');
+ return;
+ }
+
+ try {
+ let report = await this.generateReport(task, measurement);
+ Glean.dap.reportGenerationStatus.success.add(1);
+ await this.sendReport(task.leader_endpoint, report);
+ } catch (e) {
+ Glean.dap.reportGenerationStatus.failure.add(1);
+ lazy.logConsole.error("DAP report generation failed: " + e.message);
+ }
+ }
+
+ /**
+ * Downloads HPKE configs for endpoints and generates report.
+ *
+ * @param {Task} task
+ * Definition of the task for which the measurement was taken.
+ * @param {number} measurement
+ * The measured value for which a report is generated.
+ * @returns Promise
+ * @resolves {Uint8Array} The generated binary report data.
+ * @rejects {Error} If an exception is thrown while generating the report.
+ */
+ async generateReport(task, measurement) {
+ let [leader_config_bytes, helper_config_bytes] = await Promise.all([
+ this.getHpkeConfig(
+ task.leader_endpoint + "/hpke_config?task_id=" + task.id_base64
+ ),
+ this.getHpkeConfig(
+ task.helper_endpoint + "/hpke_config?task_id=" + task.id_base64
+ ),
+ ]);
+ let task_id = hexString2Binary(task.id_hexstring);
+ let report = {};
+ Services.DAPTelemetry.GetReport(
+ leader_config_bytes,
+ helper_config_bytes,
+ measurement,
+ task_id,
+ task.time_precision,
+ report
+ );
+ let reportData = new Uint8Array(report.value);
+ return reportData;
+ }
+
+ /**
+ * Fetches TLS encoded HPKE config from a URL.
+ *
+ * @param {string} endpoint
+ * The URL from where to get the data.
+ * @returns Promise
+ * @resolves {Uint8Array} The binary representation of the endpoint configuration.
+ * @rejects {Error} If an exception is thrown while fetching the configuration.
+ */
+ async getHpkeConfig(endpoint) {
+ let response = await fetch(endpoint);
+ if (!response.ok) {
+ throw new Error(
+ `Failed to retrieve HPKE config for DAP from: ${endpoint}. Response: ${
+ response.status
+ }: ${await response.text()}.`
+ );
+ }
+ let buffer = await response.arrayBuffer();
+ let hpke_config_bytes = new Uint8Array(buffer);
+ return hpke_config_bytes;
+ }
+
+ /**
+ * Sends a report to the leader.
+ *
+ * @param {string} leader_endpoint
+ * The URL for the leader.
+ * @param {Uint8Array} report
+ * Raw bytes of the TLS encoded report.
+ * @returns Promise
+ * @resolves {undefined} Once the attempt to send the report completes, whether or not it was successful.
+ */
+ async sendReport(leader_endpoint, report) {
+ const upload_path = leader_endpoint + "/upload";
+ try {
+ let response = await fetch(upload_path, {
+ method: "POST",
+ headers: { "Content-Type": "application/dap-report" },
+ body: report,
+ });
+
+ if (response.status != 200) {
+ let error = await response.json();
+ lazy.logConsole.error(
+ `Sending failed. HTTP response: ${response.status} ${response.statusText}. Error: ${error.type} ${error.title}`
+ );
+
+ Glean.dap.uploadStatus.failure.add(1);
+ } else {
+ lazy.logConsole.info("DAP report sent");
+ Glean.dap.uploadStatus.success.add(1);
+ }
+ } catch (err) {
+ lazy.logConsole.error("Failed to send report. fetch failed", err);
+ Glean.dap.uploadStatus.failure.add(1);
+ }
+ }
+})();
+
+/**
+ * Converts a hex representation of a byte string into an array
+ * @param {string} hexstring - a list of bytes represented as a hex string two characters per bytes
+ * @return {Uint8Array} - the input byte list as an array
+ */
+function hexString2Binary(hexstring) {
+ const binlen = hexstring.length / 2;
+ let binary = new Uint8Array(binlen);
+ for (var i = 0; i < binlen; i++) {
+ binary[i] = parseInt(hexstring.substring(2 * i, 2 * (i + 1)), 16);
+ }
+ return binary;
+}
diff --git a/toolkit/components/telemetry/dap/components.conf b/toolkit/components/telemetry/dap/components.conf
new file mode 100644
index 0000000000..237868bcee
--- /dev/null
+++ b/toolkit/components/telemetry/dap/components.conf
@@ -0,0 +1,10 @@
+Classes = [
+ {
+ 'cid': '{58a4c579-d2dd-46b7-9c3b-6881a1c36c6a}',
+ 'interfaces': ['nsIDAPTelemetry'],
+ 'contract_ids': ['@mozilla.org/base/daptelemetry;1'],
+ 'type': 'mozilla::DAPTelemetry',
+ 'headers': ['mozilla/DAPTelemetry.h'],
+ 'js_name': 'DAPTelemetry',
+ },
+]
diff --git a/toolkit/components/telemetry/dap/ffi-gtest/Cargo.toml b/toolkit/components/telemetry/dap/ffi-gtest/Cargo.toml
new file mode 100644
index 0000000000..756d93a74e
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi-gtest/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "dap_ffi-gtest"
+version = "0.1.0"
+authors = [
+ "Simon Friedberger <simon@mozilla.com>",
+]
+license = "MPL-2.0"
+description = "Tests for Rust code for DAP; mainly encoding and NSS bindings."
+edition = "2021"
+
+[dependencies]
+dap_ffi = { path = "../ffi" }
+hex = { version = "0.4.3", features = ["serde"] }
+prio = {version = "0.9.0", default-features = false }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = { version = "1.0" }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+
+[lib]
+path = "test.rs"
diff --git a/toolkit/components/telemetry/dap/ffi-gtest/PrgAes128_tests.json b/toolkit/components/telemetry/dap/ffi-gtest/PrgAes128_tests.json
new file mode 100644
index 0000000000..f433e3ef31
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi-gtest/PrgAes128_tests.json
@@ -0,0 +1 @@
+[{"seed":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"info_string":[0,0,0,0,0,0,0,0,0,0],"buffer1_out":[73,12,252,221,214,131,103,213,8,125,53,152,93,163,78,35,181,41,221,100],"buffer2_out":[244,233,25,27,130,143,245,245,158,25,70,216,49,52,48,166,234,182,146,63,140,157,155,191,24,195,111,244,212,81,93,76,198,93,108,144,80,116,232,76,229,167,252,3,88,209,226,11,167,130,127,16,165,185,225,16,200,194,93,70,166,7,104,33,164,93,26,14,98,188,210,45,76,191,10,107,145,174,4,247,99,162,77,183,198,246,163,162,1,109,16,172,213,145,124,163,219,215,60,58,210,65,21,106,109,244,51,140,167,82,216,222,113,105,194,189,119,146,17,170,232,216,191,224,64,216,54,37,242,62,127,108,232,195,19,20,0,168,102,98,72,30,21,198,235,241,35,230,107,24,81,75,174,49,10,177,238,183,131,209,64,95,220,30,87,230,221,72,66,201,106,44,22,52,39,159,73,157,120,133,3,103,114,54,48,59,223,200,37,182,24,160,43,224,39,242,20,252,24,197,181,91,1,189,78,207,184,200,98,141,141,172,212,22,13,86,63,54,85,97,230,123,117,85,60,48,111,136,254,126,252,250,21,126,157,127,72,148,100,205,179,154,67,69,149,96,95,62,241,104,30,63,72,198,75,238,42,174,128,118,110,8,105,176,219,24,69,17,69,76,9,56,146,195,198,12,89,50,133,144,43,93,98,45,54,253,48,72,38,128,108,22,173,8,228,180,254,96,224,103,215,255,163,189,142,35,18,102,166,241,225,16,231,106,31,29,230,172,108,134,57,69,126,120,45,60,149,96,91,17,43,220,103,217,94,149,25,111,50,252,237,147,4,21,230,128,132,41,51,132,6,134,167,155,179,79,38,181,129,149,223,125,192,48,71,122,69,160,136,172,171,62,135,206,109,219,68,184,173,248,255,120,31,195,85,207,177,158,241,42,246,250,7,124,135,67,6,2,149,107,98,118,63,54,55,104,176,194,128,79,49,220,31,31,185,63,205,176,36,28,17,34,138,162,2,77,60,82,174,137,223,14,113,206,111,132,76,246,185,64,161,205,118,132,142,133,165,75,139,161,244,42,189,21,198,199,9,252,244,181,36,210,46,13,173,199,33,252,174,231,207,112,132,192,146,201,55,45,90,176,47,111,190,198,154,191,178,238,103,255,239,130,179,60,84,217,156,246,208,179]},{"seed":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"info_string":[105,110,102,111,32,115,116,114,105,110,103],"buffer1_out":[],"buffer2_out":[204,243,190,112,76,152,33,130,173,41,97,233,121,90,136,170]},{"seed":[5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5],"info_string":[105,110,102,111,32,115,116,114,105,110,103],"buffer1_out":[],"buffer2_out":[134,173,103,37,215,0,146,211,132,6,147,110,147,170,26,196]},{"seed":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"info_string":[110,110,102,111,32,115,116,114,105,110,103],"buffer1_out":[],"buffer2_out":[245,62,144,220,139,16,59,178,153,145,113,98,101,104,47,213]},{"seed":[3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3],"info_string":[],"buffer1_out":[67,49],"buffer2_out":[108,157,199,13,12]},{"seed":[3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3],"info_string":[110,110,110,110,110],"buffer1_out":[152,11],"buffer2_out":[186,202,32,223,212]}] \ No newline at end of file
diff --git a/toolkit/components/telemetry/dap/ffi-gtest/TestDAPTelemetry.cpp b/toolkit/components/telemetry/dap/ffi-gtest/TestDAPTelemetry.cpp
new file mode 100644
index 0000000000..080224ad99
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi-gtest/TestDAPTelemetry.cpp
@@ -0,0 +1,23 @@
+
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/DAPTelemetryBindings.h"
+
+using namespace mozilla;
+
+extern "C" void dap_test_prg();
+TEST(DAPTelemetryTests, TestPrg)
+{ dap_test_prg(); }
+
+extern "C" void dap_test_hpke_encrypt();
+TEST(DAPTelemetryTests, TestHpkeEnc)
+{ dap_test_hpke_encrypt(); }
+
+extern "C" void dap_test_encoding();
+TEST(DAPTelemetryTests, TestReportSerialization)
+{ dap_test_encoding(); }
diff --git a/toolkit/components/telemetry/dap/ffi-gtest/moz.build b/toolkit/components/telemetry/dap/ffi-gtest/moz.build
new file mode 100644
index 0000000000..b8444a26d5
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi-gtest/moz.build
@@ -0,0 +1,7 @@
+UNIFIED_SOURCES = ["TestDAPTelemetry.cpp"]
+FINAL_LIBRARY = "xul-gtest"
+
+TEST_HARNESS_FILES.gtest += [
+ "../../../../../security/nss/gtests/pk11_gtest/hpke-vectors.json",
+ "PrgAes128_tests.json",
+]
diff --git a/toolkit/components/telemetry/dap/ffi-gtest/test.rs b/toolkit/components/telemetry/dap/ffi-gtest/test.rs
new file mode 100644
index 0000000000..8db79f5f05
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi-gtest/test.rs
@@ -0,0 +1,230 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use serde::Deserialize;
+use std::ffi::c_void;
+use std::fs::File;
+use std::io::Cursor;
+
+use thin_vec::ThinVec;
+
+use dap_ffi::prg::PrgAes128Alt;
+use dap_ffi::types::Report;
+
+use prio::codec::{Decode, Encode};
+use prio::vdaf::prg::{Prg, SeedStream};
+
+#[no_mangle]
+pub extern "C" fn dap_test_encoding() {
+ let r = Report::new_dummy();
+ let mut encoded = Vec::<u8>::new();
+ Report::encode(&r, &mut encoded);
+ let decoded = Report::decode(&mut Cursor::new(&encoded)).expect("Report decoding failed!");
+ if r != decoded {
+ println!("Report:");
+ println!("{:?}", r);
+ println!("Encoded Report:");
+ println!("{:?}", encoded);
+ println!("Decoded Report:");
+ println!("{:?}", decoded);
+ panic!("Report changed after encoding & decoding.");
+ }
+}
+
+#[derive(Deserialize, Debug)]
+struct PrgTestCase {
+ seed: [u8; 16],
+ info_string: Vec<u8>,
+ buffer1_out: Vec<u8>,
+ buffer2_out: Vec<u8>,
+}
+
+#[no_mangle]
+pub extern "C" fn dap_test_prg() {
+ let file = File::open("PrgAes128_tests.json").unwrap();
+ let testcases: Vec<PrgTestCase> = serde_json::from_reader(file).unwrap();
+ for testcase in testcases {
+ let mut p = PrgAes128Alt::init(&testcase.seed);
+ p.update(&testcase.info_string);
+ let mut s = p.into_seed_stream();
+ let mut b1 = vec![0u8; testcase.buffer1_out.len()];
+ s.fill(&mut b1);
+ assert_eq!(b1, testcase.buffer1_out);
+ let mut b2 = vec![0u8; testcase.buffer2_out.len()];
+ s.fill(&mut b2);
+ assert_eq!(b2, testcase.buffer2_out);
+ }
+}
+
+extern "C" {
+ pub fn dapHpkeEncrypt(
+ aContext: *mut c_void,
+ aAad: *mut u8,
+ aAadLength: u32,
+ aPlaintext: *mut u8,
+ aPlaintextLength: u32,
+ aOutputShare: &mut ThinVec<u8>,
+ ) -> bool;
+ pub fn dapSetupHpkeContextForTesting(
+ aKey: *const u8,
+ aKeyLength: u32,
+ aInfo: *mut u8,
+ aInfoLength: u32,
+ aPkEm: *const u8,
+ aPkEmLength: u32,
+ aSkEm: *const u8,
+ aSkEmLength: u32,
+ aOutputEncapsulatedKey: &mut ThinVec<u8>,
+ ) -> *mut c_void;
+ pub fn dapDestroyHpkeContext(aContext: *mut c_void);
+}
+
+struct HpkeContext(*mut c_void);
+
+impl Drop for HpkeContext {
+ fn drop(&mut self) {
+ unsafe {
+ dapDestroyHpkeContext(self.0);
+ }
+ }
+}
+
+type Testsuites = Vec<CiphersuiteTest>;
+
+#[derive(Debug, Deserialize)]
+pub struct HexString(#[serde(with = "hex")] Vec<u8>);
+impl AsRef<[u8]> for HexString {
+ fn as_ref(&self) -> &[u8] {
+ &self.0
+ }
+}
+#[derive(Debug, Deserialize)]
+struct CiphersuiteTest {
+ mode: i64,
+ kem_id: i64,
+ kdf_id: i64,
+ aead_id: i64,
+ info: HexString,
+ #[serde(rename = "ikmR")]
+ ikm_r: HexString,
+ #[serde(rename = "ikmE")]
+ ikm_e: HexString,
+ #[serde(rename = "skRm")]
+ sk_r_m: HexString,
+ #[serde(rename = "skEm")]
+ sk_e_m: HexString,
+ #[serde(rename = "pkRm")]
+ pk_r_m: HexString,
+ #[serde(rename = "pkEm")]
+ pk_e_m: HexString,
+ enc: HexString,
+ shared_secret: HexString,
+ key_schedule_context: HexString,
+ secret: HexString,
+ key: HexString,
+ base_nonce: HexString,
+ exporter_secret: HexString,
+ encryptions: Vec<Encryption>,
+ exports: Vec<Export>,
+ psk: Option<HexString>,
+ psk_id: Option<HexString>,
+ ikm_s: Option<HexString>,
+ sk_sm: Option<HexString>,
+ pk_sm: Option<HexString>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Encryption {
+ pub aad: HexString,
+ pub ciphertext: HexString,
+ pub nonce: HexString,
+ pub plaintext: HexString,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Export {
+ pub exporter_context: HexString,
+ #[serde(rename = "L")]
+ pub length: i64,
+ pub exported_value: HexString,
+}
+
+#[no_mangle]
+pub extern "C" fn dap_test_hpke_encrypt() {
+ let file = File::open("hpke-vectors.json").unwrap();
+ let tests: Testsuites = serde_json::from_reader(file).unwrap();
+
+ let mut have_tested = false;
+
+ for (test_idx, test) in tests.into_iter().enumerate() {
+ // Mode must be "Base"
+ if test.mode != 0
+ // KEM must be DHKEM(X25519, HKDF-SHA256)
+ || test.kem_id != 32
+ // KDF must be HKDF-SHA256
+ || test.kdf_id != 1
+ // AEAD must be AES-128-GCM
+ || test.aead_id != 1
+ {
+ continue;
+ }
+
+ have_tested = true;
+
+ let mut pk_r_serialized = test.pk_r_m.0;
+ let mut info = test.info.0;
+ let mut pk_e_serialized = test.pk_e_m.0;
+ let mut sk_e_serialized = test.sk_e_m.0;
+
+ let mut encapsulated_key = ThinVec::<u8>::new();
+
+ let ctx = HpkeContext(unsafe {
+ dapSetupHpkeContextForTesting(
+ pk_r_serialized.as_mut_ptr(),
+ pk_r_serialized.len().try_into().unwrap(),
+ info.as_mut_ptr(),
+ info.len().try_into().unwrap(),
+ pk_e_serialized.as_mut_ptr(),
+ pk_e_serialized.len().try_into().unwrap(),
+ sk_e_serialized.as_mut_ptr(),
+ sk_e_serialized.len().try_into().unwrap(),
+ &mut encapsulated_key,
+ )
+ });
+ if ctx.0.is_null() {
+ panic!("Failed to set up HPKE context.");
+ }
+ if encapsulated_key != test.enc.0 {
+ panic!("Encapsulated key is wrong!");
+ }
+
+ for (encryption_idx, encryption) in test.encryptions.into_iter().enumerate() {
+ let mut encrypted_share = ThinVec::<u8>::new();
+
+ let mut aad = encryption.aad.0.clone();
+ let mut pt = encryption.plaintext.0.clone();
+ unsafe {
+ dapHpkeEncrypt(
+ ctx.0,
+ aad.as_mut_ptr(),
+ aad.len().try_into().unwrap(),
+ pt.as_mut_ptr(),
+ pt.len().try_into().unwrap(),
+ &mut encrypted_share,
+ );
+ }
+
+ if encrypted_share != encryption.ciphertext.0 {
+ println!("Test: {}, Encryption: {}", test_idx, encryption_idx);
+ println!("Expected:");
+ println!("{:?}", encryption.ciphertext.0);
+ println!("Actual:");
+ println!("{:?}", encrypted_share);
+ panic!("Encryption outputs did not match!");
+ }
+ }
+ }
+
+ assert!(have_tested);
+}
diff --git a/toolkit/components/telemetry/dap/ffi/Cargo.toml b/toolkit/components/telemetry/dap/ffi/Cargo.toml
new file mode 100644
index 0000000000..412e98eb80
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "dap_ffi"
+version = "0.1.0"
+edition = "2021"
+authors = [
+ "Simon Friedberger <simon@mozilla.com>",
+]
+license = "MPL-2.0"
+
+[dependencies]
+prio = {version = "0.9.0", default-features = false }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+rand = "0.8"
diff --git a/toolkit/components/telemetry/dap/ffi/cbindgen.toml b/toolkit/components/telemetry/dap/ffi/cbindgen.toml
new file mode 100644
index 0000000000..12b3a58a1a
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi/cbindgen.toml
@@ -0,0 +1,11 @@
+header = """/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */"""
+autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */
+#ifndef DAPTelemetryBindings_h
+#error "Don't include this file directly, instead include DAPTelemetryBindings.h"
+#endif
+"""
+
+[export.rename]
+"ThinVec" = "nsTArray" \ No newline at end of file
diff --git a/toolkit/components/telemetry/dap/ffi/src/lib.rs b/toolkit/components/telemetry/dap/ffi/src/lib.rs
new file mode 100644
index 0000000000..a8a52a248b
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi/src/lib.rs
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::io::Cursor;
+
+use thin_vec::ThinVec;
+
+pub mod types;
+use types::HpkeConfig;
+use types::Report;
+use types::ReportID;
+use types::ReportMetadata;
+use types::TaskID;
+use types::Time;
+
+pub mod prg;
+use prg::PrgAes128Alt;
+
+use prio::codec::Encode;
+use prio::codec::{encode_u32_items, Decode};
+use prio::field::Field128;
+use prio::flp::types::Sum;
+use prio::vdaf::prio3::Prio3;
+use prio::vdaf::Client;
+use prio::vdaf::VdafError;
+
+use crate::types::HpkeCiphertext;
+
+type Prio3Aes128SumAlt = Prio3<Sum<Field128>, PrgAes128Alt, 16>;
+
+extern "C" {
+ pub fn dapHpkeEncryptOneshot(
+ aKey: *const u8,
+ aKeyLength: u32,
+ aInfo: *const u8,
+ aInfoLength: u32,
+ aAad: *const u8,
+ aAadLength: u32,
+ aPlaintext: *const u8,
+ aPlaintextLength: u32,
+ aOutputEncapsulatedKey: &mut ThinVec<u8>,
+ aOutputShare: &mut ThinVec<u8>,
+ ) -> bool;
+}
+
+pub fn new_prio(num_aggregators: u8, bits: u32) -> Result<Prio3Aes128SumAlt, VdafError> {
+ if bits > 64 {
+ return Err(VdafError::Uncategorized(format!(
+ "bit length ({}) exceeds limit for aggregate type (64)",
+ bits
+ )));
+ }
+
+ Prio3::new(num_aggregators, Sum::new(bits as usize)?)
+}
+
+enum Role {
+ Leader = 2,
+ Helper = 3,
+}
+
+/// A minimal wrapper around the FFI function which mostly just converts datatypes.
+fn hpke_encrypt_wrapper(
+ plain_share: &Vec<u8>,
+ aad: &Vec<u8>,
+ info: &Vec<u8>,
+ hpke_config: &HpkeConfig,
+) -> Result<HpkeCiphertext, Box<dyn std::error::Error>> {
+ let mut encrypted_share = ThinVec::<u8>::new();
+ let mut encapsulated_key = ThinVec::<u8>::new();
+ unsafe {
+ if !dapHpkeEncryptOneshot(
+ hpke_config.public_key.as_ptr(),
+ hpke_config.public_key.len() as u32,
+ info.as_ptr(),
+ info.len() as u32,
+ aad.as_ptr(),
+ aad.len() as u32,
+ plain_share.as_ptr(),
+ plain_share.len() as u32,
+ &mut encapsulated_key,
+ &mut encrypted_share,
+ ) {
+ return Err(Box::from("Encryption failed."));
+ }
+ }
+
+ Ok(HpkeCiphertext {
+ config_id: hpke_config.id,
+ enc: encapsulated_key.to_vec(),
+ payload: encrypted_share.to_vec(),
+ })
+}
+
+/// Pre-fill the info part of the HPKE sealing with the constants from the standard.
+fn make_base_info() -> Vec<u8> {
+ let mut info = Vec::<u8>::new();
+ const START: &[u8] = "dap-02 input share".as_bytes();
+ info.extend(START);
+ const FIXED: u8 = 1;
+ info.push(FIXED);
+
+ info
+}
+
+/// This function creates a full report - ready to send - for a measurement.
+///
+/// To do that it also needs the HPKE configurations for the endpoints and some
+/// additional data which is part of the authentication.
+fn get_dap_report_internal(
+ leader_hpke_config_encoded: &ThinVec<u8>,
+ helper_hpke_config_encoded: &ThinVec<u8>,
+ measurement: u32,
+ task_id: &[u8; 32],
+ time_precision: u64,
+) -> Result<Report, Box<dyn std::error::Error>> {
+ let leader_hpke_config = HpkeConfig::decode(&mut Cursor::new(leader_hpke_config_encoded))?;
+ let helper_hpke_config = HpkeConfig::decode(&mut Cursor::new(helper_hpke_config_encoded))?;
+
+ let prio = new_prio(2, 2)?;
+ let (public_share, input_shares) = prio.shard(&(measurement as u128))?;
+ debug_assert_eq!(input_shares.len(), 2);
+ debug_assert_eq!(public_share, ());
+ let public_share = Vec::new(); // the encoding wants an empty vector not ()
+
+ let metadata = ReportMetadata {
+ report_id: ReportID::generate(),
+ time: Time::generate(time_precision),
+ extensions: vec![],
+ };
+
+ // This quote from the standard describes which info and aad to use for the encryption:
+ // enc, payload = SealBase(pk,
+ // "dap-02 input share" || 0x01 || server_role,
+ // task_id || metadata || public_share, input_share)
+ // https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-upload-request
+ let mut info = make_base_info();
+
+ let mut aad = Vec::from(*task_id);
+ metadata.encode(&mut aad);
+ encode_u32_items(&mut aad, &(), &public_share);
+
+ info.push(Role::Leader as u8);
+
+ let leader_payload = hpke_encrypt_wrapper(
+ &input_shares[0].get_encoded(),
+ &aad,
+ &info,
+ &leader_hpke_config,
+ )?;
+
+ *info.last_mut().unwrap() = Role::Helper as u8;
+
+ let helper_payload = hpke_encrypt_wrapper(
+ &input_shares[1].get_encoded(),
+ &aad,
+ &info,
+ &helper_hpke_config,
+ )?;
+
+ Ok(Report {
+ task_id: TaskID(*task_id),
+ metadata,
+ public_share,
+ encrypted_input_shares: vec![leader_payload, helper_payload],
+ })
+}
+
+/// Wraps the function above with minor C interop.
+/// Mostly it turns any error result into a return value of false.
+#[no_mangle]
+pub extern "C" fn dapGetReport(
+ leader_hpke_config_encoded: &ThinVec<u8>,
+ helper_hpke_config_encoded: &ThinVec<u8>,
+ measurement: u32,
+ task_id: &ThinVec<u8>,
+ time_precision: u64,
+ out_report: &mut ThinVec<u8>,
+) -> bool {
+ assert_eq!(task_id.len(), 32);
+
+ if let Ok(report) = get_dap_report_internal(
+ leader_hpke_config_encoded,
+ helper_hpke_config_encoded,
+ measurement,
+ &task_id.as_slice().try_into().unwrap(),
+ time_precision,
+ ) {
+ let encoded_report = report.get_encoded();
+ out_report.extend(encoded_report);
+
+ true
+ } else {
+ false
+ }
+}
diff --git a/toolkit/components/telemetry/dap/ffi/src/prg.rs b/toolkit/components/telemetry/dap/ffi/src/prg.rs
new file mode 100644
index 0000000000..a7ebeb11cb
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi/src/prg.rs
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::ffi::c_void;
+
+use prio::vdaf::prg::{Prg, SeedStream};
+
+extern "C" {
+ pub fn dapStartCmac(aSeed: *mut u8) -> *mut c_void;
+ pub fn dapUpdateCmac(aContext: *mut c_void, aData: *const u8, aDataLen: u32);
+ pub fn dapFinalizeCmac(aContext: *mut c_void, aMacOutput: *mut u8);
+ pub fn dapReleaseCmac(aContext: *mut c_void);
+
+ pub fn dapStartAesCtr(aKey: *const u8) -> *mut c_void;
+ pub fn dapCtrFillBuffer(aContext: *mut c_void, aBuffer: *mut u8, aBufferSize: i32);
+ pub fn dapReleaseCtrCtx(aContext: *mut c_void);
+}
+
+#[derive(Clone, Debug)]
+pub struct PrgAes128Alt {
+ nss_context: *mut c_void,
+}
+
+impl Prg<16> for PrgAes128Alt {
+ type SeedStream = SeedStreamAes128Alt;
+
+ fn init(seed_bytes: &[u8; 16]) -> Self {
+ let mut my_seed_bytes = *seed_bytes;
+ let ctx = unsafe { dapStartCmac(my_seed_bytes.as_mut_ptr()) };
+ assert!(!ctx.is_null());
+
+ Self { nss_context: ctx }
+ }
+
+ fn update(&mut self, data: &[u8]) {
+ unsafe {
+ dapUpdateCmac(
+ self.nss_context,
+ data.as_ptr(),
+ u32::try_from(data.len()).unwrap(),
+ );
+ }
+ }
+
+ fn into_seed_stream(self) -> Self::SeedStream {
+ // finish the MAC and create a new random data stream using the result as key and 0 as IV for AES-CTR
+ let mut key = [0u8; 16];
+ unsafe {
+ dapFinalizeCmac(self.nss_context, key.as_mut_ptr());
+ }
+
+ SeedStreamAes128Alt::new(&mut key, &[0; 16])
+ }
+}
+
+impl Drop for PrgAes128Alt {
+ fn drop(&mut self) {
+ unsafe {
+ dapReleaseCmac(self.nss_context);
+ }
+ }
+}
+
+pub struct SeedStreamAes128Alt {
+ nss_context: *mut c_void,
+}
+
+impl SeedStreamAes128Alt {
+ pub(crate) fn new(key: &mut [u8; 16], iv: &[u8; 16]) -> Self {
+ debug_assert_eq!(iv, &[0; 16]);
+ let ctx = unsafe { dapStartAesCtr(key.as_ptr()) };
+ Self { nss_context: ctx }
+ }
+}
+
+impl SeedStream for SeedStreamAes128Alt {
+ fn fill(&mut self, buf: &mut [u8]) {
+ unsafe {
+ dapCtrFillBuffer(
+ self.nss_context,
+ buf.as_mut_ptr(),
+ i32::try_from(buf.len()).unwrap(),
+ );
+ }
+ }
+}
+
+impl Drop for SeedStreamAes128Alt {
+ fn drop(&mut self) {
+ unsafe { dapReleaseCtrCtx(self.nss_context) };
+ }
+}
diff --git a/toolkit/components/telemetry/dap/ffi/src/types.rs b/toolkit/components/telemetry/dap/ffi/src/types.rs
new file mode 100644
index 0000000000..bfbf3264c0
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi/src/types.rs
@@ -0,0 +1,338 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//! This file contains structs for use in the DAP protocol and implements TLS compatible
+//! serialization/deserialization as required for the wire protocol.
+//!
+//! The current draft standard with the definition of these structs is available here:
+//! https://github.com/ietf-wg-ppm/draft-ietf-ppm-dap
+//! This code is based on version 02 of the standard available here:
+//! https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html
+
+use prio::codec::{
+ decode_u16_items, decode_u32_items, encode_u16_items, encode_u32_items, CodecError, Decode,
+ Encode,
+};
+use std::io::{Cursor, Read};
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use rand::Rng;
+
+/// opaque TaskId[32];
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-task-configuration
+#[derive(Debug, PartialEq, Eq)]
+pub struct TaskID(pub [u8; 32]);
+
+impl Decode for TaskID {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ // this should probably be available in codec...?
+ let mut data: [u8; 32] = [0; 32];
+ bytes.read_exact(&mut data)?;
+ Ok(TaskID(data))
+ }
+}
+
+impl Encode for TaskID {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ bytes.extend_from_slice(&self.0);
+ }
+}
+
+/// Time uint64;
+/// seconds elapsed since start of UNIX epoch
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-protocol-definition
+#[derive(Debug, PartialEq, Eq)]
+pub struct Time(pub u64);
+
+impl Decode for Time {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ Ok(Time(u64::decode(bytes)?))
+ }
+}
+
+impl Encode for Time {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ u64::encode(&self.0, bytes);
+ }
+}
+
+impl Time {
+ /// Generates a Time for the current system time rounded to the desired precision.
+ pub fn generate(time_precision: u64) -> Time {
+ let now_secs = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("Failed to get time.")
+ .as_secs();
+ let timestamp = (now_secs / time_precision) * time_precision;
+ Time(timestamp)
+ }
+}
+
+/// struct {
+/// ExtensionType extension_type;
+/// opaque extension_data<0..2^16-1>;
+/// } Extension;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-upload-extensions
+#[derive(Debug, PartialEq)]
+pub struct Extension {
+ extension_type: ExtensionType,
+ extension_data: Vec<u8>,
+}
+
+impl Decode for Extension {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ let extension_type = ExtensionType::from_u16(u16::decode(bytes)?);
+ let extension_data: Vec<u8> = decode_u16_items(&(), bytes)?;
+
+ Ok(Extension {
+ extension_type,
+ extension_data,
+ })
+ }
+}
+
+impl Encode for Extension {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ (self.extension_type as u16).encode(bytes);
+ encode_u16_items(bytes, &(), &self.extension_data);
+ }
+}
+
+/// enum {
+/// TBD(0),
+/// (65535)
+/// } ExtensionType;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-upload-extensions
+#[derive(Debug, PartialEq, Clone, Copy)]
+#[repr(u16)]
+enum ExtensionType {
+ Tbd = 0,
+}
+
+impl ExtensionType {
+ fn from_u16(value: u16) -> ExtensionType {
+ match value {
+ 0 => ExtensionType::Tbd,
+ _ => panic!("Unknown value for Extension Type: {}", value),
+ }
+ }
+}
+
+/// Identifier for a server's HPKE configuration
+/// uint8 HpkeConfigId;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-protocol-definition
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+pub struct HpkeConfigId(u8);
+
+impl Decode for HpkeConfigId {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ Ok(HpkeConfigId(u8::decode(bytes)?))
+ }
+}
+
+impl Encode for HpkeConfigId {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ self.0.encode(bytes);
+ }
+}
+
+/// struct {
+/// HpkeConfigId id;
+/// HpkeKemId kem_id;
+/// HpkeKdfId kdf_id;
+/// HpkeAeadId aead_id;
+/// HpkePublicKey public_key;
+/// } HpkeConfig;
+/// opaque HpkePublicKey<1..2^16-1>;
+/// uint16 HpkeAeadId; /* Defined in [HPKE] */
+/// uint16 HpkeKemId; /* Defined in [HPKE] */
+/// uint16 HpkeKdfId; /* Defined in [HPKE] */
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-hpke-configuration-request
+#[derive(Debug)]
+pub struct HpkeConfig {
+ pub id: HpkeConfigId,
+ pub kem_id: u16,
+ pub kdf_id: u16,
+ pub aead_id: u16,
+ pub public_key: Vec<u8>,
+}
+
+impl Decode for HpkeConfig {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ Ok(HpkeConfig {
+ id: HpkeConfigId::decode(bytes)?,
+ kem_id: u16::decode(bytes)?,
+ kdf_id: u16::decode(bytes)?,
+ aead_id: u16::decode(bytes)?,
+ public_key: decode_u16_items(&(), bytes)?,
+ })
+ }
+}
+
+impl Encode for HpkeConfig {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ self.id.encode(bytes);
+ self.kem_id.encode(bytes);
+ self.kdf_id.encode(bytes);
+ self.aead_id.encode(bytes);
+ encode_u16_items(bytes, &(), &self.public_key);
+ }
+}
+
+/// An HPKE ciphertext.
+/// struct {
+/// HpkeConfigId config_id; /* config ID */
+/// opaque enc<1..2^16-1>; /* encapsulated HPKE key */
+/// opaque payload<1..2^32-1>; /* ciphertext */
+/// } HpkeCiphertext;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-protocol-definition
+#[derive(Debug, PartialEq, Eq)]
+pub struct HpkeCiphertext {
+ pub config_id: HpkeConfigId,
+ pub enc: Vec<u8>,
+ pub payload: Vec<u8>,
+}
+
+impl Decode for HpkeCiphertext {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ let config_id = HpkeConfigId::decode(bytes)?;
+ let enc: Vec<u8> = decode_u16_items(&(), bytes)?;
+ let payload: Vec<u8> = decode_u32_items(&(), bytes)?;
+
+ Ok(HpkeCiphertext {
+ config_id,
+ enc,
+ payload,
+ })
+ }
+}
+
+impl Encode for HpkeCiphertext {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ self.config_id.encode(bytes);
+ encode_u16_items(bytes, &(), &self.enc);
+ encode_u32_items(bytes, &(), &self.payload);
+ }
+}
+
+/// uint8 ReportID[16];
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-protocol-definition
+#[derive(Debug, PartialEq, Eq)]
+pub struct ReportID(pub [u8; 16]);
+
+impl Decode for ReportID {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ let mut data: [u8; 16] = [0; 16];
+ bytes.read_exact(&mut data)?;
+ Ok(ReportID(data))
+ }
+}
+
+impl Encode for ReportID {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ bytes.extend_from_slice(&self.0);
+ }
+}
+
+impl ReportID {
+ pub fn generate() -> ReportID {
+ ReportID(rand::thread_rng().gen())
+ }
+}
+
+/// struct {
+/// ReportID report_id;
+/// Time time;
+/// Extension extensions<0..2^16-1>;
+/// } ReportMetadata;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-upload-request
+#[derive(Debug, PartialEq)]
+pub struct ReportMetadata {
+ pub report_id: ReportID,
+ pub time: Time,
+ pub extensions: Vec<Extension>,
+}
+
+impl Decode for ReportMetadata {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ let report_id = ReportID::decode(bytes)?;
+ let time = Time::decode(bytes)?;
+ let extensions = decode_u16_items(&(), bytes)?;
+
+ Ok(ReportMetadata {
+ report_id,
+ time,
+ extensions,
+ })
+ }
+}
+
+impl Encode for ReportMetadata {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ self.report_id.encode(bytes);
+ self.time.encode(bytes);
+ encode_u16_items(bytes, &(), &self.extensions);
+ }
+}
+
+/// struct {
+/// TaskID task_id;
+/// ReportMetadata metadata;
+/// opaque public_share<0..2^32-1>;
+/// HpkeCiphertext encrypted_input_shares<1..2^32-1>;
+/// } Report;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-upload-request
+#[derive(Debug, PartialEq)]
+pub struct Report {
+ pub task_id: TaskID,
+ pub metadata: ReportMetadata,
+ pub public_share: Vec<u8>,
+ pub encrypted_input_shares: Vec<HpkeCiphertext>,
+}
+
+impl Report {
+ /// Creates a minimal report for use in tests.
+ pub fn new_dummy() -> Self {
+ Report {
+ task_id: TaskID([0x12; 32]),
+ metadata: ReportMetadata {
+ report_id: ReportID::generate(),
+ time: Time::generate(1),
+ extensions: vec![],
+ },
+ public_share: vec![],
+ encrypted_input_shares: vec![],
+ }
+ }
+}
+
+impl Decode for Report {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ let task_id = TaskID::decode(bytes)?;
+ let metadata = ReportMetadata::decode(bytes)?;
+ let public_share: Vec<u8> = decode_u32_items(&(), bytes)?;
+ let encrypted_input_shares: Vec<HpkeCiphertext> = decode_u32_items(&(), bytes)?;
+
+ let remaining_bytes = bytes.get_ref().len() - (bytes.position() as usize);
+ if remaining_bytes == 0 {
+ Ok(Report {
+ task_id,
+ metadata,
+ public_share,
+ encrypted_input_shares,
+ })
+ } else {
+ Err(CodecError::BytesLeftOver(remaining_bytes))
+ }
+ }
+}
+
+impl Encode for Report {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ self.task_id.encode(bytes);
+ self.metadata.encode(bytes);
+ encode_u32_items(bytes, &(), &self.public_share);
+ encode_u32_items(bytes, &(), &self.encrypted_input_shares);
+ }
+}
diff --git a/toolkit/components/telemetry/dap/metrics.yaml b/toolkit/components/telemetry/dap/metrics.yaml
new file mode 100644
index 0000000000..572250ec7c
--- /dev/null
+++ b/toolkit/components/telemetry/dap/metrics.yaml
@@ -0,0 +1,46 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Toolkit :: Telemetry'
+
+dap:
+ upload_status:
+ type: labeled_counter
+ labels:
+ - success
+ - failure
+ description: >
+ The result of trying to upload a report to the DAP server.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1775035
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1775035
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - simon@mozilla.com
+ expires: never
+
+ report_generation_status:
+ type: labeled_counter
+ labels:
+ - success
+ - failure
+ description: >
+ The result of trying to generate a DAP report.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1775035
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1775035
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - simon@mozilla.com
+ expires: never
diff --git a/toolkit/components/telemetry/dap/nsIDAPTelemetry.idl b/toolkit/components/telemetry/dap/nsIDAPTelemetry.idl
new file mode 100644
index 0000000000..e9d33bdd26
--- /dev/null
+++ b/toolkit/components/telemetry/dap/nsIDAPTelemetry.idl
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, builtinclass, uuid(58a4c579-d2dd-46b7-9c3b-6881a1c36c6a)]
+interface nsIDAPTelemetry : nsISupports {
+ /**
+ * Split measurement into shares and create a report with encrypted shares.
+ *
+ * @param leaderHpkeConfig The leader share will be encrypted with this
+ * config.
+ * @param helperHpkeConfig Same for the helper.
+ * @param measurement The data which will be encoded and shared.
+ * @param task_id Identifies which task this measurement is for
+ * which influences both encoding and encryption.
+ * @param time_precision Determines the report timestamp.
+ *
+ * @return The raw bytes of a report, ready for sending.
+ *
+ * @note This can potentially run for a long time. Take care not to block
+ * the main thread for too long.
+ */
+ void GetReport(in Array<uint8_t> leaderHpkeConfig,
+ in Array<uint8_t> helperHpkeConfig, in uint8_t measurement,
+ in Array<uint8_t> task_id, in uint64_t time_precision,
+ out Array<uint8_t> report);
+};
diff --git a/toolkit/components/telemetry/dap/tests/xpcshell/test_dap.js b/toolkit/components/telemetry/dap/tests/xpcshell/test_dap.js
new file mode 100644
index 0000000000..9c1cae0b1e
--- /dev/null
+++ b/toolkit/components/telemetry/dap/tests/xpcshell/test_dap.js
@@ -0,0 +1,195 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ DAPTelemetrySender: "resource://gre/modules/DAPTelemetrySender.sys.mjs",
+});
+
+const BinaryOutputStream = Components.Constructor(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"
+);
+
+const BinaryInputStream = Components.Constructor(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+const PREF_LEADER = "toolkit.telemetry.dap_leader";
+const PREF_HELPER = "toolkit.telemetry.dap_helper";
+
+let received = false;
+let server;
+let server_addr;
+
+function hpkeConfigHandler(request, response) {
+ if (
+ request.queryString == "task_id=StldO2czL_iaUF2iljFbiLiNTxxVNdPHgPuuEWLHnsk"
+ ) {
+ let config_bytes;
+ if (request.path.startsWith("/leader")) {
+ config_bytes = new Uint8Array([
+ 47,
+ 0,
+ 32,
+ 0,
+ 1,
+ 0,
+ 1,
+ 0,
+ 32,
+ 11,
+ 33,
+ 206,
+ 33,
+ 131,
+ 56,
+ 220,
+ 82,
+ 153,
+ 110,
+ 228,
+ 200,
+ 53,
+ 98,
+ 210,
+ 38,
+ 177,
+ 197,
+ 252,
+ 198,
+ 36,
+ 201,
+ 86,
+ 121,
+ 169,
+ 238,
+ 220,
+ 34,
+ 143,
+ 112,
+ 177,
+ 10,
+ ]);
+ } else {
+ config_bytes = new Uint8Array([
+ 42,
+ 0,
+ 32,
+ 0,
+ 1,
+ 0,
+ 1,
+ 0,
+ 32,
+ 28,
+ 62,
+ 242,
+ 195,
+ 117,
+ 7,
+ 173,
+ 149,
+ 250,
+ 15,
+ 139,
+ 178,
+ 86,
+ 241,
+ 117,
+ 143,
+ 75,
+ 26,
+ 57,
+ 60,
+ 88,
+ 130,
+ 199,
+ 175,
+ 195,
+ 9,
+ 241,
+ 130,
+ 61,
+ 47,
+ 215,
+ 101,
+ ]);
+ }
+ response.setHeader("Content-Type", "application/dap-hpke-config");
+ let bos = new BinaryOutputStream(response.bodyOutputStream);
+ bos.writeByteArray(config_bytes);
+ } else {
+ Assert.ok(false, "Unknown query string.");
+ }
+}
+
+function uploadHandler(request, response) {
+ Assert.equal(
+ request.getHeader("Content-Type"),
+ "application/dap-report",
+ "Wrong Content-Type header."
+ );
+ let body = new BinaryInputStream(request.bodyInputStream);
+ Assert.equal(body.available(), 432, "Wrong request body size.");
+ received = true;
+ response.setStatusLine(request.httpVersion, 200);
+}
+
+add_setup(async function() {
+ do_get_profile();
+ Services.fog.initializeFOG();
+
+ // Set up a mock server to represent the DAP endpoints.
+ server = new HttpServer();
+ server.registerPathHandler("/leader_endpoint/hpke_config", hpkeConfigHandler);
+ server.registerPathHandler("/helper_endpoint/hpke_config", hpkeConfigHandler);
+ server.registerPathHandler("/leader_endpoint/upload", uploadHandler);
+ server.start(-1);
+
+ const orig_leader = Services.prefs.getStringPref(PREF_LEADER);
+ const orig_helper = Services.prefs.getStringPref(PREF_HELPER);
+ const i = server.identity;
+ server_addr = i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort;
+ Services.prefs.setStringPref(PREF_LEADER, server_addr + "/leader_endpoint");
+ Services.prefs.setStringPref(PREF_HELPER, server_addr + "/helper_endpoint");
+ registerCleanupFunction(() => {
+ Services.prefs.setStringPref(PREF_LEADER, orig_leader);
+ Services.prefs.setStringPref(PREF_HELPER, orig_helper);
+
+ return new Promise(resolve => {
+ server.stop(resolve);
+ });
+ });
+});
+
+add_task(async function testVerificationTask() {
+ Services.fog.testResetFOG();
+ let before = Glean.dap.uploadStatus.success.testGetValue() ?? 0;
+ await lazy.DAPTelemetrySender.sendVerificationTaskReport();
+ let after = Glean.dap.uploadStatus.success.testGetValue();
+ Assert.equal(before + 1, after, "Successful submissions should be counted.");
+ Assert.ok(received, "Report upload successful.");
+});
+
+add_task(async function testNetworkError() {
+ Services.fog.testResetFOG();
+ let before = Glean.dap.reportGenerationStatus.failure.testGetValue() ?? 0;
+ Services.prefs.setStringPref(PREF_LEADER, server_addr + "/invalid-endpoint");
+ await lazy.DAPTelemetrySender.sendVerificationTaskReport();
+ let after = Glean.dap.reportGenerationStatus.failure.testGetValue() ?? 0;
+ Assert.equal(
+ before + 1,
+ after,
+ "Failed report generation should be counted."
+ );
+});
diff --git a/toolkit/components/telemetry/dap/tests/xpcshell/xpcshell.ini b/toolkit/components/telemetry/dap/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..6698b43873
--- /dev/null
+++ b/toolkit/components/telemetry/dap/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,2 @@
+[test_dap.js]
+skip-if = os == "android" # DAP is not supported on Android