diff options
Diffstat (limited to 'toolkit/components/telemetry/dap')
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 |