Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for public extensions in Reports. #754

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jhoyla
Copy link
Contributor

@jhoyla jhoyla commented Dec 31, 2024

Draft 13 adds mandatory support for public extensions in the ReportMetadata, but doesn't define any (Taskprov is (for now) a private extension). This PR adds support for rejecting unknown extensions, which is all of them.

Copy link
Contributor

@cjpatton cjpatton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we're missing one feature: we need to resolve repeated extensions across the public and private extension fields.

crates/daphne/src/error/aborts.rs Outdated Show resolved Hide resolved
crates/daphne/src/error/aborts.rs Outdated Show resolved Hide resolved
crates/daphne/src/messages/mod.rs Outdated Show resolved Hide resolved
crates/daphne/src/protocol/report_init.rs Outdated Show resolved Hide resolved
crates/daphne/src/protocol/report_init.rs Outdated Show resolved Hide resolved
@jhoyla
Copy link
Contributor Author

jhoyla commented Jan 2, 2025

I believe we're missing one feature: we need to resolve repeated extensions across the public and private extension fields.

So the spec says:

type. (For the purposes of this check, a private report extension can conflict
with a public report extension.) If so, the Aggregator MUST mark the input share
as invalid with error invalid_message.

Which I take to mean a conflict between the public and private extensions is allowed.
If I add support for taskprov as a public extension do I reject it if it appears in both places?

Copy link
Collaborator

@mendess mendess left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not even a nit, just a comment. I don't have a preference but instead of Vec::new() you can write vec![]

@cjpatton
Copy link
Contributor

cjpatton commented Jan 2, 2025

I believe we're missing one feature: we need to resolve repeated extensions across the public and private extension fields.

So the spec says:

type. (For the purposes of this check, a private report extension can conflict
with a public report extension.) If so, the Aggregator MUST mark the input share
as invalid with error invalid_message.

Which I take to mean a conflict between the public and private extensions is allowed. If I add support for taskprov as a public extension do I reject it if it appears in both places?

The intent of the text is: "if the same extension appears twice, then the report MUST be rejected - regardless of what fields the extensions appear in." In particular, if the taskprov extension appears in the public and private fields, then reject.

@jhoyla jhoyla force-pushed the jhoyla/public-extensions branch 4 times, most recently from eda9b3c to 5078b99 Compare January 8, 2025 12:41
@jhoyla jhoyla requested review from mendess and cjpatton January 8, 2025 12:49
Copy link
Contributor

@cjpatton cjpatton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks largely correct, though I have some suggestions for behavior changes. I think the tests need a bit of work. Also, I have some suggestions for making the code a little cleaner/easier to read.

@@ -533,6 +537,150 @@ async fn leader_upload_taskprov_wrong_version(version: DapVersion) {

async_test_versions!(leader_upload_taskprov_wrong_version);

#[tokio::test]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the test coverage in the daphne::roles module is high enough that we could just as easily move theses tests there. You can call leader::handle_upload_req() on the mock aggregator, just as you already do in a new test in that module.

Any code path that can be tested without the e2e setup ought to be moved to a unit test, since unit tests are easier to run.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move these tests to the daphne crate?

task_id: &TaskId,
unknown_extensions: &[u16],
) -> Result<Self, DapError> {
let detail = serde_json::to_string(&unknown_extensions);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary reference

Suggested change
let detail = serde_json::to_string(&unknown_extensions);
let detail = serde_json::to_string(unknown_extensions);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use json here? We don't expect anyone to parse the detail, you could just use

Suggested change
let detail = serde_json::to_string(&unknown_extensions);
let detail = format!("{unknown_extensions:?}");

which I think will output exactly the same thing in this case

detail: s,
task_id: *task_id,
}),
Err(x) => Err(fatal_error!(err = %x,)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Err(x) => Err(fatal_error!(err = %x,)),
Err(x) => Err(fatal_error!(err = %x)),

task_id: &TaskId,
unknown_extensions: &[u16],
) -> Result<Self, DapError> {
let detail = serde_json::to_string(&unknown_extensions);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More Rusty

Suggested change
let detail = serde_json::to_string(&unknown_extensions);
let detail = serde_json::to_string(&unknown_extensions).map_err(|e| fatal_error!(err = e))?;

Comment on lines 79 to 82
public_extensions: match version {
DapVersion::Draft09 => None,
DapVersion::Latest => Some(Vec::new()),
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the extensions are replicated between the Leader and Helper, perhaps it makes more sense to include them in the public extensions field rather than in the private extension fields.

If you take this suggestion, let's also rename extensions to public_extensions.

}
);
}
test_versions! {handle_unknown_public_extensions_in_report}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This creates the test for all versions of DAP, including draft 09.

) {
(Some(extensions), DapVersion::Latest) => {
let mut unknown_extensions = Vec::<u16>::new();
if crate::protocol::no_duplicates(extensions.iter()).is_err() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And use crate::protocol;. Also, we should rename no_duplicates to something more distinctive.

Suggested change
if crate::protocol::no_duplicates(extensions.iter()).is_err() {
if protocol::no_duplicates(extensions.iter()).is_err() {

crates/daphne/src/roles/leader/mod.rs Show resolved Hide resolved
Comment on lines 253 to 252
(Some(_), DapVersion::Draft09) => {
return Err(DapError::Abort(DapAbort::version_mismatch(
DapVersion::Draft09,
DapVersion::Latest,
)))
}
(None, DapVersion::Latest) => {
return Err(DapError::Abort(DapAbort::version_mismatch(
DapVersion::Latest,
DapVersion::Draft09,
)))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure either of these arms could ever be reached, assuming the code that sets public_extensions is correct. If I were you, I would use the public_extension.is_some() to deduce which version was negotiated. Tnen we could replace this big ugly match with:

if let Some(public_extensions) = &report.report_metadata.public_extensions {
    ...  // check for duplicates, unorecgonized values, and so on
}

@@ -1348,6 +1496,116 @@ async fn leader_selected() {
.unwrap();
}

#[tokio::test]
async fn leader_collect_taskprov_repeated_abort() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this new test do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It checks that the Leader aborts if the taskprov extension appears in both the public and private extensions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed a bug in it too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this to the unit tests in the daphne crate. or does exercise a behavior that's not implemented by InMemoryAggregator?

@jhoyla jhoyla requested review from cjpatton and mendess January 14, 2025 14:33
@jhoyla jhoyla force-pushed the jhoyla/public-extensions branch from 2728fcd to 28ff330 Compare January 14, 2025 14:42
@jhoyla jhoyla force-pushed the jhoyla/public-extensions branch from 28ff330 to 0287417 Compare January 14, 2025 14:46
Copy link
Contributor

@cjpatton cjpatton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good. Let's chat in our 1:1 about the necessity of the e2e tests.

@@ -533,6 +537,150 @@ async fn leader_upload_taskprov_wrong_version(version: DapVersion) {

async_test_versions!(leader_upload_taskprov_wrong_version);

#[tokio::test]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move these tests to the daphne crate?

@@ -1348,6 +1496,116 @@ async fn leader_selected() {
.unwrap();
}

#[tokio::test]
async fn leader_collect_taskprov_repeated_abort() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this to the unit tests in the daphne crate. or does exercise a behavior that's not implemented by InMemoryAggregator?

(DapVersion::Latest, Some(extensions)) => {
encode_u16_items(bytes, version, extensions.as_slice())?;
}
_ => return Err(CodecError::UnexpectedValue),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually use this for encoding. How about his:

Suggested change
_ => return Err(CodecError::UnexpectedValue),
_ => return Err(CodecError::Other("encountered incorrectly set public extensions".into()))s

Comment on lines +77 to +79
return Err(DapError::ReportError(
crate::messages::ReportError::InvalidMessage,
))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would actually go with a fatal error here

Suggested change
return Err(DapError::ReportError(
crate::messages::ReportError::InvalidMessage,
))
return Err(fatal_error!(
err = format!("public extensions not set correctly for {version:?}")
));

crate::messages::ReportError::InvalidMessage,
))
}
_ => (),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you can avoid having to specify this arm using if let:F

if let (Some(_), DapVersion::Draft09) | (None, DapVersion::Latest) =
            (&public_extensions, version)
{
    ...
}

@@ -223,6 +226,34 @@ pub async fn handle_upload_req<A: DapLeader>(
.into());
}

if let Some(public_extensions) = &report.report_metadata.public_extensions {
// We can be sure at this point that the ReportMetadata is well formed (as
// the decoding / error checking happens in the extractor).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// the decoding / error checking happens in the extractor).
// the decoding / error checking happens in the extractor.

Comment on lines +234 to +239
if protocol::check_no_duplicates(public_extensions.iter()).is_err() {
return Err(DapError::Abort(DapAbort::InvalidMessage {
detail: "Repeated public extension".into(),
task_id,
}));
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IF the metdata "well-formed" (see above comment), then do we need to check for repeats here?

Comment on lines +630 to +640
let mut report = task_config
.vdaf
.produce_report(
&hpke_config_list,
t.now + 1,
&task_id,
DapMeasurement::U32Vec(vec![1; 10]),
version,
)
.unwrap();
report.report_metadata.public_extensions = Some(vec![Extension::Taskprov, Extension::Taskprov]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overwriting exetensions causes an HPKE decryption failure, which isn't what we intend to test here. Modify produce_report() to take in public extensions and set them as you've set them here.

Comment on lines +670 to +676
report.report_metadata.public_extensions = Some(vec![
Extension::Taskprov,
Extension::NotImplemented {
typ: 3,
payload: b"ignore".to_vec(),
},
]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here: don't override

@@ -533,6 +549,150 @@ async fn leader_upload_taskprov_wrong_version(version: DapVersion) {

async_test_versions!(leader_upload_taskprov_wrong_version);

#[tokio::test]
async fn leader_upload_taskprov_public() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this test (already covered by unit tests).

@@ -1348,6 +1508,116 @@ async fn leader_selected() {
.unwrap();
}

#[tokio::test]
async fn leader_collect_taskprov_repeated_abort() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure there's a unit test for this and delete.

Comment on lines +782 to +786
let mut report = t.gen_test_report(task_id).await;
report.report_metadata.public_extensions = Some(vec![Extension::NotImplemented {
typ: 0x01,
payload: vec![0x01],
}]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't override, as this causes hpke decryption to fail. Here and below.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants