summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/dap/ffi
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/telemetry/dap/ffi
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/telemetry/dap/ffi')
-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.rs335
-rw-r--r--toolkit/components/telemetry/dap/ffi/src/types.rs358
4 files changed, 717 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/dap/ffi/Cargo.toml b/toolkit/components/telemetry/dap/ffi/Cargo.toml
new file mode 100644
index 0000000000..dfb69e4821
--- /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.15.3", 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..e2c032133a
--- /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"
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..998c8af204
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi/src/lib.rs
@@ -0,0 +1,335 @@
+/* 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::error::Error;
+use std::io::Cursor;
+
+use prio::vdaf::prio3::Prio3Sum;
+use prio::vdaf::prio3::Prio3SumVec;
+use thin_vec::ThinVec;
+
+pub mod types;
+use types::HpkeConfig;
+use types::PlaintextInputShare;
+use types::Report;
+use types::ReportID;
+use types::ReportMetadata;
+use types::Time;
+
+use prio::codec::Encode;
+use prio::codec::{decode_u16_items, encode_u32_items};
+use prio::flp::types::{Sum, SumVec};
+use prio::vdaf::prio3::Prio3;
+use prio::vdaf::Client;
+use prio::vdaf::VdafError;
+
+use crate::types::HpkeCiphertext;
+
+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<Prio3Sum, 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_vecu8(num_aggregators: u8, len: usize) -> Result<Prio3SumVec, VdafError> {
+ let chunk_length = prio::vdaf::prio3::optimal_chunk_length(8 * len);
+ Prio3::new(num_aggregators, SumVec::new(8, len, chunk_length)?)
+}
+
+pub fn new_prio_vecu16(num_aggregators: u8, len: usize) -> Result<Prio3SumVec, VdafError> {
+ let chunk_length = prio::vdaf::prio3::optimal_chunk_length(16 * len);
+ Prio3::new(num_aggregators, SumVec::new(16, len, chunk_length)?)
+}
+
+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,
+ nonce: &[u8; 16],
+ ) -> Result<(Vec<u8>, Vec<Vec<u8>>), Box<dyn std::error::Error>>;
+}
+
+impl Shardable for u8 {
+ fn shard(
+ &self,
+ nonce: &[u8; 16],
+ ) -> Result<(Vec<u8>, 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), nonce)?;
+
+ debug_assert_eq!(input_shares.len(), 2);
+
+ let encoded_input_shares = input_shares.iter().map(|s| s.get_encoded()).collect();
+ let encoded_public_share = public_share.get_encoded();
+ Ok((encoded_public_share, encoded_input_shares))
+ }
+}
+
+impl Shardable for ThinVec<u8> {
+ fn shard(
+ &self,
+ nonce: &[u8; 16],
+ ) -> Result<(Vec<u8>, Vec<Vec<u8>>), Box<dyn std::error::Error>> {
+ let prio = new_prio_vecu8(2, self.len())?;
+
+ let measurement: Vec<u128> = self.iter().map(|e| (*e as u128)).collect();
+ let (public_share, input_shares) = prio.shard(&measurement, nonce)?;
+
+ debug_assert_eq!(input_shares.len(), 2);
+
+ let encoded_input_shares = input_shares.iter().map(|s| s.get_encoded()).collect();
+ let encoded_public_share = public_share.get_encoded();
+ Ok((encoded_public_share, encoded_input_shares))
+ }
+}
+
+impl Shardable for ThinVec<u16> {
+ fn shard(
+ &self,
+ nonce: &[u8; 16],
+ ) -> Result<(Vec<u8>, 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, nonce)?;
+
+ debug_assert_eq!(input_shares.len(), 2);
+
+ let encoded_input_shares = input_shares.iter().map(|s| s.get_encoded()).collect();
+ let encoded_public_share = public_share.get_encoded();
+ Ok((encoded_public_share, 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-07 input share".as_bytes();
+ info.extend(START);
+ const FIXED: u8 = 1;
+ info.push(FIXED);
+
+ info
+}
+
+fn select_hpke_config(configs: Vec<HpkeConfig>) -> Result<HpkeConfig, Box<dyn Error>> {
+ for config in configs {
+ if config.kem_id == 0x20 /* DHKEM(X25519, HKDF-SHA256) */ &&
+ config.kdf_id == 0x01 /* HKDF-SHA256 */ &&
+ config.aead_id == 0x01
+ /* AES-128-GCM */
+ {
+ return Ok(config);
+ }
+ }
+
+ Err("No suitable HPKE config found.".into())
+}
+
+/// 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_configs: Vec<HpkeConfig> =
+ decode_u16_items(&(), &mut Cursor::new(leader_hpke_config_encoded))?;
+ let leader_hpke_config = select_hpke_config(leader_hpke_configs)?;
+ let helper_hpke_configs: Vec<HpkeConfig> =
+ decode_u16_items(&(), &mut Cursor::new(helper_hpke_config_encoded))?;
+ let helper_hpke_config = select_hpke_config(helper_hpke_configs)?;
+
+ let report_id = ReportID::generate();
+ let (encoded_public_share, encoded_input_shares) = measurement.shard(report_id.as_ref())?;
+
+ let plaintext_input_shares: Vec<Vec<u8>> = encoded_input_shares
+ .into_iter()
+ .map(|encoded_input_share| {
+ PlaintextInputShare {
+ extensions: Vec::new(),
+ payload: encoded_input_share,
+ }
+ .get_encoded()
+ })
+ .collect();
+
+ let metadata = ReportMetadata {
+ report_id,
+ time: Time::generate(time_precision),
+ };
+
+ // 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, &(), &encoded_public_share);
+
+ info.push(Role::Leader as u8);
+
+ let leader_payload =
+ hpke_encrypt_wrapper(&plaintext_input_shares[0], &aad, &info, &leader_hpke_config)?;
+
+ *info.last_mut().unwrap() = Role::Helper as u8;
+
+ let helper_payload =
+ hpke_encrypt_wrapper(&plaintext_input_shares[1], &aad, &info, &helper_hpke_config)?;
+
+ Ok(Report {
+ metadata,
+ public_share: encoded_public_share,
+ leader_encrypted_input_share: leader_payload,
+ helper_encrypted_input_share: 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 dapGetReportVecU8(
+ leader_hpke_config_encoded: &ThinVec<u8>,
+ helper_hpke_config_encoded: &ThinVec<u8>,
+ measurement: &ThinVec<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::<ThinVec<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/types.rs b/toolkit/components/telemetry/dap/ffi/src/types.rs
new file mode 100644
index 0000000000..e8f6385dcd
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi/src/types.rs
@@ -0,0 +1,358 @@
+/* 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 07 of the standard available here:
+//! https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.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, RngCore};
+
+/// opaque TaskId[32];
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.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);
+ }
+}
+
+/// uint64 Time;
+/// seconds elapsed since start of UNIX epoch
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.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-07.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-07.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),
+ }
+ }
+}
+
+/// struct {
+/// Extension extensions<0..2^16-1>;
+/// opaque payload<0..2^32-1>;
+/// } PlaintextInputShare;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#section-4.4.2-9
+#[derive(Debug)]
+pub struct PlaintextInputShare {
+ pub extensions: Vec<Extension>,
+ pub payload: Vec<u8>,
+}
+
+impl Encode for PlaintextInputShare {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ encode_u16_items(bytes, &(), &self.extensions);
+ encode_u32_items(bytes, &(), &self.payload);
+ }
+}
+
+/// Identifier for a server's HPKE configuration
+/// uint8 HpkeConfigId;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.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-07.html#name-hpke-configuration-request
+#[derive(Debug, Clone)]
+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-07.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);
+ }
+}
+
+/// opaque ReportID[16];
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.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())
+ }
+}
+
+impl AsRef<[u8; 16]> for ReportID {
+ fn as_ref(&self) -> &[u8; 16] {
+ &self.0
+ }
+}
+
+/// struct {
+/// ReportID report_id;
+/// Time time;
+/// } ReportMetadata;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#name-upload-request
+#[derive(Debug, PartialEq)]
+pub struct ReportMetadata {
+ pub report_id: ReportID,
+ pub time: Time,
+}
+
+impl Decode for ReportMetadata {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ let report_id = ReportID::decode(bytes)?;
+ let time = Time::decode(bytes)?;
+
+ Ok(ReportMetadata { report_id, time })
+ }
+}
+
+impl Encode for ReportMetadata {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ self.report_id.encode(bytes);
+ self.time.encode(bytes);
+ }
+}
+
+/// struct {
+/// ReportMetadata metadata;
+/// opaque public_share<0..2^32-1>;
+/// HpkeCiphertext leader_encrypted_input_share;
+/// HpkeCiphertext helper_encrypted_input_share;
+/// } Report;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#name-upload-request
+#[derive(Debug, PartialEq)]
+pub struct Report {
+ pub metadata: ReportMetadata,
+ pub public_share: Vec<u8>,
+ pub leader_encrypted_input_share: HpkeCiphertext,
+ pub helper_encrypted_input_share: HpkeCiphertext,
+}
+
+
+impl Report {
+ /// Creates a minimal report for use in tests.
+ pub fn new_dummy() -> Self {
+ let mut enc = [0u8; 32];
+ rand::thread_rng().fill_bytes(&mut enc);
+ let mut payload = [0u8; 200];
+ rand::thread_rng().fill_bytes(&mut payload);
+ Report {
+ metadata: ReportMetadata {
+ report_id: ReportID::generate(),
+ time: Time::generate(1),
+ },
+ public_share: vec![],
+ leader_encrypted_input_share: HpkeCiphertext { config_id: HpkeConfigId(5), enc: vec![1, 2, 3, 4, 5], payload: vec![6, 7, 8, 9, 10] },
+ helper_encrypted_input_share: HpkeCiphertext { config_id: HpkeConfigId(100), enc: enc.into(), payload: payload.into() },
+ }
+ }
+}
+
+impl Decode for Report {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ let metadata = ReportMetadata::decode(bytes)?;
+ let public_share: Vec<u8> = decode_u32_items(&(), bytes)?;
+ let leader_encrypted_input_share: HpkeCiphertext = HpkeCiphertext::decode(bytes)?;
+ let helper_encrypted_input_share: HpkeCiphertext = HpkeCiphertext::decode(bytes)?;
+
+ let remaining_bytes = bytes.get_ref().len() - (bytes.position() as usize);
+ if remaining_bytes == 0 {
+ Ok(Report {
+ metadata,
+ public_share,
+ leader_encrypted_input_share,
+ helper_encrypted_input_share,
+ })
+ } else {
+ Err(CodecError::BytesLeftOver(remaining_bytes))
+ }
+ }
+}
+
+impl Encode for Report {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ self.metadata.encode(bytes);
+ encode_u32_items(bytes, &(), &self.public_share);
+ self.leader_encrypted_input_share.encode(bytes);
+ self.helper_encrypted_input_share.encode(bytes);
+ }
+}