summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/dap
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/telemetry/dap
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
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.cpp309
-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.mjs258
-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.json80
-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.rs255
-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.idl33
-rw-r--r--toolkit/components/telemetry/dap/tests/xpcshell/test_dap.js126
-rw-r--r--toolkit/components/telemetry/dap/tests/xpcshell/xpcshell.ini2
19 files changed, 1890 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..c20dc50557
--- /dev/null
+++ b/toolkit/components/telemetry/dap/DAPTelemetry.cpp
@@ -0,0 +1,309 @@
+/* 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* chCt = nullptr;
+ SECStatus rv = PK11_HPKE_Seal(aContext, &aad_si, &plaintext_si, &chCt);
+ if (rv != SECSuccess) {
+ return false;
+ }
+ UniqueSECItem ct(chCt);
+
+ 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::GetReportU8(
+ 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 (!dapGetReportU8(&aLeaderHpkeConfig, &aHelperHpkeConfig, aMeasurement,
+ &aTaskID, aTimePrecision, &aOutReport)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP DAPTelemetry::GetReportVecU16(
+ const nsTArray<uint8_t>& aLeaderHpkeConfig,
+ const nsTArray<uint8_t>& aHelperHpkeConfig,
+ const nsTArray<uint16_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 (!dapGetReportVecU16(&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..3a9de97e4d
--- /dev/null
+++ b/toolkit/components/telemetry/dap/DAPTelemetrySender.sys.mjs
@@ -0,0 +1,258 @@
+/* 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",
+ });
+});
+ChromeUtils.defineESModuleGetters(lazy, {
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+});
+
+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);
+ }
+ }
+
+ async sendTestReports() {
+ /** @typedef { 'u8' | 'vecu16'} measurementtype */
+
+ /**
+ * @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.
+ * @property {measurementtype} measurement_type - Defines measurements and aggregations used by this task. Effectively specifying the VDAF.
+ */
+
+ // For now tasks are hardcoded here.
+ const tasks = [
+ {
+ // this is load testing task 1
+ id_hexstring:
+ "423303e27f25fcc1c1a0badb09f2d3162f210b6eb87c2e7d48a1cfbe23c5d2af",
+ id_base64: "QjMD4n8l_MHBoLrbCfLTFi8hC264fC59SKHPviPF0q8",
+ leader_endpoint: null,
+ helper_endpoint: null,
+ time_precision: 60, // TODO what is a reasonable value
+ measurement_type: "u8",
+ },
+ {
+ // this is load testing task 2
+ id_hexstring:
+ "0d2646305876ea10585cd68abe12ff3780070373f99439f5f689f5bc53c1c493",
+ id_base64: "DSZGMFh26hBYXNaKvhL_N4AHA3P5lDn19on1vFPBxJM",
+ leader_endpoint: null,
+ helper_endpoint: null,
+ time_precision: 60,
+ measurement_type: "vecu16",
+ },
+ ];
+
+ for (let task of tasks) {
+ let measurement;
+ if (task.measurement_type == "u8") {
+ measurement = 3;
+ } else if (task.measurement_type == "vecu16") {
+ measurement = new Uint16Array(1024);
+ let r = Math.floor(Math.random() * 10);
+ measurement[r] += 1;
+ measurement[1000] += 1;
+ }
+
+ await this.sendTestReport(task, measurement);
+ }
+ }
+
+ /**
+ * Creates a DAP report for a specific task from a measurement and sends it.
+ *
+ * @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.
+ */
+ async sendTestReport(task, measurement) {
+ 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 = {};
+ if (task.measurement_type == "u8") {
+ Services.DAPTelemetry.GetReportU8(
+ leader_config_bytes,
+ helper_config_bytes,
+ measurement,
+ task_id,
+ task.time_precision,
+ report
+ );
+ } else if (task.measurement_type == "vecu16") {
+ Services.DAPTelemetry.GetReportVecU16(
+ leader_config_bytes,
+ helper_config_bytes,
+ measurement,
+ task_id,
+ task.time_precision,
+ report
+ );
+ } else {
+ throw new Error(
+ `Unknown measurement type for task ${task.id_base64}: ${task.measurement_type}`
+ );
+ }
+ 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) {
+ const content_type = response.headers.get("content-type");
+ if (content_type && content_type === "application/json") {
+ // A JSON error from the DAP server.
+ let error = await response.json();
+ lazy.logConsole.error(
+ `Sending failed. HTTP response: ${response.status} ${response.statusText}. Error: ${error.type} ${error.title}`
+ );
+ } else {
+ // A different error, e.g. from a load-balancer.
+ let error = await response.text();
+ lazy.logConsole.error(
+ `Sending failed. HTTP response: ${response.status} ${response.statusText}. Error: ${error}`
+ );
+ }
+
+ 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..7227b589c1
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi-gtest/PrgAes128_tests.json
@@ -0,0 +1,80 @@
+[
+ {
+ "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]
+ }
+]
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..6e5601d743
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi/src/lib.rs
@@ -0,0 +1,255 @@
+/* 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::gadgets::{BlindPolyEval, ParallelSum};
+use prio::flp::types::{CountVec, 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>;
+type Prio3Aes128CountVecAlt =
+ Prio3<CountVec<Field128, ParallelSum<Field128, BlindPolyEval<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_u8(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)?)
+}
+
+pub fn new_prio_vecu16(
+ num_aggregators: u8,
+ len: usize,
+) -> Result<Prio3Aes128CountVecAlt, VdafError> {
+ Prio3::new(num_aggregators, CountVec::new(len))
+}
+
+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(),
+ })
+}
+
+trait Shardable {
+ fn shard(&self) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error>>;
+}
+
+impl Shardable for ThinVec<u16> {
+ fn shard(&self) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error>> {
+ let prio = new_prio_vecu16(2, self.len())?;
+
+ let measurement: Vec<u128> = self.iter().map(|e| (*e as u128)).collect();
+ let (public_share, input_shares) = prio.shard(&measurement)?;
+
+ debug_assert_eq!(input_shares.len(), 2);
+ debug_assert_eq!(public_share, ());
+
+ let encoded_input_shares = input_shares.iter().map(|s| s.get_encoded()).collect();
+ Ok(encoded_input_shares)
+ }
+}
+impl Shardable for u8 {
+ fn shard(&self) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error>> {
+ let prio = new_prio_u8(2, 2)?;
+
+ let (public_share, input_shares) = prio.shard(&(*self as u128))?;
+
+ debug_assert_eq!(input_shares.len(), 2);
+ debug_assert_eq!(public_share, ());
+
+ let encoded_input_shares = input_shares.iter().map(|s| s.get_encoded()).collect();
+ Ok(encoded_input_shares)
+ }
+}
+
+/// 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<T: Shardable>(
+ leader_hpke_config_encoded: &ThinVec<u8>,
+ helper_hpke_config_encoded: &ThinVec<u8>,
+ measurement: &T,
+ 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 encoded_input_shares = measurement.shard()?;
+ 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(&encoded_input_shares[0], &aad, &info, &leader_hpke_config)?;
+
+ *info.last_mut().unwrap() = Role::Helper as u8;
+
+ let helper_payload =
+ hpke_encrypt_wrapper(&encoded_input_shares[1], &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 dapGetReportU8(
+ leader_hpke_config_encoded: &ThinVec<u8>,
+ helper_hpke_config_encoded: &ThinVec<u8>,
+ measurement: u8,
+ 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::<u8>(
+ 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
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn dapGetReportVecU16(
+ leader_hpke_config_encoded: &ThinVec<u8>,
+ helper_hpke_config_encoded: &ThinVec<u8>,
+ measurement: &ThinVec<u16>,
+ 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::<ThinVec<u16>>(
+ 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..80e54dbf12
--- /dev/null
+++ b/toolkit/components/telemetry/dap/nsIDAPTelemetry.idl
@@ -0,0 +1,33 @@
+/* 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 GetReportU8(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);
+ void GetReportVecU16(in Array<uint8_t> leaderHpkeConfig,
+ in Array<uint8_t> helperHpkeConfig,
+ in Array<uint16_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..2f4712e54b
--- /dev/null
+++ b/toolkit/components/telemetry/dap/tests/xpcshell/test_dap.js
@@ -0,0 +1,126 @@
+/* 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=QjMD4n8l_MHBoLrbCfLTFi8hC264fC59SKHPviPF0q8" ||
+ request.queryString == "task_id=DSZGMFh26hBYXNaKvhL_N4AHA3P5lDn19on1vFPBxJM"
+ ) {
+ 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(
+ true,
+ body.available() == 432 || body.available() == 20720,
+ "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.sendTestReports();
+ let after = Glean.dap.uploadStatus.success.testGetValue();
+ Assert.equal(before + 2, 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.sendTestReports();
+ let after = Glean.dap.reportGenerationStatus.failure.testGetValue() ?? 0;
+ Assert.equal(
+ before + 2,
+ 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