From 66dcf01faa4524d80680ba2fb8ea8554dac7d470 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sat, 24 Oct 2026 03:22:51 +0700 Subject: [PATCH 1/2] fix: preserve BReg permission boundaries and explanations Signed-off-by: Jeremi Joslin --- crates/registry-breg/Cargo.toml | 10 + crates/registry-breg/src/access.rs | 7 +- crates/registry-breg/src/auth.rs | 31 +- crates/registry-breg/src/mutation.rs | 4 +- crates/registry-breg/src/mutation/proposal.rs | 68 +- crates/registry-breg/src/postgres/proposal.rs | 116 +- .../registry-breg/src/proposal_retention.rs | 15 +- crates/registry-breg/tests/consent_access.rs | 8 +- .../postgres_proposal_recovery_permissions.rs | 536 ++++++++++ .../tests/postgres_proposal_task_authority.rs | 997 ++++++++++++++++++ crates/registry-bregctl/src/check.rs | 31 + crates/registry-bregctl/src/explain_report.rs | 255 ++++- crates/registry-bregctl/src/lib.rs | 152 ++- crates/registry-bregctl/tests/cli.rs | 203 ++++ .../content/docs/configure/breg-access.mdx | 11 +- products/breg/CHANGELOG.md | 13 + products/breg/SECURITY-REVIEW-NOTES.md | 42 + products/breg/TASK_GRANTS.md | 22 +- .../contracts/security-invariant-matrix.yaml | 3 +- .../contracts/security-test-traceability.yaml | 3 +- products/breg/scripts/test-postgres.sh | 14 +- .../breg/scripts/test_validate_product.py | 2 +- products/breg/scripts/validate_product.py | 4 +- 23 files changed, 2452 insertions(+), 95 deletions(-) create mode 100644 crates/registry-breg/tests/postgres_proposal_recovery_permissions.rs create mode 100644 crates/registry-breg/tests/postgres_proposal_task_authority.rs diff --git a/crates/registry-breg/Cargo.toml b/crates/registry-breg/Cargo.toml index 6bf203a2e4..d04f038e0f 100644 --- a/crates/registry-breg/Cargo.toml +++ b/crates/registry-breg/Cargo.toml @@ -398,6 +398,16 @@ name = "postgres_proposal" path = "tests/postgres_proposal.rs" required-features = ["postgres-test"] +[[test]] +name = "postgres_proposal_task_authority" +path = "tests/postgres_proposal_task_authority.rs" +required-features = ["postgres-test"] + +[[test]] +name = "postgres_proposal_recovery_permissions" +path = "tests/postgres_proposal_recovery_permissions.rs" +required-features = ["postgres-test", "tooling"] + [[test]] name = "postgres_proposal_human_recovery" path = "tests/postgres_proposal_human_recovery.rs" diff --git a/crates/registry-breg/src/access.rs b/crates/registry-breg/src/access.rs index 6b2a4f2900..86c8cf2dbb 100644 --- a/crates/registry-breg/src/access.rs +++ b/crates/registry-breg/src/access.rs @@ -326,14 +326,15 @@ pub(crate) fn compiled_access_findings( } } for reach in row_reach(entities, actions) { - // Ordinary permissions retain their existing finding codes above. - if reach.surface == "record" { + // Ordinary permissions and read-path grants retain their existing + // finding codes above. A read path reaches related rows under the + // root profile's authority; it is not an Action or Proposal target. + if reach.surface == "record" || reach.surface == "read_path" { continue; } // A `self` consent issuer's subject and record targets are bound // through the caller's principal link, not by a row boundary. if reach.rows == "all" - && !reach.owner_only && !link_bound.contains(&reach.source_path) && entities .get(&reach.entity) diff --git a/crates/registry-breg/src/auth.rs b/crates/registry-breg/src/auth.rs index e4c3246172..59030507f1 100644 --- a/crates/registry-breg/src/auth.rs +++ b/crates/registry-breg/src/auth.rs @@ -358,9 +358,11 @@ impl RegistryAuthenticator { fn admit_refusal_warning(last_warning: &Mutex>, now: Instant) -> bool { // One timestamp per authenticator bounds both memory and warning volume, // including concurrent requests with distinct caller-controlled key IDs. - let Ok(mut last) = last_warning.lock() else { - return false; - }; + // A timestamp contains no partial authorization state. Recover it after a + // panic so poisoning cannot permanently silence refusal diagnostics. + let mut last = last_warning + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); if last .is_some_and(|previous| now.saturating_duration_since(previous) < REFUSAL_WARNING_INTERVAL) { @@ -853,6 +855,29 @@ pub(crate) fn authentication_refused() -> Response { mod tests { use super::*; + #[test] + fn refusal_warnings_recover_poisoned_timestamps_and_preserve_rate_limits() { + let last_warning = Mutex::new(None); + let now = Instant::now(); + std::thread::scope(|scope| { + let poisoned = scope.spawn(|| { + let _guard = last_warning.lock().expect("initial warning state"); + panic!("canary warning-state poison"); + }); + assert!(poisoned.join().is_err()); + }); + assert!(last_warning.is_poisoned()); + assert!(admit_refusal_warning(&last_warning, now)); + assert!(!admit_refusal_warning( + &last_warning, + now + REFUSAL_WARNING_INTERVAL - Duration::from_nanos(1) + )); + assert!(admit_refusal_warning( + &last_warning, + now + REFUSAL_WARNING_INTERVAL + )); + } + #[test] fn key_refusal_warnings_are_bounded_across_concurrent_misses_and_resume_after_interval() { let last_warning = Mutex::new(None); diff --git a/crates/registry-breg/src/mutation.rs b/crates/registry-breg/src/mutation.rs index c69a6a54a6..9112bc7208 100644 --- a/crates/registry-breg/src/mutation.rs +++ b/crates/registry-breg/src/mutation.rs @@ -7,7 +7,9 @@ mod action; pub(crate) use action::erase_expired_action_evidence; pub(crate) use action::{HookChangeApplication, HookChangeOutcome}; mod proposal; -pub(crate) use proposal::{proposal_operation_name, ProposalApplyPreflight}; +pub(crate) use proposal::{ + proposal_operation_name, CheckedProposalTaskAuthority, ProposalApplyPreflight, +}; use std::collections::{BTreeMap, BTreeSet}; use std::sync::Arc; diff --git a/crates/registry-breg/src/mutation/proposal.rs b/crates/registry-breg/src/mutation/proposal.rs index 2f7a399c10..5c4908cd9a 100644 --- a/crates/registry-breg/src/mutation/proposal.rs +++ b/crates/registry-breg/src/mutation/proposal.rs @@ -27,6 +27,7 @@ use crate::proposal_workflow::{ pub(crate) enum ProposalApplyPreflight { Receipt(MutationOutcome), Continue { + task_authority: Option, review: Option<( String, registry_review_client::ReviewRequestAccepted, @@ -39,6 +40,42 @@ pub(crate) enum ProposalApplyPreflight { }, } +/// The retained submission authority observed outside the effect transaction. +/// +/// Apply reloads this exact binding under PostgreSQL before releasing effects. +/// The remote authority cannot share that transaction, so the local deadline +/// is also checked at effect admission and again immediately before commit. +pub(crate) struct CheckedProposalTaskAuthority { + retained: Option, +} + +impl CheckedProposalTaskAuthority { + pub(crate) fn new(retained: Option) -> Self { + Self { retained } + } + + fn verify_binding( + &self, + retained: Option<&crate::task_grant::TaskGrantBinding>, + ) -> Result<(), MutationError> { + if retained != self.retained.as_ref() { + return Err(MutationError::PreconditionFailed); + } + self.verify_current() + } + + pub(crate) fn verify_current(&self) -> Result<(), MutationError> { + if self + .retained + .as_ref() + .is_some_and(|binding| !binding.is_current()) + { + return Err(MutationError::PreconditionFailed); + } + Ok(()) + } +} + fn check_current_proposal_grant(authority: &ProposalAuthority) -> Result<(), MutationError> { if authority .task_grant @@ -941,6 +978,13 @@ impl MutationTransaction { return Err(MutationError::PreconditionFailed); } verify_apply_binding(input, proposal)?; + let task_authority = proposal_store::load_task_authority( + tx, + &document.action_id, + id, + document.header.submission_version, + ) + .await?; if !input.authority.is_system() && !crate::api::proposals::external_recovery_actor_allowed( proposal.application_policy(), @@ -1000,6 +1044,7 @@ impl MutationTransaction { vec![] }; Ok(ProposalApplyPreflight::Continue { + task_authority, review: accepted, evidence, }) @@ -1013,6 +1058,7 @@ impl MutationTransaction { input: ProposalMutationInput<'_>, review_evidence: Option<&crate::review_integration::AcceptedReviewEvidence>, commit_evidence: &[crate::action_evidence_client::VerifiedAcquisition], + checked_task_authority: Option<&CheckedProposalTaskAuthority>, deadline: tokio::time::Instant, ) -> Result { let action = proposal_action(registry, input.action_id)?; @@ -1520,6 +1566,16 @@ impl MutationTransaction { .current_proposal() .ok_or(MutationError::PreconditionFailed)?; verify_apply_binding(&input, frozen)?; + let checked_task_authority = + checked_task_authority.ok_or(MutationError::Unavailable)?; + let retained_task_authority = proposal_store::load_task_authority( + tx, + &action.id, + id, + document.header.submission_version, + ) + .await?; + checked_task_authority.verify_binding(retained_task_authority.as_ref())?; if !input.authority.is_system() && !crate::api::proposals::external_recovery_actor_allowed( frozen.application_policy(), @@ -1680,6 +1736,11 @@ impl MutationTransaction { return Err(MutationError::Unavailable); } check_current_proposal_grant(input.authority)?; + if input.operation == ProposalAuthorityOperation::Lifecycle(ProposalOperation::Apply) { + checked_task_authority + .ok_or(MutationError::Unavailable)? + .verify_current()?; + } guarded .commit() .await @@ -1785,7 +1846,6 @@ impl MutationTransaction { if tokio::time::Instant::now() >= deadline { return Err(MutationError::Unavailable); } - check_current_proposal_grant(input.authority)?; if input.operation == ProposalAuthorityOperation::Lifecycle(ProposalOperation::Apply) { let frozen = workflow .current_proposal() @@ -1830,6 +1890,12 @@ impl MutationTransaction { if tokio::time::Instant::now() >= deadline { return Err(MutationError::Unavailable); } + check_current_proposal_grant(input.authority)?; + if input.operation == ProposalAuthorityOperation::Lifecycle(ProposalOperation::Apply) { + checked_task_authority + .ok_or(MutationError::Unavailable)? + .verify_current()?; + } guarded .commit() .await diff --git a/crates/registry-breg/src/postgres/proposal.rs b/crates/registry-breg/src/postgres/proposal.rs index 63a24e2cdd..e111d1db39 100644 --- a/crates/registry-breg/src/postgres/proposal.rs +++ b/crates/registry-breg/src/postgres/proposal.rs @@ -6,7 +6,7 @@ use super::*; use crate::api::proposals::{ProposalListQuery, ProposalMutationInput}; use crate::api::VerifiedRequestClaims; use crate::model::ProposalOperation; -use crate::mutation::ProposalApplyPreflight; +use crate::mutation::{CheckedProposalTaskAuthority, ProposalApplyPreflight}; use crate::postgres::{begin_proposal_transaction, ProposalAuthority, ProposalAuthorityOperation}; use crate::proposal_store; use std::collections::BTreeMap; @@ -453,63 +453,72 @@ impl PostgresRecordMutationService { .await .map_err(|_| MutationError::Unavailable)??; } - let (review_evidence, commit_requests) = - if input.operation == ProposalAuthorityOperation::Lifecycle(ProposalOperation::Apply) { - let preflight = { - let client = tokio::time::timeout_at(deadline, self.pool.get()) - .await - .map_err(|_| MutationError::Unavailable)? - .map_err(|_| MutationError::Unavailable)?; - let mut guard = QueryCancellationGuard::new(self.pool.clone(), client); - let preflight = tokio::time::timeout_at( + let (review_evidence, commit_requests, checked_task_authority) = if input.operation + == ProposalAuthorityOperation::Lifecycle(ProposalOperation::Apply) + { + let preflight = { + let client = tokio::time::timeout_at(deadline, self.pool.get()) + .await + .map_err(|_| MutationError::Unavailable)? + .map_err(|_| MutationError::Unavailable)?; + let mut guard = QueryCancellationGuard::new(self.pool.clone(), client); + let preflight = tokio::time::timeout_at( + deadline, + self.coordinator.preflight_proposal_apply( + guard.client(), + &self.registry, + &input, + ), + ) + .await; + match preflight { + Ok(result) => { + guard.disarm(); + result? + } + Err(_) => { + guard.cancel_and_discard().await; + return Err(MutationError::Unavailable); + } + } + }; + let (task_authority, review, evidence) = match preflight { + ProposalApplyPreflight::Receipt(receipt) => return Ok(receipt), + ProposalApplyPreflight::Continue { + task_authority, + review, + evidence, + } => (task_authority, review, evidence), + }; + if let Some(binding) = &task_authority { + tokio::time::timeout_at(deadline, self.coordinator.check_task_authority(binding)) + .await + .map_err(|_| MutationError::Unavailable)??; + } + let checked_task_authority = CheckedProposalTaskAuthority::new(task_authority); + let review_evidence = match review { + None => None, + Some((authority, accepted, result_id)) => { + let source = self + .review_result_source + .as_ref() + .ok_or(MutationError::Unavailable)?; + let fresh = tokio::time::timeout_at( deadline, - self.coordinator.preflight_proposal_apply( - guard.client(), - &self.registry, - &input, - ), + source.approved_evidence(&authority, &accepted), ) - .await; - match preflight { - Ok(result) => { - guard.disarm(); - result? - } - Err(_) => { - guard.cancel_and_discard().await; - return Err(MutationError::Unavailable); - } - } - }; - match preflight { - ProposalApplyPreflight::Receipt(receipt) => return Ok(receipt), - ProposalApplyPreflight::Continue { - review: None, - evidence, - } => (None, evidence), - ProposalApplyPreflight::Continue { - review: Some((authority, accepted, result_id)), - evidence, - } => { - let source = self - .review_result_source - .as_ref() - .ok_or(MutationError::Unavailable)?; - let fresh = tokio::time::timeout_at( - deadline, - source.approved_evidence(&authority, &accepted), - ) - .await - .map_err(|_| MutationError::Unavailable)??; - if result_id.is_some_and(|result_id| fresh.result_id() != result_id) { - return Err(MutationError::ProposalReviewResultMismatch); - } - (Some(fresh), evidence) + .await + .map_err(|_| MutationError::Unavailable)??; + if result_id.is_some_and(|result_id| fresh.result_id() != result_id) { + return Err(MutationError::ProposalReviewResultMismatch); } + Some(fresh) } - } else { - (None, vec![]) }; + (review_evidence, evidence, Some(checked_task_authority)) + } else { + (None, vec![], None) + }; // Preflight admitted and projected only the frozen guard selectors. // Reuse the configured, pinned provider adapter with no checked-out // database connection; the coordinator rechecks those same selectors. @@ -556,6 +565,7 @@ impl PostgresRecordMutationService { input, review_evidence.as_ref(), &commit_evidence, + checked_task_authority.as_ref(), deadline, ), ) diff --git a/crates/registry-breg/src/proposal_retention.rs b/crates/registry-breg/src/proposal_retention.rs index 9ac7f68e0b..88e6286b90 100644 --- a/crates/registry-breg/src/proposal_retention.rs +++ b/crates/registry-breg/src/proposal_retention.rs @@ -913,15 +913,24 @@ pub async fn guard_successor_activation( let CompiledActionExecution::Proposal { proposal } = &action.execution else { return Err(ProposalRetentionError::ActiveProposalRecoveryUnavailable); }; - if !proposal.permissions.iter().any(|grant| { + let inspection_available = proposal.permissions.iter().any(|grant| { grant .operations .contains(&crate::model::ProposalOperation::Get) - }) || !proposal.permissions.iter().any(|grant| { + }); + let cancellation_recovery_available = proposal.permissions.iter().any(|grant| { grant .operations .contains(&crate::model::ProposalOperation::Cancel) - }) { + && grant.operations.iter().any(|operation| { + matches!( + operation, + crate::model::ProposalOperation::Get + | crate::model::ProposalOperation::List + ) + }) + }); + if !inspection_available || !cancellation_recovery_available { return Err(ProposalRetentionError::ActiveProposalRecoveryUnavailable); } } diff --git a/crates/registry-breg/tests/consent_access.rs b/crates/registry-breg/tests/consent_access.rs index 0c58db35a3..6eb95e18d6 100644 --- a/crates/registry-breg/tests/consent_access.rs +++ b/crates/registry-breg/tests/consent_access.rs @@ -380,11 +380,9 @@ fn a_self_issued_actions_link_bound_targets_are_not_registry_wide_findings() { .collect::>() }; let expected = [ - // Direct read paths have their own disclosure finding. They remain - // visible even when the compiler proves the Action targets are bound - // through the caller's principal link. - "recordTypes[id=enrolment].accessProfiles[id=food-targeting].readPaths[path=households]", - "recordTypes[id=person].accessProfiles[id=food-targeting].readPaths[path=households]", + // Direct read paths have their own related-disclosure finding. They + // are not Action or Proposal target permissions and must not receive + // this target-authority warning as well. "actions[id=record-consent].permissions[profile=steward].targets[recordType=consent-decision].rowBoundaries", "actions[id=record-consent].permissions[profile=steward].targets[recordType=person].rowBoundaries", ]; diff --git a/crates/registry-breg/tests/postgres_proposal_recovery_permissions.rs b/crates/registry-breg/tests/postgres_proposal_recovery_permissions.rs new file mode 100644 index 0000000000..558f2fa861 --- /dev/null +++ b/crates/registry-breg/tests/postgres_proposal_recovery_permissions.rs @@ -0,0 +1,536 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Successor activation preserves one usable public recovery path for pending Proposals. +#![cfg(feature = "postgres-test")] + +#[path = "support/client_http.rs"] +#[allow(dead_code)] +mod client_http; +#[path = "support/postgres_harness.rs"] +#[allow(dead_code)] +mod postgres_harness; +#[cfg(feature = "tooling")] +#[path = "support/proposal_activation.rs"] +mod proposal_activation; + +use registry_breg::api::{VerifiedClaimValue, VerifiedRequestClaims}; +use registry_breg::compiler::{compile_project, CompileProfile}; +use registry_breg::contract::parse_project_json; +use registry_breg::proposal_retention::ProposalRetentionError; +use registry_breg_client::{ + BRegActionInvocationRequest, BRegCreateRequest, BRegDirectWrite, BRegIdempotencyKey, + BRegProposal, BRegProposalQuery, BRegProposalState, BRegRecordFormat, +}; +use serde_json::{json, Map, Value}; +use std::collections::{BTreeMap, BTreeSet}; + +fn claims() -> VerifiedRequestClaims { + VerifiedRequestClaims::authenticated_with_actor_kind( + "sub", + "author", + BTreeSet::from(["proposal:manage".into()]), + Some("registry-administration".into()), + BTreeMap::from([( + "jurisdiction".into(), + VerifiedClaimValue::direct_string("zone-a").unwrap(), + )]), + registry_platform_oidc::ActorKind::Human, + ) + .unwrap() +} + +fn key(value: &str) -> BRegIdempotencyKey { + BRegIdempotencyKey::parse(value).unwrap() +} + +fn map(value: Value) -> Map { + value.as_object().unwrap().clone() +} + +fn registry_source() -> Value { + json!({ + "apiVersion":"id.registrystack.org/formats/breg/project/v1alpha1", + "kind":"BRegProject", + "project":{ + "id":"proposal-recovery-permissions", + "version":"1", + "defaultLanguage":"en", + "canonicalBaseIri":"https://proposal-recovery.example.test" + }, + "datasets":[{"id":"directory"}], + "recordTypes":[{ + "id":"target", + "primaryDataset":"directory", + "route":"targets", + "mutationMode":"mutable", + "mutationPolicy":{"patch":"proposal-required"}, + "fields":[ + {"id":"name","type":"string","required":true,"maximumLength":64,"classification":"internal"}, + {"id":"jurisdiction","type":"string","required":true,"maximumLength":64,"classification":"internal"} + ] + }], + "actions":[{ + "id":"correct-name", + "execution":{"mode":"proposal","proposal":{ + "review":{"type":"none"}, + "application":{"mode":"manual"}, + "retention":{"mode":"retain"} + }}, + "inputs":[ + {"id":"target","type":"reference","target":"target","required":true,"classification":"internal"}, + {"id":"value","type":"string","required":true,"maximumLength":64,"classification":"internal"} + ], + "commitGuards":{"targets":[{"input":"target","revision":"unchanged"}]}, + "plan":{"type":"fixed","effects":[{ + "id":"name", + "target":{"fromInput":"target"}, + "operation":"patch", + "set":{"name":{"fromInput":"value"}} + }]} + }], + "accessProfiles":[{ + "id":"clerk", + "default":true, + "principalClaim":"sub", + "requiredScopes":["proposal:manage"], + "requiredPurposes":["registry-administration"], + "permissions":{ + "records":[{ + "recordType":"target", + "operations":["create","get","list"], + "readableFields":["name","jurisdiction"], + "writableFields":["name","jurisdiction"], + "rowBoundaries":[{"field":"jurisdiction","claim":"jurisdiction","operator":"equals"}] + }], + "actions":[{ + "action":"correct-name", + "operations":["start"], + "targets":[{ + "recordType":"target", + "rowBoundaries":[{"field":"jurisdiction","claim":"jurisdiction","operator":"equals"}] + }] + }], + "proposals":[{ + "action":"correct-name", + "operations":["get","list","edit","submit","revise","rebase","retry","cancel","apply"], + "ownership":"caller", + "targets":[{ + "recordType":"target", + "rowBoundaries":[{"field":"jurisdiction","claim":"jurisdiction","operator":"equals"}] + }] + }] + } + }] + }) +} + +fn compile(source: &Value) -> registry_breg::CompiledRegistry { + let project = parse_project_json(&serde_json::to_vec(source).unwrap()).unwrap(); + compile_project(&project, &[], CompileProfile::Authoring) + .expect("Proposal recovery fixture compiles") +} + +fn set_operations(profile: &mut Value, operations: Value) { + profile["permissions"]["proposals"][0]["operations"] = operations; +} + +fn add_profile(source: &mut Value, id: &str, operations: Value) { + let mut profile = source["accessProfiles"][0].clone(); + profile["id"] = json!(id); + profile["default"] = json!(false); + profile["permissions"]["records"] = json!([]); + profile["permissions"]["actions"] = json!([]); + set_operations(&mut profile, operations); + source["accessProfiles"] + .as_array_mut() + .unwrap() + .push(profile); +} + +fn split_recovery_source(source: &Value) -> Value { + let mut candidate = source.clone(); + set_operations(&mut candidate["accessProfiles"][0], json!(["get"])); + add_profile(&mut candidate, "canceller", json!(["cancel"])); + candidate +} + +fn listed_recovery_source(source: &Value) -> Value { + let mut candidate = source.clone(); + set_operations(&mut candidate["accessProfiles"][0], json!(["get"])); + add_profile( + &mut candidate, + "recovery-operator", + json!(["list", "cancel"]), + ); + candidate +} + +async fn start_proposal(fixture: &client_http::ClientFixture) -> BRegProposal { + let metadata = fixture + .http + .client + .registry_contract(Some("clerk")) + .await + .unwrap(); + let BRegDirectWrite::Create(create) = metadata + .value + .select_direct_write("records.target.create", "clerk") + .unwrap() + else { + panic!("target create route must be direct"); + }; + let record = fixture + .http + .client + .create_record( + &create, + &BRegCreateRequest::new(map(json!({ + "name":"Before", + "jurisdiction":"zone-a" + }))) + .unwrap(), + &key("recovery-permissions-record"), + BRegRecordFormat::Json, + ) + .await + .unwrap(); + let action = metadata + .value + .select_action("correct-name", "clerk") + .unwrap(); + let input = BRegActionInvocationRequest::new( + &action, + map(json!({ + "target":record.value.data.record_identifier, + "value":"After" + })), + None, + ) + .unwrap(); + fixture + .http + .client + .start_proposal(&action, &input, &key("recovery-permissions-start")) + .await + .unwrap() + .value +} + +async fn submit_proposal( + fixture: &client_http::ClientFixture, + draft: &BRegProposal, +) -> BRegProposal { + let submit = fixture + .http + .client + .prepare_proposal( + draft, + "submit", + json!({}), + &key("recovery-permissions-submit"), + ) + .unwrap(); + fixture + .http + .client + .execute_proposal(&submit) + .await + .unwrap() + .value +} + +fn assert_same_action_revision( + original: ®istry_breg::CompiledRegistry, + candidate: ®istry_breg::CompiledRegistry, +) { + assert_eq!( + original.actions().actions[0].definition_revision, + candidate.actions().actions[0].definition_revision, + "permission-only successors must not reinterpret the frozen Action" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn successor_recovery_requires_one_profile_with_a_condition_source_and_cancel() { + let source = registry_source(); + let original = compile(&source); + let fixture = client_http::ClientFixture::start(original.clone(), claims()).await; + let _draft = start_proposal(&fixture).await; + + let mut get_and_cancel = source.clone(); + set_operations( + &mut get_and_cancel["accessProfiles"][0], + json!(["get", "cancel"]), + ); + let get_and_cancel = compile(&get_and_cancel); + assert_same_action_revision(&original, &get_and_cancel); + registry_breg::proposal_retention::guard_successor_activation( + &fixture.database.admin, + &get_and_cancel, + ) + .await + .expect("one profile can recover through Get and Cancel"); + + let list_and_cancel = compile(&listed_recovery_source(&source)); + assert_same_action_revision(&original, &list_and_cancel); + registry_breg::proposal_retention::guard_successor_activation( + &fixture.database.admin, + &list_and_cancel, + ) + .await + .expect("one profile can recover through a promoted List condition and Cancel"); + + let split = compile(&split_recovery_source(&source)); + assert_same_action_revision(&original, &split); + assert_eq!( + registry_breg::proposal_retention::guard_successor_activation( + &fixture.database.admin, + &split, + ) + .await, + Err(ProposalRetentionError::ActiveProposalRecoveryUnavailable), + "Get and Cancel in different authority contexts cannot produce a usable condition" + ); + + fixture.finish().await; +} + +#[cfg(feature = "tooling")] +struct ActiveReady; + +#[cfg(feature = "tooling")] +impl registry_breg::api::ReadinessProbe for ActiveReady { + fn is_ready(&self) -> registry_breg::api::ServiceFuture<'_, bool> { + Box::pin(async { true }) + } +} + +#[cfg(feature = "tooling")] +async fn successor_client( + fixture: &client_http::ClientFixture, + registry: registry_breg::CompiledRegistry, + identity: registry_breg::postgres::ExpectedRegistryIdentity, +) -> client_http::ClientHttp { + use registry_breg::api::{HttpService, ReadRuntimeIdentity}; + use registry_breg::postgres::{ + PostgresRecordMutationService, PostgresRecordReadService, RegistryLockKey, + }; + use std::sync::Arc; + + let registry = Arc::new(registry); + let lock = RegistryLockKey::derive(registry.registry_id()).unwrap(); + let audit = registry_breg::audit::test_support::capturing( + registry_platform_audit::AuditProfile::production_from_secret_bytes(vec![0x61; 32].into()) + .unwrap(), + ) + .0; + let cursors = Arc::new( + registry_breg::cursor::CursorCodec::new( + zeroize::Zeroizing::new(vec![0x62; 32]), + std::time::Duration::from_secs(300), + ) + .unwrap(), + ); + let records = PostgresRecordReadService::new( + fixture.pool.clone(), + registry.clone(), + identity.clone(), + lock, + std::time::Duration::from_secs(2), + audit.clone(), + cursors.clone(), + ); + let mutations = PostgresRecordMutationService::new( + fixture.pool.clone(), + registry.clone(), + identity.clone(), + "proposal-recovery-permissions-test", + lock, + std::time::Duration::from_secs(2), + audit, + ); + let service = HttpService::new( + registry, + ReadRuntimeIdentity { + package_revision: identity.activation_id, + schema_fingerprint: identity.schema_fingerprint, + }, + Arc::new(records), + Arc::new(ActiveReady), + cursors, + ) + .with_postgres_mutations(Arc::new(mutations)); + client_http::ClientHttp::start(registry_breg::api::router(Arc::new(service)), claims()).await +} + +#[cfg(feature = "tooling")] +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn refused_split_recovery_successor_preserves_activation_and_pending_work() { + use registry_breg::migration::{ + apply_verified_package, ActivationDeployment, ApplyPrecondition, ApplyRoles, ApplyTimeouts, + ApplyVerifiedPackageRequest, MigrationError, + }; + use std::time::Duration; + + let source = registry_source(); + let original = compile(&source); + let fixture = client_http::ClientFixture::start(original.clone(), claims()).await; + let draft = start_proposal(&fixture).await; + let submitted = submit_proposal(&fixture, &draft).await; + let split = split_recovery_source(&source); + assert_same_action_revision(&original, &compile(&split)); + let package = proposal_activation::verified_successor( + &fixture, + &fixture.registry, + &fixture.identity, + &split, + ) + .await; + let before = fixture + .database + .admin + .query_one( + "SELECT maintenance_status,maintenance_target_package_digest, + (SELECT count(*)::bigint FROM registry_internal.registry_migrations) + FROM registry_internal.registry_state WHERE singleton", + &[], + ) + .await + .unwrap(); + let before_status: String = before.get(0); + let before_target: Option = before.get(1); + let before_activations: i64 = before.get(2); + + let request = ApplyVerifiedPackageRequest::new( + &fixture.database.migration_config, + &package, + ActivationDeployment::new( + "local", + "proposal-recovery-permissions-test", + &fixture.identity.database_id, + ), + ApplyPrecondition::Successor { + current: &fixture.identity, + }, + ApplyRoles::new( + &fixture.database.migration_role, + &fixture.database.runtime_role, + ), + ApplyTimeouts::new(Duration::from_secs(2), Duration::from_secs(10)).unwrap(), + fixture.database.activation_audit(), + ); + assert_eq!( + apply_verified_package(request).await, + Err(MigrationError::ActiveProposalRecoveryUnavailable) + ); + + let after = fixture + .database + .admin + .query_one( + "SELECT maintenance_status,maintenance_target_package_digest, + (SELECT count(*)::bigint FROM registry_internal.registry_migrations), + EXISTS (SELECT 1 FROM registry_internal.registry_migrations WHERE package_digest=$1) + FROM registry_internal.registry_state WHERE singleton", + &[&package.package_digest()], + ) + .await + .unwrap(); + assert_eq!(after.get::<_, String>(0), before_status); + assert_eq!(after.get::<_, Option>(1), before_target); + assert_eq!(after.get::<_, i64>(2), before_activations); + assert!( + !after.get::<_, bool>(3), + "refusal must not install a ledger row" + ); + + let current = fixture + .http + .client + .read_proposal(&submitted) + .await + .unwrap() + .value; + assert_eq!(current.state(), BRegProposalState::Submitted); + assert_eq!(current.revision(), submitted.revision()); + assert_eq!(current.effect_digest(), submitted.effect_digest()); + assert!(current.blocked_reason().is_none()); + + fixture.finish().await; +} + +#[cfg(feature = "tooling")] +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn listed_recovery_profile_cancels_after_real_successor_activation() { + let source = registry_source(); + let original = compile(&source); + let fixture = client_http::ClientFixture::start(original.clone(), claims()).await; + let draft = start_proposal(&fixture).await; + let submitted = submit_proposal(&fixture, &draft).await; + let candidate = listed_recovery_source(&source); + assert_same_action_revision(&original, &compile(&candidate)); + + let (registry, identity) = proposal_activation::activate_successor(&fixture, &candidate).await; + assert_ne!(identity.activation_id, fixture.identity.activation_id); + let current = successor_client(&fixture, registry, identity).await; + let metadata = current + .client + .registry_contract(Some("recovery-operator")) + .await + .unwrap(); + let collection = metadata + .value + .select_proposals("recovery-operator") + .unwrap(); + let listed = current + .client + .list_proposals(&collection, &BRegProposalQuery::default()) + .await + .unwrap() + .value + .items + .into_iter() + .find(|proposal| proposal.id() == submitted.id()) + .expect("List returns the pending Proposal under the recovery profile"); + assert!( + listed + .link("cancel") + .and_then(|link| link.availability()) + .is_none_or(|availability| availability.available), + "List promotes a usable same-profile Cancel condition" + ); + let cancel = current + .client + .prepare_proposal( + &listed, + "cancel", + json!({}), + &key("recovery-permissions-cancel"), + ) + .unwrap(); + let cancelled = current + .client + .execute_proposal(&cancel) + .await + .unwrap() + .value; + assert_eq!(cancelled.state(), BRegProposalState::Cancelled); + assert_eq!(cancelled.revision(), submitted.revision() + 1); + + let clerk_metadata = current + .client + .registry_contract(Some("clerk")) + .await + .unwrap(); + let get = clerk_metadata.value.select_proposal_get("clerk").unwrap(); + let observed = current + .client + .get_proposal(&get, submitted.id()) + .await + .unwrap() + .value; + assert_eq!(observed.state(), BRegProposalState::Cancelled); + assert_eq!(observed.revision(), cancelled.revision()); + + drop(current); + fixture.finish().await; +} diff --git a/crates/registry-breg/tests/postgres_proposal_task_authority.rs b/crates/registry-breg/tests/postgres_proposal_task_authority.rs new file mode 100644 index 0000000000..2a42fd9b7c --- /dev/null +++ b/crates/registry-breg/tests/postgres_proposal_task_authority.rs @@ -0,0 +1,997 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Retained task authority governs every uncommitted Proposal application. +#![cfg(feature = "postgres-test")] + +#[path = "support/client_http.rs"] +#[allow(dead_code)] +mod client_http; +#[path = "support/postgres_harness.rs"] +#[allow(dead_code)] +mod postgres_harness; + +use registry_breg::{ + api::{VerifiedClaimValue, VerifiedRequestClaims}, + auth::{AuthorityClaimConfig, RegistryAuthenticator}, + compiler::{compile_project, CompileProfile}, + contract::parse_project_json, + task_grant::{TaskGrantBinding, TaskGrantError, TaskGrantStatusChecker}, +}; +use registry_platform_httputil::FetchUrlPolicy; +use registry_platform_oidc::{JwksFetcher, JwksFetcherConfig}; +use registry_platform_testing::{oidc_verifier_config, MockIdp}; +use reqwest::{Method, StatusCode}; +use serde_json::{json, Value}; +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::{ + atomic::{AtomicBool, AtomicU64, AtomicU8, AtomicUsize, Ordering}, + Arc, + }, + time::Duration, +}; +use tokio::sync::Notify; + +const ACTIVE: u8 = 0; +const UNAVAILABLE: u8 = 1; +const REFUSED: u8 = 2; + +fn registry(automatic: bool, reviewed: bool) -> registry_breg::CompiledRegistry { + let review = if reviewed { + json!({ + "type":"required", + "authority":"local-review", + "source":"proposal-task-authority", + "policy":{ + "id":"name-correction", + "version":"1", + "digest":format!("sha256:{}", "b".repeat(64)) + } + }) + } else { + json!({"type":"none"}) + }; + let source = json!({ + "apiVersion":"id.registrystack.org/formats/breg/project/v1alpha1", + "kind":"BRegProject", + "project":{ + "id":"proposal-task-authority", + "version":"1", + "defaultLanguage":"en", + "canonicalBaseIri":"https://proposal-task-authority.example.test" + }, + "datasets":[{"id":"directory"}], + "reviewAuthorities":if reviewed { + json!([{"id":"local-review","protocol":"id.registrystack.org/protocols/review/v1"}]) + } else { + json!([]) + }, + "recordTypes":[{ + "id":"target", + "primaryDataset":"directory", + "route":"targets", + "mutationMode":"mutable", + "mutationPolicy":{"patch":"proposal-required"}, + "fields":[ + {"id":"name","type":"string","required":true,"maximumLength":64,"classification":"internal"}, + {"id":"jurisdiction","type":"string","required":true,"maximumLength":64,"classification":"internal"} + ] + }], + "actions":[{ + "id":"correct-name", + "execution":{"mode":"proposal","proposal":{ + "review":review, + "application":if automatic { + json!({"mode":"automatic","maximumAttempts":3}) + } else { + json!({"mode":"manual"}) + }, + "retention":{"mode":"operator-erase"} + }}, + "inputs":[ + {"id":"target","type":"reference","target":"target","required":true,"classification":"internal"}, + {"id":"value","type":"string","required":true,"maximumLength":64,"classification":"internal"} + ], + "commitGuards":{"targets":[{"input":"target","revision":"unchanged"}]}, + "plan":{"type":"fixed","effects":[{ + "id":"name", + "target":{"fromInput":"target"}, + "operation":"patch", + "set":{"name":{"fromInput":"value"}} + }]} + }], + "accessProfiles":[ + { + "id":"clerk", + "default":true, + "principalClaim":"registry_principal", + "requiredScopes":["proposal:manage"], + "requiredPurposes":["registry-administration"], + "permissions":{ + "records":[{ + "recordType":"target", + "operations":["create","get","list"], + "readableFields":["name","jurisdiction"], + "writableFields":["name","jurisdiction"], + "rowBoundaries":[{"field":"jurisdiction","claim":"jurisdiction","operator":"equals"}] + }], + "actions":[{ + "action":"correct-name", + "operations":["start"], + "targets":[{"recordType":"target","rowBoundaries":[{"field":"jurisdiction","claim":"jurisdiction","operator":"equals"}]}] + }], + "proposals":[{ + "action":"correct-name", + "operations":["get","edit","submit","retry","cancel","apply"], + "ownership":"caller", + "targets":[{"recordType":"target","rowBoundaries":[{"field":"jurisdiction","claim":"jurisdiction","operator":"equals"}]}] + }] + } + }, + { + "id":"task-agent", + "principalClaim":"registry_principal", + "actorKind":"agent", + "requesterClients":["agent-client"], + "requiredScopes":["proposal:manage"], + "requiredPurposes":["registry-administration"], + "taskGrant":{"sourceIssuer":"https://casework.example"}, + "permissions":{ + "actions":[{ + "action":"correct-name", + "operations":["start"], + "targets":[{"recordType":"target","rowBoundaries":[{"field":"jurisdiction","claim":"jurisdiction","operator":"equals"}]}] + }], + "proposals":[{ + "action":"correct-name", + "operations":["get","edit","submit","cancel"], + "ownership":"caller", + "targets":[{"recordType":"target","rowBoundaries":[{"field":"jurisdiction","claim":"jurisdiction","operator":"equals"}]}] + }] + } + } + ] + }); + compile_project( + &parse_project_json(&serde_json::to_vec(&source).unwrap()).unwrap(), + &[], + CompileProfile::Authoring, + ) + .expect("task-authority registry compiles") +} + +fn clerk() -> VerifiedRequestClaims { + VerifiedRequestClaims::authenticated( + "registry_principal", + "author", + BTreeSet::from(["proposal:manage".into()]), + Some("registry-administration".into()), + BTreeMap::from([( + "jurisdiction".into(), + VerifiedClaimValue::direct_string("zone-a").unwrap(), + )]), + ) + .unwrap() +} + +struct Status { + mode: AtomicU8, + refused_grant: AtomicU64, + calls: AtomicUsize, + pause: AtomicBool, + entered: Notify, + release: Notify, +} + +impl TaskGrantStatusChecker for Status { + fn check<'a>( + &'a self, + binding: &'a TaskGrantBinding, + ) -> std::pin::Pin> + Send + 'a>> + { + self.calls.fetch_add(1, Ordering::SeqCst); + Box::pin(async move { + if binding.grant_id() == grant_id(2) && self.pause.load(Ordering::SeqCst) { + self.entered.notify_one(); + self.release.notified().await; + } + match self.mode.load(Ordering::SeqCst) { + UNAVAILABLE => Err(TaskGrantError::Unavailable), + REFUSED + if binding.grant_id() + == grant_id(self.refused_grant.load(Ordering::SeqCst)) => + { + Err(TaskGrantError::Refused) + } + _ => Ok(()), + } + }) + } +} + +fn status() -> Arc { + Arc::new(Status { + mode: AtomicU8::new(ACTIVE), + refused_grant: AtomicU64::new(0), + calls: AtomicUsize::new(0), + pause: AtomicBool::new(false), + entered: Notify::new(), + release: Notify::new(), + }) +} + +struct NoToken; + +#[async_trait::async_trait] +impl registry_platform_httputil::TokenProvider for NoToken { + async fn bearer_token( + &self, + ) -> Result + { + Err(registry_platform_httputil::TokenError::Unavailable) + } +} + +fn review_binding() -> Arc { + use registry_breg::review_store::{ReviewAuthorityClient, ReviewAuthorityRegistry}; + let client = registry_review_client::ReviewClient::new( + registry_review_client::ReviewClientConfig::new("http://127.0.0.1:9/".parse().unwrap()), + ) + .unwrap(); + let authority = Arc::new( + ReviewAuthorityClient::new( + "local-review".into(), + client, + Arc::new(NoToken), + "producer".into(), + "proposal-producer".into(), + 7, + None, + None, + ) + .unwrap(), + ); + Arc::new( + ReviewAuthorityRegistry::new(BTreeMap::from([("local-review".into(), authority)])).unwrap(), + ) +} + +struct ApprovedReview { + calls: AtomicUsize, + result_id: uuid::Uuid, +} + +#[async_trait::async_trait] +impl registry_breg::review_store::ReviewResultSource for ApprovedReview { + async fn approved_evidence( + &self, + authority: &str, + accepted: ®istry_review_client::ReviewRequestAccepted, + ) -> Result< + registry_breg::review_integration::AcceptedReviewEvidence, + registry_breg::mutation::MutationError, + > { + self.calls.fetch_add(1, Ordering::SeqCst); + let now = chrono::Utc::now(); + let result = registry_review_client::ReviewResult { + result_id: self.result_id, + request_id: accepted.request_id, + subject: accepted.subject.clone(), + policy: accepted.policy.clone(), + submission_digest: accepted.submission_digest.clone(), + status: registry_review_client::ReviewResultStatus::Approved, + outcome: Some("approved".to_owned()), + public_reason: None, + result: None, + completed_at: now - chrono::Duration::seconds(1), + available_until: now + chrono::Duration::minutes(5), + }; + registry_breg::review_integration::AcceptedReviewEvidence::from_protocol( + authority, accepted, &result, + ) + .map_err(|_| registry_breg::mutation::MutationError::PreconditionFailed) + } +} + +fn grant_id(grant: u64) -> String { + format!("00000000-0000-4000-8000-{grant:012x}") +} + +fn token(grant: u64, permissions: Value) -> Value { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + json!({ + "aud":"urn:example:breg", + "sub":"author", + "azp":"agent-client", + "registry_principal":"author", + "registry_actor_kind":"agent", + "registry_purpose":"registry-administration", + "scope":"proposal:manage", + "registry_grant_id":grant_id(grant), + "registry_approver":"h:approver", + "registry_grant_source_issuer":"https://casework.example", + "registry_grant_client":"agent-client", + "registry_grant_resource":"urn:example:breg", + "registry_grant_exp":now+600, + "registry_grant_bounds":{"type":"breg","permissions":permissions}, + "identity":{"jurisdiction":"zone-a"}, + "jurisdiction":"zone-b" + }) +} + +async fn authenticator( + registry: ®istry_breg::CompiledRegistry, + idp: &MockIdp, +) -> RegistryAuthenticator { + let mut verifier = oidc_verifier_config(idp.issuer(), vec!["urn:example:breg".into()]); + verifier.allowed_clients = vec!["agent-client".into()]; + RegistryAuthenticator::new( + registry, + verifier, + Arc::new(JwksFetcher::new_with_fetch_url_policy( + idp.jwks_uri(), + JwksFetcherConfig::defaults(), + FetchUrlPolicy::dev(), + )), + AuthorityClaimConfig::new("registry_principal", Some("registry_purpose".into())), + ) + .unwrap() +} + +async fn send( + http: &client_http::ClientHttp, + method: Method, + path: &str, + body: Value, + key: Option<&str>, + etag: Option<&str>, +) -> (StatusCode, Value, Option) { + let mut request = reqwest::Client::new() + .request(method, format!("{}{path}", http.base_url)) + .header("content-type", "application/json"); + if let Some(key) = key { + request = request.header("idempotency-key", key); + } + if let Some(etag) = etag { + request = request.header("if-match", etag); + } + if !body.is_null() { + request = request.json(&body); + } + let response = request.send().await.unwrap(); + let status = response.status(); + let etag = response + .headers() + .get("etag") + .map(|value| value.to_str().unwrap().to_owned()); + (status, response.json().await.unwrap(), etag) +} + +async fn create_target(fixture: &client_http::ClientFixture, suffix: &str) -> String { + let (status, value, _) = send( + &fixture.http, + Method::POST, + "/v1/records/targets?accessProfile=clerk", + json!({"data":{"name":"Before","jurisdiction":"zone-a"}}), + Some(&format!("create-target-{suffix}")), + None, + ) + .await; + assert_eq!(status, StatusCode::CREATED, "{value}"); + value["data"]["recordIdentifier"] + .as_str() + .unwrap() + .to_owned() +} + +async fn submit_with_task( + fixture: &client_http::ClientFixture, + auth: &RegistryAuthenticator, + idp: &MockIdp, + target: &str, + suffix: &str, +) -> (String, Value, String) { + let starter = fixture + .client_for( + auth.authenticate(&idp.mint_token(token( + 1, + json!([{"collection":"action:correct-name","operations":["start"]}]), + ))) + .await + .unwrap(), + ) + .await; + let (status, draft, etag) = send( + &starter, + Method::POST, + "/v1/actions/correct-name/proposals?accessProfile=task-agent", + json!({"input":{"target":target,"value":"After"}}), + Some(&format!("start-{suffix}")), + None, + ) + .await; + assert_eq!(status, StatusCode::CREATED, "{draft}"); + let proposal_id = draft["id"].as_str().unwrap().to_owned(); + let lifecycle = fixture + .client_for( + auth.authenticate(&idp.mint_token(token( + 2, + json!([{ + "collection":format!("proposal:{proposal_id}"), + "operations":["get","edit","submit","cancel"] + }]), + ))) + .await + .unwrap(), + ) + .await; + let (status, submitted, submitted_etag) = send( + &lifecycle, + Method::POST, + &format!("/v1/proposals/{proposal_id}/submit?accessProfile=task-agent"), + json!({}), + Some(&format!("submit-{suffix}")), + etag.as_deref(), + ) + .await; + assert_eq!(status, StatusCode::OK, "{submitted}"); + (proposal_id, submitted, submitted_etag.unwrap()) +} + +async fn read_target_name(fixture: &client_http::ClientFixture, target: &str) -> String { + let (status, value, _) = send( + &fixture.http, + Method::GET, + &format!("/v1/records/targets/{target}?accessProfile=clerk"), + Value::Null, + None, + None, + ) + .await; + assert_eq!(status, StatusCode::OK, "{value}"); + value["data"]["domainData"]["name"] + .as_str() + .unwrap() + .to_owned() +} + +async fn clerk_proposal( + fixture: &client_http::ClientFixture, + proposal_id: &str, +) -> (Value, String) { + let (status, proposal, etag) = send( + &fixture.http, + Method::GET, + &format!("/v1/proposals/{proposal_id}?accessProfile=clerk"), + Value::Null, + None, + None, + ) + .await; + assert_eq!(status, StatusCode::OK, "{proposal}"); + (proposal, etag.unwrap()) +} + +async fn accept_review( + fixture: &client_http::ClientFixture, + proposal_id: &str, +) -> registry_review_client::ReviewRequestAccepted { + let id = uuid::Uuid::parse_str(proposal_id).unwrap(); + let row = fixture + .database + .admin + .query_one( + "SELECT create_request,expected_policy,expected_submission_digest + FROM registry_internal.registry_proposal_review_submissions WHERE proposal_id=$1", + &[&id], + ) + .await + .unwrap(); + let create: registry_review_client::ReviewCreateRequest = + serde_json::from_value(row.get(0)).unwrap(); + let accepted = registry_review_client::ReviewRequestAccepted { + request_id: uuid::Uuid::new_v4(), + subject: create.subject, + policy: serde_json::from_value(row.get(1)).unwrap(), + submission_digest: registry_review_client::SubmissionDigest::parse( + &row.get::<_, String>(2), + ) + .unwrap(), + }; + fixture + .database + .admin + .execute( + "UPDATE registry_internal.registry_proposal_review_submissions + SET state='accepted',accepted_binding=$2 WHERE proposal_id=$1", + &[&id, &serde_json::to_value(&accepted).unwrap()], + ) + .await + .unwrap(); + accepted +} + +fn apply_body(submitted: &Value) -> Value { + json!({ + "submissionVersion":submitted["submissionVersion"], + "effectDigest":submitted["effectDigest"] + }) +} + +async fn wait_until_epoch(deadline: i64) { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + tokio::time::sleep(Duration::from_millis( + (u128::try_from(deadline).unwrap() * 1_000).saturating_sub(now) as u64 + 50, + )) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn automatic_apply_requires_active_retained_task_authority() { + let status = status(); + let checker = status.clone(); + let fixture = client_http::ClientFixture::start_with_mutations( + registry(true, false), + clerk(), + move |service| service.with_task_status(checker), + ) + .await; + let target = create_target(&fixture, "automatic-revoked").await; + let idp = MockIdp::start().await; + let auth = authenticator(&fixture.registry, &idp).await; + let (proposal_id, _, _) = + submit_with_task(&fixture, &auth, &idp, &target, "automatic-revoked").await; + let before = status.calls.load(Ordering::SeqCst); + status.refused_grant.store(2, Ordering::SeqCst); + status.mode.store(REFUSED, Ordering::SeqCst); + + assert!(fixture + .mutations + .run_automatic_proposal_once_for_test() + .await + .unwrap()); + let row = fixture + .database + .admin + .query_one( + "SELECT state,attempt_count FROM registry_internal.registry_proposal_application_jobs + WHERE proposal_id=$1", + &[&uuid::Uuid::parse_str(&proposal_id).unwrap()], + ) + .await + .unwrap(); + assert_eq!(status.calls.load(Ordering::SeqCst), before + 1); + assert_eq!(row.get::<_, String>(0), "blocked"); + assert_eq!(row.get::<_, i32>(1), 1); + assert_eq!(read_target_name(&fixture, &target).await, "Before"); + fixture.finish().await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn separately_authorized_manual_apply_requires_active_retained_task_authority() { + let status = status(); + let checker = status.clone(); + let fixture = client_http::ClientFixture::start_with_mutations( + registry(false, false), + clerk(), + move |service| service.with_task_status(checker), + ) + .await; + let target = create_target(&fixture, "manual-revoked").await; + let idp = MockIdp::start().await; + let auth = authenticator(&fixture.registry, &idp).await; + let (proposal_id, submitted, _) = + submit_with_task(&fixture, &auth, &idp, &target, "manual-revoked").await; + let (_, etag) = clerk_proposal(&fixture, &proposal_id).await; + let before = status.calls.load(Ordering::SeqCst); + status.refused_grant.store(2, Ordering::SeqCst); + status.mode.store(REFUSED, Ordering::SeqCst); + + let (apply_status, refused, _) = send( + &fixture.http, + Method::POST, + &format!("/v1/proposals/{proposal_id}/apply?accessProfile=clerk"), + apply_body(&submitted), + Some("manual-revoked-apply"), + Some(&etag), + ) + .await; + assert_eq!(apply_status, StatusCode::PRECONDITION_FAILED, "{refused}"); + assert_eq!(status.calls.load(Ordering::SeqCst), before + 1); + assert_eq!(read_target_name(&fixture, &target).await, "Before"); + fixture.finish().await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn reviewed_manual_apply_checks_retained_authority_before_review_evidence() { + let status = status(); + let checker = status.clone(); + let review = Arc::new(ApprovedReview { + calls: AtomicUsize::new(0), + result_id: uuid::Uuid::new_v4(), + }); + let review_source = review.clone(); + let fixture = client_http::ClientFixture::start_with_mutations( + registry(false, true), + clerk(), + move |service| { + service + .with_task_status(checker) + .with_proposal_review_substitute_for_test(review_binding(), review_source) + }, + ) + .await; + let target = create_target(&fixture, "reviewed-revoked").await; + let idp = MockIdp::start().await; + let auth = authenticator(&fixture.registry, &idp).await; + let (proposal_id, submitted, _) = + submit_with_task(&fixture, &auth, &idp, &target, "reviewed-revoked").await; + accept_review(&fixture, &proposal_id).await; + let (_, etag) = clerk_proposal(&fixture, &proposal_id).await; + let before = status.calls.load(Ordering::SeqCst); + status.refused_grant.store(2, Ordering::SeqCst); + status.mode.store(REFUSED, Ordering::SeqCst); + + let (apply_status, refused, _) = send( + &fixture.http, + Method::POST, + &format!("/v1/proposals/{proposal_id}/apply?accessProfile=clerk"), + apply_body(&submitted), + Some("reviewed-revoked-apply"), + Some(&etag), + ) + .await; + assert_eq!(apply_status, StatusCode::PRECONDITION_FAILED, "{refused}"); + assert_eq!(status.calls.load(Ordering::SeqCst), before + 1); + assert_eq!( + review.calls.load(Ordering::SeqCst), + 0, + "review evidence was disclosed after retained task authority failed" + ); + assert_eq!(read_target_name(&fixture, &target).await, "Before"); + fixture.finish().await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn automatic_apply_rechecks_status_for_each_retry() { + let status = status(); + let checker = status.clone(); + let fixture = client_http::ClientFixture::start_with_mutations( + registry(true, false), + clerk(), + move |service| service.with_task_status(checker), + ) + .await; + let target = create_target(&fixture, "automatic-retry").await; + let idp = MockIdp::start().await; + let auth = authenticator(&fixture.registry, &idp).await; + let (proposal_id, _, _) = + submit_with_task(&fixture, &auth, &idp, &target, "automatic-retry").await; + let before = status.calls.load(Ordering::SeqCst); + + status.mode.store(UNAVAILABLE, Ordering::SeqCst); + assert!(fixture + .mutations + .run_automatic_proposal_once_for_test() + .await + .unwrap()); + let after_outage = status.calls.load(Ordering::SeqCst); + assert_eq!(after_outage, before + 1); + assert_eq!(read_target_name(&fixture, &target).await, "Before"); + + status.mode.store(ACTIVE, Ordering::SeqCst); + fixture + .database + .admin + .execute( + "UPDATE registry_internal.registry_proposal_application_jobs + SET next_attempt_at=clock_timestamp() WHERE proposal_id=$1", + &[&uuid::Uuid::parse_str(&proposal_id).unwrap()], + ) + .await + .unwrap(); + assert!(fixture + .mutations + .run_automatic_proposal_once_for_test() + .await + .unwrap()); + assert_eq!(status.calls.load(Ordering::SeqCst), after_outage + 1); + assert_eq!(read_target_name(&fixture, &target).await, "After"); + fixture.finish().await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn automatic_apply_rechecks_retained_deadline_before_effects() { + let status = status(); + let checker = status.clone(); + let fixture = client_http::ClientFixture::start_with_mutations( + registry(true, false), + clerk(), + move |service| service.with_task_status(checker), + ) + .await; + let target = create_target(&fixture, "automatic-expired").await; + let idp = MockIdp::start().await; + let auth = authenticator(&fixture.registry, &idp).await; + let (proposal_id, _, _) = + submit_with_task(&fixture, &auth, &idp, &target, "automatic-expired").await; + fixture.database.admin.execute( + "UPDATE registry_internal.registry_proposal_task_authority + SET binding=jsonb_set(binding,'{expiresAt}',to_jsonb(extract(epoch FROM clock_timestamp())::bigint-1)) + WHERE proposal_id=$1", + &[&uuid::Uuid::parse_str(&proposal_id).unwrap()], + ).await.unwrap(); + + assert!(fixture + .mutations + .run_automatic_proposal_once_for_test() + .await + .unwrap()); + assert_eq!(read_target_name(&fixture, &target).await, "Before"); + fixture.finish().await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn retained_deadline_is_rechecked_after_live_status_before_effects() { + let status = status(); + let checker = status.clone(); + let fixture = client_http::ClientFixture::start_with_mutations( + registry(true, false), + clerk(), + move |service| service.with_task_status(checker), + ) + .await; + let target = create_target(&fixture, "deadline-during-status").await; + let idp = MockIdp::start().await; + let auth = authenticator(&fixture.registry, &idp).await; + let (proposal_id, _, _) = + submit_with_task(&fixture, &auth, &idp, &target, "deadline-during-status").await; + let retained_deadline: i64 = fixture + .database + .admin + .query_one( + "UPDATE registry_internal.registry_proposal_task_authority + SET binding=jsonb_set(binding,'{expiresAt}',to_jsonb(extract(epoch FROM clock_timestamp())::bigint+3)) + WHERE proposal_id=$1 RETURNING (binding->>'expiresAt')::bigint", + &[&uuid::Uuid::parse_str(&proposal_id).unwrap()], + ) + .await + .unwrap() + .get(0); + status.pause.store(true, Ordering::SeqCst); + + let apply = fixture.mutations.run_automatic_proposal_once_for_test(); + let expire = async { + tokio::time::timeout(Duration::from_secs(2), status.entered.notified()) + .await + .expect("automatic Apply checks the retained grant"); + wait_until_epoch(retained_deadline).await; + status.release.notify_one(); + }; + let (worker, ()) = tokio::join!(apply, expire); + assert!(worker.unwrap()); + assert_eq!(read_target_name(&fixture, &target).await, "Before"); + fixture.finish().await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn exact_retained_binding_is_rechecked_after_live_status() { + let status = status(); + let checker = status.clone(); + let fixture = client_http::ClientFixture::start_with_mutations( + registry(true, false), + clerk(), + move |service| service.with_task_status(checker), + ) + .await; + let target = create_target(&fixture, "binding-substitution").await; + let idp = MockIdp::start().await; + let auth = authenticator(&fixture.registry, &idp).await; + let (proposal_id, _, _) = + submit_with_task(&fixture, &auth, &idp, &target, "binding-substitution").await; + status.pause.store(true, Ordering::SeqCst); + + let apply = fixture.mutations.run_automatic_proposal_once_for_test(); + let substitute = async { + tokio::time::timeout(Duration::from_secs(2), status.entered.notified()) + .await + .expect("automatic Apply checks the retained grant"); + let proposal_uuid = uuid::Uuid::parse_str(&proposal_id).unwrap(); + let replacement_grant = grant_id(99); + fixture + .database + .admin + .execute( + "UPDATE registry_internal.registry_proposal_task_authority + SET binding=jsonb_set(binding,'{grantId}',to_jsonb($2::text)) + WHERE proposal_id=$1", + &[&proposal_uuid, &replacement_grant], + ) + .await + .unwrap(); + status.release.notify_one(); + }; + let (worker, ()) = tokio::join!(apply, substitute); + assert!(worker.unwrap()); + let job = fixture + .database + .admin + .query_one( + "SELECT state,last_error_code FROM registry_internal.registry_proposal_application_jobs + WHERE proposal_id=$1", + &[&uuid::Uuid::parse_str(&proposal_id).unwrap()], + ) + .await + .unwrap(); + assert_eq!(job.get::<_, String>(0), "blocked"); + assert_eq!( + job.get::<_, Option>(1).as_deref(), + Some("application-refused") + ); + assert_eq!(read_target_name(&fixture, &target).await, "Before"); + fixture.finish().await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn retained_deadline_is_rechecked_after_slow_effect_before_commit() { + let status = status(); + let checker = status.clone(); + let fixture = client_http::ClientFixture::start_with_mutations( + registry(true, false), + clerk(), + move |service| service.with_task_status(checker), + ) + .await; + let target = create_target(&fixture, "deadline-during-effect").await; + let idp = MockIdp::start().await; + let auth = authenticator(&fixture.registry, &idp).await; + let (proposal_id, _, _) = + submit_with_task(&fixture, &auth, &idp, &target, "deadline-during-effect").await; + let retained_deadline: i64 = fixture + .database + .admin + .query_one( + "UPDATE registry_internal.registry_proposal_task_authority + SET binding=jsonb_set(binding,'{expiresAt}',to_jsonb(extract(epoch FROM clock_timestamp())::bigint+3)) + WHERE proposal_id=$1 RETURNING (binding->>'expiresAt')::bigint", + &[&uuid::Uuid::parse_str(&proposal_id).unwrap()], + ) + .await + .unwrap() + .get(0); + let table = &fixture.registry.entities()["target"].physical_table; + fixture.database.admin.batch_execute(&format!( + "CREATE FUNCTION registry_data.pause_task_authority_effect() RETURNS trigger LANGUAGE plpgsql AS $body$ + BEGIN + PERFORM set_config('lock_timeout','10s',true); + PERFORM pg_advisory_xact_lock(732995002); + RETURN NEW; + END $body$; + CREATE TRIGGER pause_task_authority_effect BEFORE UPDATE ON registry_data.\"{table}\" + FOR EACH ROW EXECUTE FUNCTION registry_data.pause_task_authority_effect()" + )).await.unwrap(); + let (barrier, barrier_task) = fixture.database.connect_migration().await; + barrier + .execute("SELECT pg_advisory_lock(732995002)", &[]) + .await + .unwrap(); + + let mutations = fixture.mutations.clone(); + let applying = + tokio::spawn(async move { mutations.run_automatic_proposal_once_for_test().await }); + tokio::time::timeout(Duration::from_secs(5), async { + loop { + let waiting: i64 = fixture + .database + .admin + .query_one( + "SELECT count(*) FROM pg_locks WHERE locktype='advisory' AND NOT granted + AND database=(SELECT oid FROM pg_database WHERE datname=current_database())", + &[], + ) + .await + .unwrap() + .get(0); + if waiting != 0 { + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("automatic Apply reached the effect after retained status and admission"); + wait_until_epoch(retained_deadline).await; + barrier + .execute("SELECT pg_advisory_unlock(732995002)", &[]) + .await + .unwrap(); + barrier_task.abort(); + + assert!(applying.await.unwrap().unwrap()); + let job = fixture + .database + .admin + .query_one( + "SELECT state,last_error_code FROM registry_internal.registry_proposal_application_jobs + WHERE proposal_id=$1", + &[&uuid::Uuid::parse_str(&proposal_id).unwrap()], + ) + .await + .unwrap(); + assert_eq!(job.get::<_, String>(0), "blocked"); + assert_eq!( + job.get::<_, Option>(1).as_deref(), + Some("application-refused"), + "the retained deadline fence, not a lock or dependency timeout, refused Apply" + ); + assert_eq!(read_target_name(&fixture, &target).await, "Before"); + fixture.finish().await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn completed_apply_receipt_and_owner_cancel_do_not_require_retained_status() { + let status = status(); + let checker = status.clone(); + let fixture = client_http::ClientFixture::start_with_mutations( + registry(false, false), + clerk(), + move |service| service.with_task_status(checker), + ) + .await; + let idp = MockIdp::start().await; + let auth = authenticator(&fixture.registry, &idp).await; + + let applied_target = create_target(&fixture, "receipt").await; + let (applied_id, submitted, _) = + submit_with_task(&fixture, &auth, &idp, &applied_target, "receipt").await; + let (_, etag) = clerk_proposal(&fixture, &applied_id).await; + let apply_path = format!("/v1/proposals/{applied_id}/apply?accessProfile=clerk"); + let body = apply_body(&submitted); + let (apply_status, applied, _) = send( + &fixture.http, + Method::POST, + &apply_path, + body.clone(), + Some("receipt-apply"), + Some(&etag), + ) + .await; + assert_eq!(apply_status, StatusCode::OK, "{applied}"); + + let cancelled_target = create_target(&fixture, "cancel").await; + let (cancelled_id, _, _) = + submit_with_task(&fixture, &auth, &idp, &cancelled_target, "cancel").await; + let (_, cancelled_etag) = clerk_proposal(&fixture, &cancelled_id).await; + status.refused_grant.store(2, Ordering::SeqCst); + status.mode.store(REFUSED, Ordering::SeqCst); + let calls_before_exceptions = status.calls.load(Ordering::SeqCst); + + let (replay_status, replay, _) = send( + &fixture.http, + Method::POST, + &apply_path, + body, + Some("receipt-apply"), + Some(&etag), + ) + .await; + assert_eq!(replay_status, StatusCode::OK, "{replay}"); + let (cancel_status, cancelled, _) = send( + &fixture.http, + Method::POST, + &format!("/v1/proposals/{cancelled_id}/cancel?accessProfile=clerk"), + json!({}), + Some("owner-cancel"), + Some(&cancelled_etag), + ) + .await; + assert_eq!(cancel_status, StatusCode::OK, "{cancelled}"); + assert_eq!(cancelled["state"], "cancelled"); + assert_eq!(status.calls.load(Ordering::SeqCst), calls_before_exceptions); + fixture.finish().await; +} diff --git a/crates/registry-bregctl/src/check.rs b/crates/registry-bregctl/src/check.rs index 3a352ef6f0..4c2e452400 100644 --- a/crates/registry-bregctl/src/check.rs +++ b/crates/registry-bregctl/src/check.rs @@ -1552,6 +1552,18 @@ fn alternatives(steps: &[Step]) -> Vec> { { under_profile("actions", "action", action, profile, rest) } + [Step::Member(actions), Step::Select { value: action, .. }, Step::Member(execution), Step::Member(proposal), Step::Member(permissions), Step::Select { + key, + value: profile, + }, rest @ ..] + if actions == "actions" + && execution == "execution" + && proposal == "proposal" + && permissions == "permissions" + && key.as_deref() == Some("profile") => + { + under_profile("proposals", "action", action, profile, rest) + } _ => Vec::new(), } } @@ -2087,4 +2099,23 @@ mod tests { "/accessProfiles/1/permissions/actions/0/targets/1/rowBoundaries" ); } + + #[test] + fn a_proposal_permission_path_is_found_under_the_profile_that_grants_it() { + let project = document( + "apiVersion: id.registrystack.org/formats/breg/project/v1alpha1\nkind: BRegProject\nproject:\n id: example\n canonicalBaseIri: https://example.invalid\n version: 0.1.0\n defaultLanguage: en\naccessProfiles:\n - id: reader\n principalClaim: registry_principal\n requiredScopes: unrestricted\n - id: steward\n principalClaim: registry_principal\n requiredScopes: unrestricted\n permissions:\n proposals:\n - action: update-record\n operations: [edit]\n targets:\n - {recordType: other, rowBoundaries: unrestricted}\n - {recordType: record, rowBoundaries: unrestricted}\n", + ); + let steps = parse_steps( + "actions[id=update-record].execution.proposal.permissions[profile=steward].targets[recordType=record].rowBoundaries", + ); + let found = alternatives(&steps) + .iter() + .map(|alternative| resolve(project.root(), alternative)) + .find(|found| found.complete) + .expect("the profile's permission names the Proposal action"); + assert_eq!( + found.pointer, + "/accessProfiles/1/permissions/proposals/0/targets/1/rowBoundaries" + ); + } } diff --git a/crates/registry-bregctl/src/explain_report.rs b/crates/registry-bregctl/src/explain_report.rs index 39603e1eab..943030852b 100644 --- a/crates/registry-bregctl/src/explain_report.rs +++ b/crates/registry-bregctl/src/explain_report.rs @@ -174,7 +174,7 @@ pub(crate) fn actions(value: &Value, lines: &mut Lines) { ), ); } - permissions(&action["permissions"], 2, lines); + action_permissions(&action["permissions"], 2, lines); if action["execution"]["mode"] == "proposal" { let proposal = &action["execution"]["proposal"]; let review = &proposal["review"]; @@ -207,18 +207,135 @@ pub(crate) fn actions(value: &Value, lines: &mut Lines) { 2, "Restricted built-in executor holds the compiled system apply grant.", ); + system_apply_grant(&proposal["systemApplyGrant"], 2, lines); } - permissions(&proposal["permissions"], 2, lines); + proposal_permissions(&proposal["permissions"], &action["inputs"], 2, lines); } } } -fn permissions(value: &Value, depth: usize, lines: &mut Lines) { +pub(crate) fn json_list_or(value: &Value, empty: &str) -> String { + if value.as_array().is_some_and(|values| !values.is_empty()) { + value.to_string() + } else { + empty.to_owned() + } +} + +fn row_restrictions(value: &Value) -> String { + if value.is_null() + || value.as_array().is_some_and(Vec::is_empty) + || value.as_str() == Some("unrestricted") + { + "unrestricted".to_owned() + } else { + value.to_string() + } +} + +fn permission_gates(permission: &Value) -> Vec<(&'static str, String)> { + let mut gates = vec![ + ( + "actor kind", + permission["actorKind"] + .as_str() + .unwrap_or("not declared") + .to_owned(), + ), + ( + "requester clients", + json_list_or(&permission["requesterClients"], "any"), + ), + ( + "principal claim", + permission["principalClaim"] + .as_str() + .unwrap_or("none") + .to_owned(), + ), + ( + "required scopes (all)", + json_list_or(&permission["requiredScopes"], "unrestricted"), + ), + ( + "allowed purposes (any)", + json_list_or(&permission["requiredPurposes"], "unrestricted"), + ), + ]; + if permission["taskGrant"].is_object() { + gates.push(( + "task grant source issuer", + text(&permission["taskGrant"]["sourceIssuer"]).to_owned(), + )); + if permission["taskGrant"]["permissions"] + .as_array() + .is_some_and(|permissions| !permissions.is_empty()) + { + let permissions = items(&permission["taskGrant"]["permissions"]) + .map(|grant| { + format!( + "{}: {}", + text(&grant["collection"]), + names(&grant["operations"], "id") + ) + }) + .collect::>() + .join("; "); + gates.push(("task grant permissions", permissions)); + } + } else { + gates.push(("task grant", "none".to_owned())); + } + gates +} + +fn action_permissions(value: &Value, depth: usize, lines: &mut Lines) { + for permission in items(value) { + lines.listed( + depth, + &format!( + "Action profile {}: {}{}", + text(&permission["profileId"]), + names(&permission["operations"], "id"), + if permission["default"] == true { + " (default)" + } else { + "" + } + ), + ); + lines.pairs_at(depth + 1, &permission_gates(permission)); + lines.pairs_at( + depth + 1, + &[("results", names(&permission["results"], "id"))], + ); + for target in items(&permission["targets"]) { + lines.listed( + depth + 1, + &format!( + "target {}: {} from {}", + text(&target["recordType"]), + text(&target["operation"]), + target["source"] + ), + ); + lines.pairs_at( + depth + 2, + &[( + "row restrictions (all)", + row_restrictions(&target["rowBoundaries"]), + )], + ); + } + } +} + +fn proposal_permissions(value: &Value, action_inputs: &Value, depth: usize, lines: &mut Lines) { for permission in items(value) { lines.listed( depth, &format!( - "profile {}: {}{}", + "Proposal profile {}: {}{}", text(&permission["profileId"]), names(&permission["operations"], "id"), if permission["default"] == true { @@ -228,6 +345,85 @@ fn permissions(value: &Value, depth: usize, lines: &mut Lines) { } ), ); + let mut fields = permission_gates(permission); + fields.extend([ + ( + "ownership", + permission["ownership"] + .as_str() + .unwrap_or("unrestricted") + .to_owned(), + ), + ( + "readable context fields", + names(&permission["readableContextFields"], "id"), + ), + ( + "readable inputs", + names(&permission["readableInputs"], "id"), + ), + ( + "writable inputs", + if permission.get("writableInputs").is_none() + || permission["writableInputs"].is_null() + { + format!("all Action inputs ({})", names(action_inputs, "id")) + } else { + names(&permission["writableInputs"], "id") + }, + ), + ( + "readable attachments", + names(&permission["readableAttachments"], "id"), + ), + ( + "writable attachments", + names(&permission["writableAttachments"], "id"), + ), + ]); + lines.pairs_at(depth + 1, &fields); + for target in items(&permission["targets"]) { + lines.listed( + depth + 1, + &format!("target {}", text(&target["recordType"])), + ); + lines.pairs_at( + depth + 2, + &[( + "row restrictions (all)", + row_restrictions(&target["rowBoundaries"]), + )], + ); + } + } +} + +fn system_apply_grant(grant: &Value, depth: usize, lines: &mut Lines) { + lines.item_at(depth, "system apply grant"); + lines.pairs_at( + depth + 1, + &[ + ("action", text(&grant["actionId"]).to_owned()), + ("action revision", text(&grant["actionRevision"]).to_owned()), + ("grant revision", text(&grant["revision"]).to_owned()), + ], + ); + for target in items(&grant["targets"]) { + lines.listed( + depth + 1, + &format!( + "{} {}", + text(&target["operation"]), + text(&target["recordType"]) + ), + ); + lines.pairs_at( + depth + 2, + &[ + ("writable fields", names(&target["fields"], "id")), + ("readable fields", names(&target["readFields"], "id")), + ], + ); } } @@ -405,6 +601,57 @@ mod tests { use super::*; use serde_json::json; + #[test] + fn permission_gates_quote_literal_identifiers_and_label_empty_lists() { + for identifiers in [ + json!(["none"]), + json!(["any"]), + json!(["unrestricted"]), + json!(["none", "any", "unrestricted"]), + ] { + let expected = identifiers.to_string(); + let literal = permission_gates(&json!({ + "requesterClients": identifiers.clone(), + "requiredScopes": identifiers.clone(), + "requiredPurposes": identifiers + })); + for label in [ + "requester clients", + "required scopes (all)", + "allowed purposes (any)", + ] { + assert_eq!( + literal.iter().find_map( + |(candidate, value)| (*candidate == label).then_some(value.as_str()) + ), + Some(expected.as_str()) + ); + } + } + + for empty in [ + permission_gates(&json!({ + "requesterClients": [], + "requiredScopes": [], + "requiredPurposes": [] + })), + permission_gates(&json!({})), + ] { + for (label, expected) in [ + ("requester clients", "any"), + ("required scopes (all)", "unrestricted"), + ("allowed purposes (any)", "unrestricted"), + ] { + assert_eq!( + empty.iter().find_map( + |(candidate, value)| (*candidate == label).then_some(value.as_str()) + ), + Some(expected) + ); + } + } + } + #[test] fn lifecycle_summary_renders_every_possible_turn_and_its_condition() { let value = json!({"lifecycles": [registry_breg::lifecycle::proposal_lifecycle()]}); diff --git a/crates/registry-bregctl/src/lib.rs b/crates/registry-bregctl/src/lib.rs index 781ae244ba..f5914f7201 100644 --- a/crates/registry-bregctl/src/lib.rs +++ b/crates/registry-bregctl/src/lib.rs @@ -10924,6 +10924,9 @@ fn push_access_explanation(explanation: &Value, lines: &mut report::Lines) { "purposeMatching", "rowMatching", "profileSelection", + "relationshipMatching", + "missingClaims", + "evaluation", ] { let sentence = explanation[key].as_str().unwrap_or(""); if !sentence.is_empty() { @@ -10957,6 +10960,7 @@ fn push_access_explanation(explanation: &Value, lines: &mut report::Lines) { } } explain_report::actions(&explanation["actions"]["actions"], lines); + push_row_reach(&explanation["rowReach"], lines); explain_report::datasets(&explanation["statisticalDatasets"], lines); if explanation["consent"].is_object() { lines.blank(); @@ -10964,6 +10968,101 @@ fn push_access_explanation(explanation: &Value, lines: &mut report::Lines) { } } +fn push_row_reach(row_reach: &Value, lines: &mut report::Lines) { + let Some(entries) = row_reach.as_array().filter(|entries| !entries.is_empty()) else { + return; + }; + lines.heading("Target row reach:"); + for reach in entries { + lines.item_at( + 1, + &format!( + "{} as {}", + reach["recordType"].as_str().unwrap_or(""), + reach["profile"].as_str().unwrap_or("") + ), + ); + let mut fields = vec![ + ( + "surface", + reach["surface"].as_str().unwrap_or("").to_owned(), + ), + ("rows", reach["rows"].as_str().unwrap_or("").to_owned()), + ( + "configuration path", + reach["sourcePath"].as_str().unwrap_or("").to_owned(), + ), + ]; + if reach["surface"] == "proposal_target" { + fields.push(( + "Proposal ownership", + if reach["ownerOnly"] == true { + "caller only".to_owned() + } else { + "unrestricted".to_owned() + }, + )); + } + if reach["rowBoundaries"] + .as_array() + .is_some_and(|boundaries| !boundaries.is_empty()) + { + fields.push(("row restrictions (all)", reach["rowBoundaries"].to_string())); + } + if reach["membershipBoundaries"] + .as_array() + .is_some_and(|boundaries| !boundaries.is_empty()) + { + fields.push(( + "membership restrictions (all)", + reach["membershipBoundaries"].to_string(), + )); + } + let fields = fields + .iter() + .map(|(label, value)| (*label, value.clone())) + .collect::>(); + lines.pairs_at(2, &fields); + if reach["actionTargetAuthority"].is_object() { + let authority = &reach["actionTargetAuthority"]; + lines.pairs_at( + 2, + &[ + ( + "Action target enforcement", + authority["enforcement"].as_str().unwrap_or("").to_owned(), + ), + ( + "direct Record grant relationship", + authority["recordGrantRelationship"] + .as_str() + .unwrap_or("") + .to_owned(), + ), + ( + "same-profile direct Record grant", + authority["hasDirectRecordGrant"] + .as_bool() + .unwrap_or(false) + .to_string(), + ), + ], + ); + for use_ in authority["uses"].as_array().into_iter().flatten() { + lines.listed( + 2, + &format!( + "target use {}: {} from {}", + use_["role"].as_str().unwrap_or(""), + use_["operation"].as_str().unwrap_or(""), + use_["source"] + ), + ); + } + } + } +} + /// The `module add consent` explanation: what the generated module holds, the /// vocabularies it added versus reused, the `requireConsent` line each gated /// entity still needs, and whether the project compiles yet. @@ -11204,13 +11303,52 @@ fn push_access_profile(profile: &Value, depth: usize, lines: &mut report::Lines) depth, &format!("profile {}", profile["id"].as_str().unwrap_or("")), ); - let mut fields = vec![( - "principal claim", - profile["principalClaim"] - .as_str() - .unwrap_or("none") - .to_owned(), - )]; + let mut fields = vec![ + ( + "actor kind", + profile["actorKind"] + .as_str() + .unwrap_or("not declared") + .to_owned(), + ), + ( + "requester clients", + explain_report::json_list_or(&profile["requesterClients"], "any"), + ), + ( + "principal claim", + profile["principalClaim"] + .as_str() + .unwrap_or("none") + .to_owned(), + ), + ]; + if profile["taskGrant"].is_object() { + fields.push(( + "task grant source issuer", + profile["taskGrant"]["sourceIssuer"] + .as_str() + .unwrap_or("") + .to_owned(), + )); + let permissions = profile["taskGrant"]["permissions"] + .as_array() + .into_iter() + .flatten() + .map(|permission| { + format!( + "{}: {}", + permission["collection"].as_str().unwrap_or(""), + joined_or(&permission["operations"], "none") + ) + }) + .collect::>(); + if !permissions.is_empty() { + fields.push(("task grant permissions", permissions.join("; "))); + } + } else { + fields.push(("task grant", "none".to_owned())); + } let membership_restricted = profile["membershipBoundaries"] .as_array() .is_some_and(|boundaries| !boundaries.is_empty()); diff --git a/crates/registry-bregctl/tests/cli.rs b/crates/registry-bregctl/tests/cli.rs index 37b2994f8d..0036fdd05c 100644 --- a/crates/registry-bregctl/tests/cli.rs +++ b/crates/registry-bregctl/tests/cli.rs @@ -4867,6 +4867,209 @@ fn explain_access_includes_action_only_grants_and_target_reach() { .any(|finding| finding["code"] == "breg.access.target-unrestricted-rows")); } +#[test] +fn explain_access_human_reports_profile_gates_permissions_and_target_row_reach() { + let mut source = registry_platform_yaml::Reader::new("registry.yaml") + .scan(include_bytes!( + "../../../products/breg/examples/minimal/registry.yaml" + )) + .unwrap() + .unwrap() + .to_json_value(); + source["accessProfiles"][0]["actorKind"] = json!("agent"); + source["accessProfiles"][0]["requesterClients"] = json!(["task-worker"]); + source["accessProfiles"][0]["taskGrant"] = json!({"sourceIssuer":"https://casework.example"}); + source["accessProfiles"][0]["permissions"]["records"][0]["operations"] = json!(["get", "list"]); + source["accessProfiles"][0]["permissions"]["records"][0]["writableFields"] = json!([]); + let project = + TestProject::from_registry_source(serde_norway::to_string(&source).unwrap().as_bytes()); + + let output = bregctl(&["explain", "access", path(project.path())]); + + assert!(output.status.success(), "{output:?}"); + let human = String::from_utf8(output.stdout).expect("access report is UTF-8"); + let aligned = human.split_whitespace().collect::>().join(" "); + let record_profiles = human + .split("Actions:") + .next() + .expect("Record profiles precede Actions"); + let aligned_record_profiles = record_profiles + .split_whitespace() + .collect::>() + .join(" "); + assert!( + aligned_record_profiles.contains( + "profile directory-editor actor kind agent requester clients [\"task-worker\"]" + ), + "{human}" + ); + for expected in [ + "profile directory-editor", + "actor kind agent", + "requester clients [\"task-worker\"]", + "task grant source issuer https://casework.example", + "Action profile directory-editor", + "required scopes (all) [\"registry:directory:write\"]", + "target contact: patch", + "results contact-email", + "Proposal profile directory-editor", + "ownership caller", + "readable inputs email, jurisdiction, name", + "writable inputs all Action inputs", + "Target row reach:", + "surface record", + "surface action_target", + "surface proposal_target", + "rows claim_bound", + ] { + assert!(aligned.contains(expected), "{expected:?} in {human}"); + } + + let row_reach = human + .split("Target row reach:") + .nth(1) + .expect("the human report has a row-reach section"); + let mut blocks = Vec::>::new(); + for line in row_reach.lines() { + if line.starts_with(" ") && !line.starts_with(" ") { + blocks.push(vec![line]); + } else if line.starts_with(" ") { + if let Some(block) = blocks.last_mut() { + block.push(line); + } + } + } + let blocks = blocks + .iter() + .map(|block| { + block + .join(" ") + .split_whitespace() + .collect::>() + .join(" ") + }) + .collect::>(); + for surface in ["record", "action_target"] { + let block = blocks + .iter() + .find(|block| block.contains(&format!("surface {surface}"))) + .unwrap_or_else(|| panic!("missing {surface} block in {human}")); + assert!(!block.contains("Proposal ownership"), "{block}"); + } + let proposal = blocks + .iter() + .find(|block| block.contains("surface proposal_target")) + .unwrap_or_else(|| panic!("missing Proposal target block in {human}")); + assert!( + proposal.contains("Proposal ownership caller only"), + "{proposal}" + ); +} + +#[test] +fn explain_access_human_distinguishes_proposal_ownership_and_writable_inputs() { + let source = registry_platform_yaml::Reader::new("registry.yaml") + .scan(include_bytes!( + "../../../products/breg/examples/minimal/registry.yaml" + )) + .unwrap() + .unwrap() + .to_json_value(); + let baseline = + TestProject::from_registry_source(serde_norway::to_string(&source).unwrap().as_bytes()); + let baseline = bregctl(&["explain", "access", path(baseline.path())]); + assert!(baseline.status.success(), "{baseline:?}"); + let baseline = String::from_utf8(baseline.stdout) + .unwrap() + .split_whitespace() + .collect::>() + .join(" "); + assert!(baseline.contains("ownership caller"), "{baseline}"); + assert!( + baseline.contains("writable inputs all Action inputs"), + "{baseline}" + ); + + let mut narrowed = source; + let permission = &mut narrowed["accessProfiles"][0]["permissions"]["proposals"][0]; + permission.as_object_mut().unwrap().remove("ownership"); + permission["writableInputs"] = json!(["email"]); + let narrowed = + TestProject::from_registry_source(serde_norway::to_string(&narrowed).unwrap().as_bytes()); + let narrowed = bregctl(&["explain", "access", path(narrowed.path())]); + assert!(narrowed.status.success(), "{narrowed:?}"); + let narrowed = String::from_utf8(narrowed.stdout) + .unwrap() + .split_whitespace() + .collect::>() + .join(" "); + assert!(narrowed.contains("ownership unrestricted"), "{narrowed}"); + assert!(narrowed.contains("writable inputs email"), "{narrowed}"); + assert_ne!(baseline, narrowed); +} + +#[test] +fn explain_access_human_reports_attachment_and_system_apply_authority() { + let project = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../products/breg/examples/facility") + .canonicalize() + .unwrap(); + + let output = bregctl(&["explain", "access", path(&project)]); + + assert!(output.status.success(), "{output:?}"); + let human = String::from_utf8(output.stdout).expect("access report is UTF-8"); + let aligned = human.split_whitespace().collect::>().join(" "); + for expected in [ + "Proposal profile facility-correction-author", + "readable attachments supporting-document", + "writable attachments supporting-document", + "system apply grant", + "action correct-address", + "patch facility", + "writable fields address-line, city, postal-code", + "readable fields address-line, city, postal-code", + ] { + assert!(aligned.contains(expected), "{expected:?} in {human}"); + } +} + +#[test] +fn proposal_ownership_does_not_suppress_an_unrestricted_target_warning() { + let mut source = registry_platform_yaml::Reader::new("registry.yaml") + .scan(include_bytes!( + "../../../products/breg/examples/minimal/registry.yaml" + )) + .unwrap() + .unwrap() + .to_json_value(); + source["accessProfiles"][0]["permissions"]["proposals"][0]["targets"][0]["rowBoundaries"] = + json!("unrestricted"); + let project = + TestProject::from_registry_source(serde_norway::to_string(&source).unwrap().as_bytes()); + + let output = bregctl(&["--format", "json", "check", path(project.path())]); + + assert!(output.status.success(), "{output:?}"); + let report = json_stdout(&output); + let finding = report["diagnostics"] + .as_array() + .unwrap() + .iter() + .find(|finding| finding["code"] == "breg.access.target-unrestricted-rows") + .unwrap_or_else(|| panic!("missing unrestricted target diagnostic in {report:#}")); + assert_eq!( + finding["path"], + "/accessProfiles/0/permissions/proposals/0/targets/0/rowBoundaries" + ); + assert!( + finding["message"] + .as_str() + .is_some_and(|message| message.contains("Review this registry-wide target authority")), + "{finding:#}" + ); +} + #[test] fn explain_actions_reports_the_normalized_ordered_plan() { let project = TestProject::from_registry_source(action_fixture()); diff --git a/docs/site/src/content/docs/configure/breg-access.mdx b/docs/site/src/content/docs/configure/breg-access.mdx index 3a0fc4bc7d..fd1d4b83c5 100644 --- a/docs/site/src/content/docs/configure/breg-access.mdx +++ b/docs/site/src/content/docs/configure/breg-access.mdx @@ -273,14 +273,21 @@ bregctl explain access facility allowed purposes (any) ["facility-administration"] ``` -The report lists every profile under each Record type with its fields and row restrictions. Each +The human report shows the actor kind, requester clients and task-grant requirements beside +each profile's fields and row restrictions. Action and Proposal permissions include their target +rows, inputs, attachments and ownership. Omitted ownership and writable-input defaults are +explicit. Caller ownership limits which Proposal documents can be managed; it does not bound +the target Records. The report also shows the built-in executor's system application ceiling. Each read-path grant names its target Record type, through Record type, and route. `rowReach` also lists the target under a `read_path` surface with the root profile's row boundaries, so a review that starts from the disclosed Record type does not miss incoming paths. Add `--scenario` with a file of synthetic claims to preview a decision without a token or a database. To prove the refusals against a running registry, write [journeys](../breg-journeys/) that read as both readers. -{/* Evidence: crates/registry-bregctl/src/lib.rs, explain access; crates/registry-breg/src/access_preview.rs. */} +{/* Evidence: crates/registry-bregctl/src/lib.rs, explain access; + crates/registry-bregctl/src/explain_report.rs, actions(); + crates/registry-bregctl/tests/cli.rs, explain_access_human_reports_profile_gates_permissions_and_target_row_reach(); + crates/registry-breg/src/access_preview.rs. */} ## Next diff --git a/products/breg/CHANGELOG.md b/products/breg/CHANGELOG.md index a91271b48a..14823d4ab2 100644 --- a/products/breg/CHANGELOG.md +++ b/products/breg/CHANGELOG.md @@ -2,6 +2,19 @@ ## Unreleased +- Revalidate a submitted Proposal's retained task authority on every new + application attempt, including automatic application and separately authorized + manual Apply. Preserve completed receipt recovery and owner cancellation. +- Before a successor package activates, require a usable cancellation path for + pending Proposals: one profile must provide Cancel with Get or List. +- Human-readable access explanations include actor, client and task gates, + Action and Proposal targets, inputs, attachments, ownership and row reach. + Omitted ownership and writable-input defaults are explicit. +- Keep related-record disclosure warnings separate from Action target warnings, + and report unrestricted Proposal targets even when Proposal ownership is + restricted to the caller. +- Preserve bounded authentication-refusal warnings after a poisoned timestamp + lock, while retaining their rate limit. - A manual Proposal Apply that hits a unique constraint rolls back its Record effects and durably blocks that application. It returns the same value-free `409 mutation.conflict`, disables Apply, and leaves Rebase available to correct diff --git a/products/breg/SECURITY-REVIEW-NOTES.md b/products/breg/SECURITY-REVIEW-NOTES.md index b20ae3425a..a40af17a64 100644 --- a/products/breg/SECURITY-REVIEW-NOTES.md +++ b/products/breg/SECURITY-REVIEW-NOTES.md @@ -8,6 +8,48 @@ security-invariant matrix in `contracts/security-invariant-matrix.yaml` is the row-per-invariant baseline; this file is the narrative behind the decisions and the residuals the matrix rows do not state. +## Proposal task authority, recovery and access explanations + +A new Proposal application requires both current executor authority and the +original task authority retained for its exact submission, when one exists. +Apply preflight establishes current disclosure and the frozen submission before +loading that binding. The service observes current status outside the mutation +transaction, before acquiring Review or commit Evidence. The mutation then +reloads and compares the exact binding and checks its local deadline before +effects and at the final commit fence. Every new application attempt obtains +its own status observation. A separately authorized human executor and the +restricted built-in system executor keep their existing independent ceilings. + +An exact completed receipt returns under current disclosure without another +retained-task status request. Cancellation and a Review rejection do not need +the old submitter's grant to remain active. Status refusal is a failed +precondition; an unavailable dependency is unavailable and remains eligible for +the existing bounded worker retry. A source can revoke after the remote status +observation and before local commit: BReg and the source share no distributed +transaction. Local expiry and submission-binding checks remain enforced. + +Successor activation keeps pending Proposals inspectable and requires one +Proposal grant that can both obtain a condition, through Get or List, and +Cancel. An inspection grant still has to exist. The condition remains bound to +the selected profile, principal, purpose and target bounds. The activation +guard checks compiled permission shape; it does not prove that an operator has +provisioned a real identity satisfying the profile's claims and row bounds. + +Human access explanations render the compiled actor, client, task, target, +input, attachment and ownership gates, the system application ceiling and row +reach. Caller ownership restricts Proposal documents, not the target Records. +Read paths retain their related-disclosure finding without being classified as +Action targets. These are review aids; runtime admission remains authoritative. +Authentication refusal warnings recover a poisoned timestamp while retaining +their existing rate limit; warning-state recovery supplies no caller authority. + +The PostgreSQL `postgres_proposal_task_authority` and +`postgres_proposal_recovery_permissions` suites exercise the real public +lifecycle and worker boundaries, including successful receipt and cancellation +paths and refused activation preserving pending work. The CLI explanation and +consent-access tests cover human-readable authority and the distinct warning +surfaces. The authentication unit test pins warning recovery and rate limiting. + ## Immediate Action availability and guard refusal disclosure An application may ask whether one referenced target currently satisfies an diff --git a/products/breg/TASK_GRANTS.md b/products/breg/TASK_GRANTS.md index 07d93557a7..85fb5708c9 100644 --- a/products/breg/TASK_GRANTS.md +++ b/products/breg/TASK_GRANTS.md @@ -92,6 +92,12 @@ taskGrantStatus: statusResource: urn:casework:case-management ``` +Use a lowercase `https://` source identifier in each BReg task profile and copy +it exactly into the matching runtime entry. The shared runtime format also +accepts other absolute issuer identifiers, such as URNs; this does not make +them valid BReg profile issuers or grant authority. A source identifier selects +a configured service and is never used as an outbound URL. + `statusScope` and `statusPath` are optional. Omitted, BReg requests the `casework:grants:status` scope and calls Casework's `/v1/task-grants/{grantId}/status` route under `baseUrl`. Set them only for a @@ -125,8 +131,10 @@ original principal, client, resource, purpose, bounds, subjects, and deadline. Submission freezes the original task authority alongside the proposal version and its immutable effects. A later human reviewer remains a separate actor. -Approval and application check the original grant, including automatic -application. A database concurrency retry makes a fresh status check. +Before applying a submitted Proposal, BReg checks the original grant in +addition to the current executor's authority, including for automatic +application and separately authorized manual Apply. Every new application +attempt makes a fresh status check. An exact completed retry recovers the existing receipt under current disclosure authority. It does not perform a new mutation or require a new positive status @@ -146,10 +154,12 @@ a failed precondition. Human rejection and owner cancellation keep their existin permissions. These actions do not require the old submitter grant to remain active. Read cursors and representation ETags do not incorporate grant IDs. -The bounded status check uses the shared platform exact-status transport and -runs while the proposal is locked. Its total five-second bound includes obtaining -the status service credential; positive status is never cached. BReg keeps its -own persisted binding and BReg-only bounds validation. Deferred Casework grants +The bounded status check uses the shared platform exact-status transport +outside the mutation transaction, after preflight identifies the exact retained +submission. Its total five-second bound includes obtaining the status service +credential; positive status is never cached. The mutation transaction reloads +the retained binding and checks equality and local expiry before effects and +again checks expiry before commit. BReg keeps its own persisted binding and BReg-only bounds validation. Deferred Casework grants use the same status contract and original deadline, without changing BReg's write or human approval permissions. Casework revocation can still occur between that check and the local commit. BReg and Casework do diff --git a/products/breg/contracts/security-invariant-matrix.yaml b/products/breg/contracts/security-invariant-matrix.yaml index df5cf6f389..084c27fbb5 100644 --- a/products/breg/contracts/security-invariant-matrix.yaml +++ b/products/breg/contracts/security-invariant-matrix.yaml @@ -63,7 +63,7 @@ invariants: - {id: BREG-SEC-59, state: enforced, targetWave: W4, threat: Concurrent delegated calls cross-send bearer credentials., enforcementPoint: immutable request-scoped client configuration and shared pooled transport, refusal: Keep each derived client credential isolated without changing source-bound handles., negativeId: BREG-NEG-59, negativeTest: {path: crates/registry-breg-client/tests/http_boundary.rs, name: caller_scoped_tokens_are_isolated_across_concurrent_requests}} - {id: BREG-SEC-60, state: enforced, targetWave: W4, threat: Durable lifecycle recovery retains unrelated source content or rebinds an uncertain action., enforcementPoint: minimal versioned prepared lifecycle evidence and current metadata authority validation, refusal: Exclude record and preview content and refuse changed source or action bindings while replaying exact original intent., negativeId: BREG-NEG-60, negativeTest: {path: crates/registry-breg/tests/postgres_proposal.rs, name: prepared_lifecycle_recovery_uses_current_authority_and_the_original_exact_condition}} - {id: BREG-SEC-63, state: enforced, targetWave: W5, threat: Internal Proposal audit actor references expose raw principals or link them across databases and issuers., enforcementPoint: 'keyed Proposal actor reference scoped to source database and issuer, separate from stable internal owner identity', refusal: 'Never expose raw principals in the actor reference; vary it across databases, issuers, keys, and principals while preserving activation-independent audit correlation.', negativeId: BREG-NEG-63, negativeTest: {path: crates/registry-breg/src/mutation/proposal.rs, name: proposal_actor_is_keyed_database_and_issuer_scoped_but_stable_across_activation}} - - {id: BREG-SEC-64, state: enforced, targetWave: W5, threat: Revoked task authority commits another draft or proposal mutation or a later reviewer replaces the original authority or a retry reapplies a completed proposal., enforcementPoint: verified identity-bound HTTP claims propagated into SQL context fresh per-attempt status outside the guarded apply transaction and exact source-owned proposal authority revalidation inside that transaction before effects, refusal: Refuse revoked unavailable expired or substituted authority before new effects and each SQL retry while recovering completed receipts only under current disclosure. Revocation after the bounded status response is not cross-service atomic and remains bounded by grant expiration., negativeId: BREG-NEG-64, negativeTest: {path: crates/registry-breg/tests/postgres_proposal_security.rs, name: signed_proposal_start_and_lifecycle_receipts_reject_sibling_grants}} + - {id: BREG-SEC-64, state: enforced, targetWave: W5, threat: Revoked task authority commits another draft or proposal mutation or a later reviewer replaces the original authority or a retry reapplies a completed proposal., enforcementPoint: verified identity-bound HTTP claims propagated into SQL context fresh per-attempt status outside the guarded apply transaction and exact source-owned proposal authority revalidation inside that transaction before effects, refusal: Refuse revoked unavailable expired or substituted authority before new effects and each SQL retry while recovering completed receipts only under current disclosure. Revocation after the bounded status response is not cross-service atomic and remains bounded by grant expiration., negativeId: BREG-NEG-64, negativeTest: {path: crates/registry-breg/tests/postgres_proposal_task_authority.rs, name: automatic_apply_requires_active_retained_task_authority}} - {id: BREG-SEC-65, state: enforced, targetWave: W5, threat: 'A native action widens a frozen commit Evidence guard, accepts mismatched signed Evidence, or reacquires it during exact receipt recovery.', enforcementPoint: 'compiled commit Evidence selectors and guards, signed assertion verification, frozen candidate binding, and atomic Evidence retention', refusal: Refuse a mismatched or stale guard assertion before effects; commit retained Evidence with effects and replay committed receipts without another acquisition., negativeId: BREG-NEG-65, negativeTest: {path: crates/registry-breg/tests/postgres_action_evidence.rs, name: commit_evidence_is_guard_only_and_replay_never_reacquires}} - {id: BREG-SEC-66, state: enforced, targetWave: W5, threat: "A healthy retained local delivery blocks restart because verification treats its absent logical destination as a broken binding.", enforcementPoint: handler-kind-branched retained-binding verification over the destination and handler seams, refusal: "Verify a retained url row against its activated destination and a retained local row against the deployed reviewed program digest, refusing startup only on a genuine mismatch.", negativeId: BREG-NEG-66, negativeTest: {path: crates/registry-breg/tests/postgres_hook_changes.rs, name: real_postgres_a_retained_local_delivery_survives_restart_verification}} - {id: BREG-SEC-67, state: enforced, targetWave: W5, threat: "A changed or superseded handler answer reapplies or hides one delivery after its first application committed, its activation changed, or its lease was lost.", enforcementPoint: "delivery-scoped idempotency with the accepted answer digest, cancellation-safe activation and proposal locks, live ready-activation classification, exact claimed-lease revalidation before mutation, and terminal URL completion under the same registry/database and lease fence", refusal: "Serialize application and recovery; preserve receipt replay and changed-answer conflict; refuse uncommitted old-activation answers without replay; record only settled terminal URL decisions after activation; keep lost leases, wrong or unreadable identity, maintenance, and uncertain application fail-closed.", negativeId: BREG-NEG-67, negativeTest: {path: crates/registry-breg/tests/support/hook_activation.rs, name: classified_proposal_completion_preserves_registry_and_lease_fences}} @@ -191,3 +191,4 @@ invariants: - {"id": "BREG-SEC-186", "state": "enforced", "targetWave": "W5", "threat": "An Action availability observation or distinct guard refusal reveals a hidden target field, row boundary, or record existence, or is mistaken for mutation authority.", "enforcementPoint": "Selected-profile Action availability admission, ordinary Get disclosure, and commit-time guard problem mapping.", "refusal": "Conceal missing and row-denied targets as the same 404; return a closed not-disclosed result without inspecting hidden guard or boundary values; defer a failed guard until current-target admission prechecks pass and use action.guard_failed only when the same profile can safely read the failed target field; never treat that code as proof that later effect, proposed-value, or database checks would pass; retain every authoritative check before commit.", "negativeId": "BREG-NEG-186", "negativeTest": {"path": "crates/registry-breg/tests/postgres_immediate_action_requirements.rs", "name": "immediate_action_availability_preserves_disclosure_and_commit_boundaries"}} - {"id": "BREG-SEC-187", "state": "enforced", "targetWave": "W5", "threat": "An active Record keeps or creates a reference to a tombstoned target, including through concurrent writes or a referencing Record hidden by caller row permissions; a refusal discloses hidden records or values beyond the lifecycle eligibility inherent in an authorized link attempt.", "enforcementPoint": "Compiler-owned lifecycle key and nullable generated source lifecycle column, enforced by immediate PostgreSQL foreign keys alongside the retained UUID foreign keys.", "refusal": "Refuse inactive targets and tombstones with incoming active references atomically across all writers; keep generic value-free conflicts, independently granted Action and Proposal authority, and retained reference IDs. A caller authorized to submit a known target identifier can infer whether it is lifecycle-eligible from success versus conflict even without target read authority; disclose no target values, referring Record identifiers, or missing-versus-tombstoned detail.", "negativeId": "BREG-NEG-187", "negativeTest": {"path": "crates/registry-breg/tests/postgres_reference_integrity.rs", "name": "active_references_refuse_tombstones_and_preserve_retained_links"}} - {"id": "BREG-SEC-188", "state": "enforced", "targetWave": "W5", "threat": "A Proposal author sets or changes an Action input another role owns, or uses the fixed-input check to learn a value the grant cannot read.", "enforcementPoint": "Compile-time writableInputs validation requiring declared inputs, input-writing authority, and readable fixed inputs for Edit, Revise or Rebase; Start, Edit, Revise and Rebase compare every fixed input with the stored base inside the mutation transaction.", "refusal": "Refuse with 400 request.invalid located at the first fixed input's API name, roll back the draft, and audit the refusal without the value.", "negativeId": "BREG-NEG-188", "negativeTest": {"path": "crates/registry-breg/tests/postgres_proposal.rs", "name": "writable_inputs_bound_start_edit_revise_and_rebase_to_the_named_inputs"}} + - {"id":"BREG-SEC-189","state":"enforced","targetWave":"W5","threat":"A successor package leaves pending Proposals with inspection and cancellation grants that cannot produce a usable profile-bound condition.","enforcementPoint":"Successor activation recovery guard requires Get inspection and one Proposal grant with Cancel plus Get or List; ordinary profile-bound ETags remain enforced.","refusal":"Refuse activation before changing the ledger or pending work when no complete recovery permission exists. Retain same-profile Get+Cancel and List+Cancel recovery.","negativeId":"BREG-NEG-189","negativeTest":{"path":"crates/registry-breg/tests/postgres_proposal_recovery_permissions.rs","name":"refused_split_recovery_successor_preserves_activation_and_pending_work"}} diff --git a/products/breg/contracts/security-test-traceability.yaml b/products/breg/contracts/security-test-traceability.yaml index 5e8085d91c..40df520efa 100644 --- a/products/breg/contracts/security-test-traceability.yaml +++ b/products/breg/contracts/security-test-traceability.yaml @@ -62,7 +62,7 @@ traceability: - {id: BREG-SEC-59, state: enforced, negativeId: BREG-NEG-59, negativeTest: {path: crates/registry-breg-client/tests/http_boundary.rs, name: caller_scoped_tokens_are_isolated_across_concurrent_requests}} - {id: BREG-SEC-60, state: enforced, negativeId: BREG-NEG-60, negativeTest: {path: crates/registry-breg/tests/postgres_proposal.rs, name: prepared_lifecycle_recovery_uses_current_authority_and_the_original_exact_condition}} - {id: BREG-SEC-63, state: enforced, negativeId: BREG-NEG-63, negativeTest: {path: crates/registry-breg/src/mutation/proposal.rs, name: proposal_actor_is_keyed_database_and_issuer_scoped_but_stable_across_activation}} - - {id: BREG-SEC-64, state: enforced, negativeId: BREG-NEG-64, negativeTest: {path: crates/registry-breg/tests/postgres_proposal_security.rs, name: signed_proposal_start_and_lifecycle_receipts_reject_sibling_grants}} + - {id: BREG-SEC-64, state: enforced, negativeId: BREG-NEG-64, negativeTest: {path: crates/registry-breg/tests/postgres_proposal_task_authority.rs, name: automatic_apply_requires_active_retained_task_authority}} - {id: BREG-SEC-65, state: enforced, negativeId: BREG-NEG-65, negativeTest: {path: crates/registry-breg/tests/postgres_action_evidence.rs, name: commit_evidence_is_guard_only_and_replay_never_reacquires}} - {id: BREG-SEC-66, state: enforced, negativeId: BREG-NEG-66, negativeTest: {path: crates/registry-breg/tests/postgres_hook_changes.rs, name: real_postgres_a_retained_local_delivery_survives_restart_verification}} - {id: BREG-SEC-67, state: enforced, negativeId: BREG-NEG-67, negativeTest: {path: crates/registry-breg/tests/support/hook_activation.rs, name: classified_proposal_completion_preserves_registry_and_lease_fences}} @@ -193,3 +193,4 @@ traceability: - {"id": "BREG-SEC-186", "state": "enforced", "negativeId": "BREG-NEG-186", "negativeTest": {"path": "crates/registry-breg/tests/postgres_immediate_action_requirements.rs", "name": "immediate_action_availability_preserves_disclosure_and_commit_boundaries"}} - {"id": "BREG-SEC-187", "state": "enforced", "negativeId": "BREG-NEG-187", "negativeTest": {"path": "crates/registry-breg/tests/postgres_reference_integrity.rs", "name": "active_references_refuse_tombstones_and_preserve_retained_links"}} - {"id": "BREG-SEC-188", "state": "enforced", "negativeId": "BREG-NEG-188", "negativeTest": {"path": "crates/registry-breg/tests/postgres_proposal.rs", "name": "writable_inputs_bound_start_edit_revise_and_rebase_to_the_named_inputs"}} + - {"id":"BREG-SEC-189","state":"enforced","negativeId":"BREG-NEG-189","negativeTest":{"path":"crates/registry-breg/tests/postgres_proposal_recovery_permissions.rs","name":"refused_split_recovery_successor_preserves_activation_and_pending_work"}} diff --git a/products/breg/scripts/test-postgres.sh b/products/breg/scripts/test-postgres.sh index dff9c5c360..b77f2d29a5 100755 --- a/products/breg/scripts/test-postgres.sh +++ b/products/breg/scripts/test-postgres.sh @@ -15,8 +15,11 @@ if [[ $# -ne 0 ]]; then lane=$2 fi case "$lane" in - all|postgres|immediate-actions) ;; - *) usage; exit 2 ;; + all | postgres | immediate-actions) ;; + *) + usage + exit 2 + ;; esac if [[ -z "${BREG_TEST_DATABASE_URL:-}" ]]; then @@ -84,6 +87,8 @@ if [[ "$lane" == all || "$lane" == postgres ]]; then --test postgres_import_authority \ --test postgres_refactor \ --test postgres_proposal \ + --test postgres_proposal_task_authority \ + --test postgres_proposal_recovery_permissions \ --test postgres_proposal_shape_activation \ --test postgres_native_retention \ --test postgres_proposal_human_recovery \ @@ -129,7 +134,10 @@ if len(binaries) != 1: raise SystemExit("expected exactly one built Evidence executable") print(binaries[0]) ') - [[ -x "$evidence_binary" ]] || { printf '%s\n' 'Built Evidence executable is unavailable.' >&2; exit 2; } + [[ -x "$evidence_binary" ]] || { + printf '%s\n' 'Built Evidence executable is unavailable.' >&2 + exit 2 + } export BREG_TEST_EVIDENCE_BINARY="$evidence_binary" uv run --no-project --with PyYAML==6.0.2 cargo test --locked -p registry-breg --features postgres-test,tooling,schema \ --test postgres_action_evidence \ diff --git a/products/breg/scripts/test_validate_product.py b/products/breg/scripts/test_validate_product.py index 9f7ca1d78f..a9952874b4 100644 --- a/products/breg/scripts/test_validate_product.py +++ b/products/breg/scripts/test_validate_product.py @@ -71,7 +71,7 @@ def test_security_range_is_closed_through_derived_sql_invariants(self) -> None: ) extension_rows = matrix["invariants"][24:] self.assertEqual( - [f"BREG-NEG-{index:02d}" for index in range(25, 189) if index not in {61, 62}], + [f"BREG-NEG-{index:02d}" for index in range(25, 190) if index not in {61, 62}], [invariant["negativeId"] for invariant in extension_rows], ) for invariant in extension_rows: diff --git a/products/breg/scripts/validate_product.py b/products/breg/scripts/validate_product.py index 07f449d94c..717a15d7dd 100644 --- a/products/breg/scripts/validate_product.py +++ b/products/breg/scripts/validate_product.py @@ -30,7 +30,7 @@ CONTRACT_STATES = {"enforced", "partial", "planned"} V1_REQUIREMENT_IDS = tuple(f"BREG-V1-{index:02d}" for index in range(1, 45)) ACCEPTANCE_JOURNEY_IDS = tuple(f"BREG-J{index:02d}" for index in range(1, 25)) -SECURITY_INVARIANT_IDS = tuple(f"BREG-SEC-{index:02d}" for index in range(1, 189) if index not in {61, 62}) +SECURITY_INVARIANT_IDS = tuple(f"BREG-SEC-{index:02d}" for index in range(1, 190) if index not in {61, 62}) ACCEPTANCE_FIXTURES = { "BREG-J01": ("asset-site-placement", "acceptance/asset-site-placement"), "BREG-J02": ("asset-site-placement", "acceptance/asset-site-placement"), @@ -138,6 +138,8 @@ "cargo test --locked -p registry-breg --features postgres-test,tooling,schema --test postgres_import_authority", "cargo test --locked -p registry-breg --features postgres-test,tooling,schema --test postgres_refactor", "cargo test --locked -p registry-breg --features postgres-test,tooling,schema --test postgres_proposal", + "cargo test --locked -p registry-breg --features postgres-test,tooling,schema --test postgres_proposal_task_authority", + "cargo test --locked -p registry-breg --features postgres-test,tooling,schema --test postgres_proposal_recovery_permissions", "cargo test --locked -p registry-breg --features postgres-test,tooling,schema --test postgres_proposal_shape_activation", "cargo test --locked -p registry-breg --features postgres-test,tooling,schema --test postgres_native_retention", "cargo test --locked -p registry-breg --features postgres-test,tooling,schema --test postgres_proposal_human_recovery", From 55089183093df9a22630cb7ec17db86e83074a62 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sat, 24 Oct 2026 14:48:47 +0700 Subject: [PATCH 2/2] fix(deps): remove vulnerable P-256 arithmetic Signed-off-by: Jeremi Joslin --- Cargo.lock | 253 ++++++++++++------ Cargo.toml | 2 +- crates/registry-bregctl/src/dev/config.rs | 6 +- .../registry-bregctl/src/dev/export_client.rs | 2 +- crates/registry-caseworkctl/src/dev/config.rs | 8 +- .../src/convert.rs | 5 +- .../tests/golden_fixture.rs | 6 +- .../src/convert.rs | 6 +- .../tests/golden_fixture.rs | 6 +- .../tests/happy_path.rs | 6 +- .../registry-evidence-client/src/fixtures.rs | 10 +- .../tests/against_a_real_deployment.rs | 10 +- .../registry-evidence-oid4vci/src/testing.rs | 6 +- .../tests/inji_interoperability.rs | 10 +- .../tests/offer_authorization.rs | 2 +- .../tests/state_machine_recovery.rs | 6 +- .../src/fixtures.rs | 4 +- .../src/verifier.rs | 6 +- crates/registry-evidence/src/main.rs | 7 +- crates/registry-evidence/src/signing.rs | 6 +- crates/registry-evidencectl/src/keygen.rs | 6 +- .../tests/request_verify.rs | 2 +- crates/registry-platform-crypto/src/lib.rs | 8 +- .../src/client/private_key_jwt.rs | 5 +- .../examples/contextual-exchange.rs | 5 +- .../examples/integration.rs | 5 +- products/breg/wasm-handler-sdk/Cargo.lock | 217 ++++++++++----- products/evidence/fuzz/Cargo.lock | 217 ++++++++++----- products/platform/CHANGELOG.md | 4 + products/platform/fuzz/Cargo.lock | 221 ++++++++++----- 30 files changed, 706 insertions(+), 351 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6c3a5650f..cf81056728 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -380,9 +380,9 @@ checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" [[package]] name = "base16ct" -version = "0.2.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "base64" @@ -1051,6 +1051,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1335,12 +1341,16 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" -version = "0.5.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +checksum = "6bb08299a6046833a879ace115697f12fd2bd6c8d3e93c909ffc9e0738e7cae0" dependencies = [ - "generic-array", - "rand_core 0.6.4", + "cpubits", + "ctutils", + "getrandom 0.4.3", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", "subtle", "zeroize", ] @@ -1361,7 +1371,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ + "getrandom 0.4.3", "hybrid-array", + "rand_core 0.10.1", ] [[package]] @@ -1393,11 +1405,12 @@ checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" [[package]] name = "ctutils" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12218662ccac37d9714f2191252553d67585b97df8c839cbe49e0bde278dc865" +checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" dependencies = [ "cmov", + "subtle", ] [[package]] @@ -1513,7 +1526,18 @@ dependencies = [ "const-oid 0.9.6", "der_derive", "flagset", - "pem-rfc7468", + "pem-rfc7468 0.7.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid 0.10.2", + "pem-rfc7468 1.0.0", "zeroize", ] @@ -1592,9 +1616,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid 0.9.6", "crypto-common 0.1.6", - "subtle", ] [[package]] @@ -1643,16 +1665,17 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecdsa" -version = "0.16.9" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ - "der", - "digest 0.10.7", + "der 0.8.2", + "digest 0.11.3", "elliptic-curve", "rfc6979", - "signature", - "spki", + "signature 3.0.0", + "spki 0.8.1", + "zeroize", ] [[package]] @@ -1670,8 +1693,8 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "signature 2.2.0", ] [[package]] @@ -1709,19 +1732,20 @@ checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "elliptic-curve" -version = "0.13.8" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" dependencies = [ "base16ct", "crypto-bigint", - "digest 0.10.7", + "crypto-common 0.2.2", + "digest 0.11.3", "ff", - "generic-array", "group", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", + "hybrid-array", + "pem-rfc7468 1.0.0", + "pkcs8 0.11.0", + "rand_core 0.10.1", "sec1", "subtle", "zeroize", @@ -1802,7 +1826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1916,11 +1940,11 @@ checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693" [[package]] name = "ff" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" dependencies = [ - "rand_core 0.6.4", + "rand_core 0.10.1", "subtle", ] @@ -2163,7 +2187,6 @@ checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", - "zeroize", ] [[package]] @@ -2258,12 +2281,12 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "group" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" dependencies = [ "ff", - "rand_core 0.6.4", + "rand_core 0.10.1", "subtle", ] @@ -2554,15 +2577,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "hmac" version = "0.13.0" @@ -2632,7 +2646,9 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ + "subtle", "typenum", + "zeroize", ] [[package]] @@ -3215,7 +3231,7 @@ dependencies = [ "js-sys", "serde", "serde_json", - "signature", + "signature 2.2.0", "zeroize", ] @@ -3942,14 +3958,15 @@ dependencies = [ [[package]] name = "p256" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" dependencies = [ "ecdsa", "elliptic-curve", + "primefield", "primeorder", - "sha2 0.10.9", + "sha2 0.11.1", ] [[package]] @@ -4057,6 +4074,15 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "peniko" version = "0.6.1" @@ -4193,9 +4219,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", ] [[package]] @@ -4204,8 +4230,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.2", + "spki 0.8.1", ] [[package]] @@ -4315,7 +4351,7 @@ dependencies = [ "byteorder", "bytes", "fallible-iterator 0.2.0", - "hmac 0.13.0", + "hmac", "md-5", "memchr", "rand 0.10.2", @@ -4395,13 +4431,31 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.2", + "ff", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "primeorder" -version = "0.13.6" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" dependencies = [ "elliptic-curve", + "once_cell", + "primefield", + "serdect", + "wnaf", ] [[package]] @@ -4680,7 +4734,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4933,7 +4987,7 @@ dependencies = [ "futures-core", "getrandom 0.4.3", "hex", - "hmac 0.13.0", + "hmac", "ipnet", "jsonschema", "jsonwebtoken", @@ -5142,7 +5196,7 @@ dependencies = [ "chrono", "clap", "getrandom 0.4.3", - "hmac 0.13.0", + "hmac", "inquire", "jsonschema", "p256", @@ -5988,7 +6042,7 @@ dependencies = [ "jsonschema", "p256", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand 0.9.5", "rand_chacha 0.9.0", "registry-cli-reference", @@ -6251,7 +6305,7 @@ dependencies = [ name = "registry-platform-audit" version = "0.44.0" dependencies = [ - "hmac 0.13.0", + "hmac", "regex", "rustix 1.1.5", "schemars", @@ -6332,10 +6386,10 @@ dependencies = [ "aws-lc-rs", "base64 0.23.1", "ed25519-dalek", - "hmac 0.13.0", + "hmac", "p256", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "proptest", "registry-platform-canonical-json", "reqwest 0.12.28", @@ -6454,7 +6508,7 @@ dependencies = [ "axum", "chrono", "hex", - "hmac 0.13.0", + "hmac", "quick-xml 0.42.0", "registry-platform-config", "registry-platform-httputil", @@ -6964,12 +7018,12 @@ dependencies = [ [[package]] name = "rfc6979" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" dependencies = [ - "hmac 0.12.1", - "subtle", + "crypto-bigint", + "hmac", ] [[package]] @@ -7175,7 +7229,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7337,14 +7391,14 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sec1" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct", - "der", - "generic-array", - "pkcs8", + "ctutils", + "der 0.8.2", + "hybrid-array", "subtle", "zeroize", ] @@ -7496,6 +7550,16 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha2" version = "0.10.9" @@ -7586,10 +7650,19 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + [[package]] name = "simd-adler32" version = "0.3.10" @@ -7701,7 +7774,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" +dependencies = [ + "base64ct", + "der 0.8.2", ] [[package]] @@ -7929,7 +8012,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9734,7 +9817,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9942,6 +10025,18 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wnaf" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" +dependencies = [ + "ff", + "group", + "hybrid-array", + "primefield", +] + [[package]] name = "write-fonts" version = "0.48.1" @@ -9974,8 +10069,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" dependencies = [ "const-oid 0.9.6", - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", "tls_codec", ] diff --git a/Cargo.toml b/Cargo.toml index 1a7173e05a..7ecc83b2df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -219,7 +219,7 @@ minijinja = { version = "2.24", default-features = false } napi = { version = "3.12.0", features = ["async", "serde-json"] } napi-build = { version = "2.4.0" } napi-derive = { version = "3.6.2" } -p256 = { version = "0.13", features = ["ecdsa"] } +p256 = { version = "0.14", features = ["ecdsa", "getrandom"] } pg_query = { version = "=6.2.1", default-features = false } pkcs1 = { version = "0.7", features = ["alloc"] } pkcs8 = { version = "0.10" } diff --git a/crates/registry-bregctl/src/dev/config.rs b/crates/registry-bregctl/src/dev/config.rs index 6e5f71980c..a861136813 100644 --- a/crates/registry-bregctl/src/dev/config.rs +++ b/crates/registry-bregctl/src/dev/config.rs @@ -4,7 +4,7 @@ use super::{private, State, DATABASE_ID, MAX_BYTES, MIGRATION_ROLE, RUNTIME_ROLE}; use anyhow::{bail, Context, Result}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use p256::ecdsa::SigningKey; +use p256::{ecdsa::SigningKey, elliptic_curve::Generate as _}; use registry_breg::literal_text::{ LiteralText, WRITE_THE_VALUE, WRITE_THE_VALUE_OR_A_SECRET_REFERENCE, }; @@ -1300,8 +1300,8 @@ pub(super) fn hash(bytes: &[u8]) -> String { pub(super) fn keypair(root: &Path) -> Result { private::directory(root)?; - let key = SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng); - let point = key.verifying_key().to_encoded_point(false); + let key = SigningKey::generate(); + let point = key.verifying_key().to_sec1_point(false); let x = URL_SAFE_NO_PAD.encode(point.x().context("generated key lacks x")?); let y = URL_SAFE_NO_PAD.encode(point.y().context("generated key lacks y")?); let kid = URL_SAFE_NO_PAD.encode(Sha256::digest(serde_json::to_vec( diff --git a/crates/registry-bregctl/src/dev/export_client.rs b/crates/registry-bregctl/src/dev/export_client.rs index 9bc7baeec4..4962d948dc 100644 --- a/crates/registry-bregctl/src/dev/export_client.rs +++ b/crates/registry-bregctl/src/dev/export_client.rs @@ -113,7 +113,7 @@ pub(super) fn validate_pair(id: &[u8], key: &[u8], label: &str) -> Result<()> { } let d = Zeroizing::new(URL_SAFE_NO_PAD.decode(jwk["d"].as_str()?).ok()?); let signing = p256::ecdsa::SigningKey::from_slice(&d).ok()?; - let point = signing.verifying_key().to_encoded_point(false); + let point = signing.verifying_key().to_sec1_point(false); if jwk["x"].as_str()? != URL_SAFE_NO_PAD.encode(point.x()?) || jwk["y"].as_str()? != URL_SAFE_NO_PAD.encode(point.y()?) { diff --git a/crates/registry-caseworkctl/src/dev/config.rs b/crates/registry-caseworkctl/src/dev/config.rs index 9c08acd8b9..dcf4840ccd 100644 --- a/crates/registry-caseworkctl/src/dev/config.rs +++ b/crates/registry-caseworkctl/src/dev/config.rs @@ -4,7 +4,7 @@ use super::{private, State, MIGRATION_ROLE}; use anyhow::{bail, Context, Result}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use p256::ecdsa::SigningKey; +use p256::{ecdsa::SigningKey, elliptic_curve::Generate as _}; use registry_casework_core::{ findings_report, valid_directory_identifier, valid_profile_identifier, CaseworkProject, CaseworkRole, ConfigFinding, @@ -364,7 +364,7 @@ pub(super) fn borrow_client( )?, ); let signing = SigningKey::from_slice(&d).context("owner assertion key is invalid")?; - let point = signing.verifying_key().to_encoded_point(false); + let point = signing.verifying_key().to_sec1_point(false); if jwk["kty"] != "EC" || jwk["crv"] != "P-256" || jwk["alg"] != "ES256" @@ -907,8 +907,8 @@ pub(super) fn hex_secret() -> Result> { pub(super) fn keypair(root: &Path) -> Result { private::directory(root)?; - let key = SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng); - let point = key.verifying_key().to_encoded_point(false); + let key = SigningKey::generate(); + let point = key.verifying_key().to_sec1_point(false); let x = URL_SAFE_NO_PAD.encode(point.x().context("generated key lacks x")?); let y = URL_SAFE_NO_PAD.encode(point.y().context("generated key lacks y")?); let kid = URL_SAFE_NO_PAD.encode(Sha256::digest(serde_json::to_vec( diff --git a/crates/registry-evidence-client-node/src/convert.rs b/crates/registry-evidence-client-node/src/convert.rs index f7e7cfec29..ef7871a217 100644 --- a/crates/registry-evidence-client-node/src/convert.rs +++ b/crates/registry-evidence-client-node/src/convert.rs @@ -1163,6 +1163,7 @@ fn insert_retry_after(fields: &mut Map, retry_after_seconds: Opti mod tests { use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use ed25519_dalek::SigningKey; + use p256::elliptic_curve::Generate as _; use registry_evidence_client::{ EvidenceObjectType, OAuthErrorCode, SubjectBinding, SubjectBindingMode, SupportedValue, TransportKind, VerificationError, @@ -1726,8 +1727,8 @@ mod tests { /// A fresh ES256 signing key, in the shape `evidencectl access client add /// --generate-local-key` writes. fn generated_es256_client_key_json(key_id: &str) -> Value { - let key = p256::ecdsa::SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng); - let point = key.verifying_key().to_encoded_point(false); + let key = p256::ecdsa::SigningKey::generate(); + let point = key.verifying_key().to_sec1_point(false); serde_json::json!({ "kty": "EC", "crv": "P-256", diff --git a/crates/registry-evidence-client-node/tests/golden_fixture.rs b/crates/registry-evidence-client-node/tests/golden_fixture.rs index d400a7fb2d..0e8eec9b29 100644 --- a/crates/registry-evidence-client-node/tests/golden_fixture.rs +++ b/crates/registry-evidence-client-node/tests/golden_fixture.rs @@ -21,7 +21,7 @@ use std::{fs, path::Path}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use chrono::{DateTime, Duration as ChronoDuration, Utc}; -use p256::{ecdsa::SigningKey, elliptic_curve::rand_core::OsRng}; +use p256::{ecdsa::SigningKey, elliptic_curve::Generate as _}; use registry_evidence_client::{ AssuranceProfile, Evidence, EvidenceObjectType, EvidenceVerificationPolicyDocument, ExpectedFormDocument, ExpectedOutputDocument, ExpectedScalarFormDocument, @@ -68,8 +68,8 @@ fn fixture_issued_at() -> DateTime { /// leaves the process that made it: regeneration commits only the public key, /// and the real-clock check below discards the whole pair when it returns. fn fixture_signer() -> LocalJwkSigner { - let signing_key = SigningKey::random(&mut OsRng); - let public = signing_key.verifying_key().to_encoded_point(false); + let signing_key = SigningKey::generate(); + let public = signing_key.verifying_key().to_sec1_point(false); let mut private_jwk = PrivateJwk { kty: "EC".to_owned(), kid: None, diff --git a/crates/registry-evidence-client-py/src/convert.rs b/crates/registry-evidence-client-py/src/convert.rs index 80717f0060..c43d041bcf 100644 --- a/crates/registry-evidence-client-py/src/convert.rs +++ b/crates/registry-evidence-client-py/src/convert.rs @@ -1138,6 +1138,7 @@ mod tests { use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use ed25519_dalek::SigningKey; use evidence_client_sdk::{NonceError, OAuthErrorCode, TransportKind}; + use p256::elliptic_curve::Generate as _; use pyo3::{ types::{PyDict, PyList}, Python, @@ -1163,9 +1164,8 @@ mod tests { /// The shape `evidencectl access client add --generate-local-key` writes, /// which is the file the published tutorial hands to this binding. fn generated_es256_private_jwk_json() -> Value { - let signing_key = - p256::ecdsa::SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng); - let point = signing_key.verifying_key().to_encoded_point(false); + let signing_key = p256::ecdsa::SigningKey::generate(); + let point = signing_key.verifying_key().to_sec1_point(false); serde_json::json!({ "kty": "EC", "crv": "P-256", diff --git a/crates/registry-evidence-client-py/tests/golden_fixture.rs b/crates/registry-evidence-client-py/tests/golden_fixture.rs index a28bd16f50..70fa3a8b35 100644 --- a/crates/registry-evidence-client-py/tests/golden_fixture.rs +++ b/crates/registry-evidence-client-py/tests/golden_fixture.rs @@ -26,7 +26,7 @@ use std::{fs, path::Path}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use chrono::{DateTime, Duration as ChronoDuration, Utc}; -use p256::{ecdsa::SigningKey, elliptic_curve::rand_core::OsRng}; +use p256::{ecdsa::SigningKey, elliptic_curve::Generate as _}; // This file is a separate integration-test crate, and Cargo auto-links two // different crates under the identical name `registry_evidence_client` here: // this package's own compiled library (its `[lib] name`) and the wrapped SDK @@ -84,8 +84,8 @@ fn fixture_issued_at() -> DateTime { /// leaves the process that made it: regeneration commits only the public key, /// and the real-clock check below discards the whole pair when it returns. fn fixture_signer() -> LocalJwkSigner { - let signing_key = SigningKey::random(&mut OsRng); - let public = signing_key.verifying_key().to_encoded_point(false); + let signing_key = SigningKey::generate(); + let public = signing_key.verifying_key().to_sec1_point(false); let mut private_jwk = PrivateJwk { kty: "EC".to_owned(), kid: None, diff --git a/crates/registry-evidence-client-py/tests/happy_path.rs b/crates/registry-evidence-client-py/tests/happy_path.rs index 75d7e56311..ffb48d2bb9 100644 --- a/crates/registry-evidence-client-py/tests/happy_path.rs +++ b/crates/registry-evidence-client-py/tests/happy_path.rs @@ -41,7 +41,7 @@ use evidence_client_sdk::{ }; use p256::{ ecdsa::{signature::Signer, Signature, SigningKey}, - elliptic_curve::rand_core::OsRng, + elliptic_curve::Generate as _, }; use pyo3::prelude::*; use registry_evidence_verifier::{ @@ -121,11 +121,11 @@ fn request_batch_spec_json() -> serde_json::Value { /// response for a nonce that does not exist until `prepare()` runs, so they /// cannot use a response signed ahead of time. fn fresh_signing_key() -> SigningKey { - SigningKey::random(&mut OsRng) + SigningKey::generate() } fn public_jwk(signing_key: &SigningKey) -> PublicJwk { - let point = signing_key.verifying_key().to_encoded_point(false); + let point = signing_key.verifying_key().to_sec1_point(false); let mut key = PublicJwk { kty: "EC".to_owned(), kid: None, diff --git a/crates/registry-evidence-client/src/fixtures.rs b/crates/registry-evidence-client/src/fixtures.rs index 81977010c2..80bb64aeb8 100644 --- a/crates/registry-evidence-client/src/fixtures.rs +++ b/crates/registry-evidence-client/src/fixtures.rs @@ -18,7 +18,7 @@ use std::collections::BTreeMap; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use chrono::{DateTime, SecondsFormat, TimeDelta, Utc}; -use p256::{ecdsa::SigningKey, elliptic_curve::rand_core::OsRng}; +use p256::{ecdsa::SigningKey, elliptic_curve::Generate as _}; use registry_evidence_verifier::{ model::{Evidence, HolderPublicKey, JwksDocument}, sdjwt_vc::issuance_input, @@ -63,9 +63,7 @@ pub(crate) struct SignedEvidenceFixture { /// coordinates are returned, because that is all a request ever carries and all /// this crate is ever given. Each call returns a distinct key. pub(crate) fn holder_key() -> HolderPublicKey { - let point = SigningKey::random(&mut OsRng) - .verifying_key() - .to_encoded_point(false); + let point = SigningKey::generate().verifying_key().to_sec1_point(false); HolderPublicKey { kty: "EC".to_owned(), crv: "P-256".to_owned(), @@ -79,8 +77,8 @@ pub(crate) fn holder_key() -> HolderPublicKey { /// A fresh issuer. Two fixtures never share a key or a key identifier, so a /// response from one is a response signed by a key the other never pinned. pub(crate) fn signed_evidence() -> SignedEvidenceFixture { - let signing_key = SigningKey::random(&mut OsRng); - let public = signing_key.verifying_key().to_encoded_point(false); + let signing_key = SigningKey::generate(); + let public = signing_key.verifying_key().to_sec1_point(false); let mut signing_key = PrivateJwk { kty: "EC".to_owned(), kid: None, diff --git a/crates/registry-evidence-client/tests/against_a_real_deployment.rs b/crates/registry-evidence-client/tests/against_a_real_deployment.rs index 3580c3836b..8919939a8a 100644 --- a/crates/registry-evidence-client/tests/against_a_real_deployment.rs +++ b/crates/registry-evidence-client/tests/against_a_real_deployment.rs @@ -39,7 +39,7 @@ use std::{ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use chrono::Utc; -use p256::{ecdsa::SigningKey, elliptic_curve::rand_core::OsRng}; +use p256::{ecdsa::SigningKey, elliptic_curve::Generate as _}; use registry_evidence::{runtime::EvidenceRuntime, server}; use registry_evidence_client::{ AssuranceProfile, AudienceScopedRequest, BearerToken, ConceptForm, DefinitionCardinality, @@ -3250,8 +3250,8 @@ fn generate_key(key_id: &str) -> PrivateJwk { /// A fresh ES256 private JWK for a client, in the shape `evidencectl access /// client add --generate-local-key` writes. fn generate_es256_key(key_id: &str) -> PrivateJwk { - let signing_key = SigningKey::random(&mut OsRng); - let point = signing_key.verifying_key().to_encoded_point(false); + let signing_key = SigningKey::generate(); + let point = signing_key.verifying_key().to_sec1_point(false); let document = json!({ "kty": "EC", "crv": "P-256", @@ -3267,8 +3267,8 @@ fn generate_es256_key(key_id: &str) -> PrivateJwk { /// A fresh ES256 service key whose identifier is its RFC 7638 thumbprint. fn service_key() -> (String, PublicJwk) { - let signing_key = SigningKey::random(&mut OsRng); - let point = signing_key.verifying_key().to_encoded_point(false); + let signing_key = SigningKey::generate(); + let point = signing_key.verifying_key().to_sec1_point(false); let x = URL_SAFE_NO_PAD.encode(point.x().expect("the public point has x")); let y = URL_SAFE_NO_PAD.encode(point.y().expect("the public point has y")); let mut public = PublicJwk { diff --git a/crates/registry-evidence-oid4vci/src/testing.rs b/crates/registry-evidence-oid4vci/src/testing.rs index f6797cbb85..462e70e0fd 100644 --- a/crates/registry-evidence-oid4vci/src/testing.rs +++ b/crates/registry-evidence-oid4vci/src/testing.rs @@ -4,14 +4,14 @@ //! key, and no value derived from one, is written to a tracked file. use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; -use p256::{ecdsa::SigningKey, elliptic_curve::rand_core::OsRng}; +use p256::{ecdsa::SigningKey, elliptic_curve::Generate as _}; use registry_platform_crypto::{sign, PrivateJwk}; use serde_json::{json, Value}; /// A freshly generated P-256 private JWK, as JSON text. pub(crate) fn private_jwk(key_id: &str) -> String { - let key = SigningKey::random(&mut OsRng); - let point = key.verifying_key().to_encoded_point(false); + let key = SigningKey::generate(); + let point = key.verifying_key().to_sec1_point(false); json!({ "kty": "EC", "crv": "P-256", diff --git a/crates/registry-evidence-oid4vci/tests/inji_interoperability.rs b/crates/registry-evidence-oid4vci/tests/inji_interoperability.rs index bd7acb31ce..ec4e6940cc 100644 --- a/crates/registry-evidence-oid4vci/tests/inji_interoperability.rs +++ b/crates/registry-evidence-oid4vci/tests/inji_interoperability.rs @@ -31,7 +31,7 @@ use axum::{ use axum_test::TestServer; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use chrono::{SecondsFormat, Utc}; -use p256::{ecdsa::SigningKey, elliptic_curve::rand_core::OsRng}; +use p256::{ecdsa::SigningKey, elliptic_curve::Generate as _}; use registry_evidence_client::{ AssuranceProfile, EvidenceDefinitionsDocument, HolderBoundRequestSpec, HolderPublicKey, EVIDENCE_SD_JWT_VC_BATCH_MEDIA_TYPE, SD_JWT_VC_BATCH_SCHEMA_V1, @@ -124,8 +124,8 @@ struct FixtureKey { impl FixtureKey { fn generate() -> Self { - let key = SigningKey::random(&mut OsRng); - let point = key.verifying_key().to_encoded_point(false); + let key = SigningKey::generate(); + let point = key.verifying_key().to_sec1_point(false); let public = json!({ "kty": "EC", "crv": "P-256", @@ -183,8 +183,8 @@ impl FixtureIssuer { fn new() -> Self { let document: EvidenceDefinitionsDocument = serde_json::from_str(DEFINITIONS).expect("the definitions document parses"); - let generated = SigningKey::random(&mut OsRng); - let point = generated.verifying_key().to_encoded_point(false); + let generated = SigningKey::generate(); + let point = generated.verifying_key().to_sec1_point(false); let public_without_kid = json!({ "kty": "EC", "crv": "P-256", diff --git a/crates/registry-evidence-oid4vci/tests/offer_authorization.rs b/crates/registry-evidence-oid4vci/tests/offer_authorization.rs index aec90d31d1..72cd8ffa53 100644 --- a/crates/registry-evidence-oid4vci/tests/offer_authorization.rs +++ b/crates/registry-evidence-oid4vci/tests/offer_authorization.rs @@ -43,7 +43,7 @@ const PRINCIPAL: &str = "urn:example:offer-caller"; fn signing_key_pair(seed: u8) -> (Value, Value) { let scalar = [seed; 32]; let signing = p256::ecdsa::SigningKey::from_slice(&scalar).expect("a valid P-256 scalar"); - let encoded = signing.verifying_key().to_encoded_point(false); + let encoded = signing.verifying_key().to_sec1_point(false); let x = URL_SAFE_NO_PAD.encode(encoded.x().expect("an uncompressed point has x")); let y = URL_SAFE_NO_PAD.encode(encoded.y().expect("an uncompressed point has y")); let bare = PublicJwk::parse( diff --git a/crates/registry-evidence-oid4vci/tests/state_machine_recovery.rs b/crates/registry-evidence-oid4vci/tests/state_machine_recovery.rs index 304f990870..daea9e5197 100644 --- a/crates/registry-evidence-oid4vci/tests/state_machine_recovery.rs +++ b/crates/registry-evidence-oid4vci/tests/state_machine_recovery.rs @@ -19,7 +19,7 @@ use async_trait::async_trait; use axum::{body::Bytes, http::StatusCode}; use axum_test::{TestResponse, TestServer}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; -use p256::{ecdsa::SigningKey, elliptic_curve::rand_core::OsRng}; +use p256::{ecdsa::SigningKey, elliptic_curve::Generate as _}; use registry_evidence_client::{ EvidenceDefinitionsDocument, HolderBoundRequestSpec, SubjectBindingMode, }; @@ -472,8 +472,8 @@ async fn minted_nonce(server: &TestServer) -> String { } fn private_jwk() -> String { - let key = SigningKey::random(&mut OsRng); - let point = key.verifying_key().to_encoded_point(false); + let key = SigningKey::generate(); + let point = key.verifying_key().to_sec1_point(false); json!({ "kty": "EC", "crv": "P-256", diff --git a/crates/registry-evidence-verifier/src/fixtures.rs b/crates/registry-evidence-verifier/src/fixtures.rs index c19d3c3f40..eb8365707f 100644 --- a/crates/registry-evidence-verifier/src/fixtures.rs +++ b/crates/registry-evidence-verifier/src/fixtures.rs @@ -160,7 +160,7 @@ pub fn jwks_document( #[cfg(test)] mod tests { - use p256::{elliptic_curve::sec1::ToEncodedPoint, SecretKey}; + use p256::{elliptic_curve::sec1::ToSec1Point, SecretKey}; use super::*; @@ -170,7 +170,7 @@ mod tests { .checked_add(1) .expect("test key index remains bounded"); let secret = SecretKey::from_slice(&scalar).expect("test scalar is valid"); - let encoded = secret.public_key().to_encoded_point(false); + let encoded = secret.public_key().to_sec1_point(false); let mut key = PublicJwk { kty: "EC".to_owned(), kid: None, diff --git a/crates/registry-evidence-verifier/src/verifier.rs b/crates/registry-evidence-verifier/src/verifier.rs index 6f995166a7..db0693c1d5 100644 --- a/crates/registry-evidence-verifier/src/verifier.rs +++ b/crates/registry-evidence-verifier/src/verifier.rs @@ -2007,7 +2007,7 @@ mod tests { )] use std::{collections::BTreeSet, sync::Arc}; - use p256::elliptic_curve::rand_core::OsRng; + use p256::elliptic_curve::Generate as _; use registry_platform_crypto::{LocalJwkSigner, PrivateJwk, SigningProvider}; use registry_platform_sdjwt::presentation_disclosure_hash; use serde::Deserialize; @@ -4563,8 +4563,8 @@ mod tests { } fn generated_public_jwk() -> PublicJwk { - let signing_key = p256::ecdsa::SigningKey::random(&mut OsRng); - let point = signing_key.verifying_key().to_encoded_point(false); + let signing_key = p256::ecdsa::SigningKey::generate(); + let point = signing_key.verifying_key().to_sec1_point(false); let mut key = PublicJwk { kty: "EC".to_owned(), kid: None, diff --git a/crates/registry-evidence/src/main.rs b/crates/registry-evidence/src/main.rs index 02bb95d0fc..c3d4348275 100644 --- a/crates/registry-evidence/src/main.rs +++ b/crates/registry-evidence/src/main.rs @@ -14,8 +14,7 @@ use std::{ use chrono::{DateTime, NaiveDate, SecondsFormat, TimeZone, Utc}; use chrono_tz::Tz; use clap::Parser; -use p256::ecdsa::SigningKey; -use rand_core::OsRng; +use p256::{ecdsa::SigningKey, elliptic_curve::Generate as _}; use registry_evidence::cli::{Cli, Command, ExplainFormat, OutputFormat}; use registry_evidence::{ audit::{last_local_audit_operation, EvidenceAuditError}, @@ -3910,9 +3909,9 @@ async fn sign_and_verify_fixture_evidence( async fn offline_fixture_signer() -> Result { use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; - let signing_key = SigningKey::random(&mut OsRng); + let signing_key = SigningKey::generate(); let private_bytes = Zeroizing::new(signing_key.to_bytes()); - let public = signing_key.verifying_key().to_encoded_point(false); + let public = signing_key.verifying_key().to_sec1_point(false); let mut private_jwk = PrivateJwk { kty: "EC".to_owned(), kid: None, diff --git a/crates/registry-evidence/src/signing.rs b/crates/registry-evidence/src/signing.rs index 7c500fa9c0..23c01fb314 100644 --- a/crates/registry-evidence/src/signing.rs +++ b/crates/registry-evidence/src/signing.rs @@ -311,7 +311,7 @@ mod tests { )] use super::*; use async_trait::async_trait; - use rand_core::OsRng; + use p256::elliptic_curve::Generate as _; use registry_platform_crypto::{LocalJwkSigner, PrivateJwk}; use sha2::{Digest, Sha256}; use std::collections::BTreeMap; @@ -523,8 +523,8 @@ mod tests { } fn generated_public_jwk() -> PublicJwk { - let signing_key = p256::ecdsa::SigningKey::random(&mut OsRng); - let point = signing_key.verifying_key().to_encoded_point(false); + let signing_key = p256::ecdsa::SigningKey::generate(); + let point = signing_key.verifying_key().to_sec1_point(false); let mut key = PublicJwk { kty: "EC".to_owned(), kid: None, diff --git a/crates/registry-evidencectl/src/keygen.rs b/crates/registry-evidencectl/src/keygen.rs index 5ae0e5c3ee..f0654cda2c 100644 --- a/crates/registry-evidencectl/src/keygen.rs +++ b/crates/registry-evidencectl/src/keygen.rs @@ -12,7 +12,7 @@ use std::{ use anyhow::{bail, Context, Result}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use clap::{Args, Subcommand, ValueEnum}; -use p256::ecdsa::SigningKey; +use p256::{ecdsa::SigningKey, elliptic_curve::Generate as _}; use registry_platform_crypto::{GeneratedKeyAlgorithm, PrivateJwk, PublicJwk}; use serde_json::{json, Map, Value}; use zeroize::Zeroizing; @@ -357,8 +357,8 @@ struct KeypairFiles { /// Generate the P-256 ES256 pair the service signing key, the holder binding /// key, and a scaffolded project's key material all use. fn generate_p256_keypair() -> Result { - let signing_key = SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng); - let point = signing_key.verifying_key().to_encoded_point(false); + let signing_key = SigningKey::generate(); + let point = signing_key.verifying_key().to_sec1_point(false); let x = URL_SAFE_NO_PAD.encode(point.x().expect("uncompressed P-256 point has x")); let y = URL_SAFE_NO_PAD.encode(point.y().expect("uncompressed P-256 point has y")); let d = Zeroizing::new(URL_SAFE_NO_PAD.encode(signing_key.to_bytes())); diff --git a/crates/registry-evidencectl/tests/request_verify.rs b/crates/registry-evidencectl/tests/request_verify.rs index 26510f2848..9b7bc831ef 100644 --- a/crates/registry-evidencectl/tests/request_verify.rs +++ b/crates/registry-evidencectl/tests/request_verify.rs @@ -904,7 +904,7 @@ impl Fixture { write_sealed_bundle(&root, &state); let signing_key = SigningKey::from_slice(&[7_u8; 32]).expect("test signing key"); - let point = signing_key.verifying_key().to_encoded_point(false); + let point = signing_key.verifying_key().to_sec1_point(false); let mut signing_jwk = json!({ "kty": "EC", "crv": "P-256", diff --git a/crates/registry-platform-crypto/src/lib.rs b/crates/registry-platform-crypto/src/lib.rs index 968f552853..2fc864edc9 100644 --- a/crates/registry-platform-crypto/src/lib.rs +++ b/crates/registry-platform-crypto/src/lib.rs @@ -38,7 +38,7 @@ use p256::ecdsa::{ signature::Verifier as _, Signature as P256Signature, VerifyingKey as P256VerifyingKey, }; #[cfg(feature = "transit")] -use p256::elliptic_curve::sec1::ToEncodedPoint as _; +use p256::elliptic_curve::sec1::ToSec1Point as _; #[cfg(feature = "transit")] use p256::pkcs8::DecodePublicKey as _; #[cfg(feature = "transit")] @@ -968,7 +968,7 @@ fn validate_transit_public_key(pem: &str, configured: &PublicJwk) -> Result<(), configured_sec1.push(0x04); configured_sec1.extend_from_slice(&x); configured_sec1.extend_from_slice(&y); - if provider.to_encoded_point(false).as_bytes() != configured_sec1 { + if provider.to_sec1_point(false).as_bytes() != configured_sec1 { return Err(transit_error("transit provider public key does not match")); } Ok(()) @@ -2032,7 +2032,7 @@ mod tests { fn p256_public_pem(private: &PrivateJwk) -> String { let scalar = decode_fixed(private.d.as_deref(), 32, "d").expect("P-256 scalar"); let signing = P256SigningKey::from_slice(&scalar).expect("P-256 signing key"); - let encoded = signing.verifying_key().to_encoded_point(false); + let encoded = signing.verifying_key().to_sec1_point(false); let public = P256PublicKey::from_sec1_bytes(encoded.as_bytes()).expect("P-256 public key"); public .to_public_key_pem(LineEnding::LF) @@ -2625,7 +2625,7 @@ mod tests { let other_scalar = [42_u8; 32]; let other_signing = P256SigningKey::from_slice(&other_scalar).expect("second P-256 key"); - let other_point = other_signing.verifying_key().to_encoded_point(false); + let other_point = other_signing.verifying_key().to_sec1_point(false); let other_public = P256PublicKey::from_sec1_bytes(other_point.as_bytes()).expect("second public key"); let mut wrong_public = transit_metadata( diff --git a/crates/registry-platform-httputil/src/client/private_key_jwt.rs b/crates/registry-platform-httputil/src/client/private_key_jwt.rs index eba809a8a6..33d9645c32 100644 --- a/crates/registry-platform-httputil/src/client/private_key_jwt.rs +++ b/crates/registry-platform-httputil/src/client/private_key_jwt.rs @@ -1478,8 +1478,9 @@ mod tests { /// A fresh ES256 client key in the shape adopter tooling writes. fn es256_client_key(key_id: Option<&str>) -> PrivateJwk { - let key = p256::ecdsa::SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng); - let point = key.verifying_key().to_encoded_point(false); + use p256::elliptic_curve::Generate as _; + let key = p256::ecdsa::SigningKey::generate(); + let point = key.verifying_key().to_sec1_point(false); let mut document = json!({ "kty": "EC", "crv": "P-256", diff --git a/crates/registry-thunderid-tooling/examples/contextual-exchange.rs b/crates/registry-thunderid-tooling/examples/contextual-exchange.rs index 70a7965d97..a55cf5bf2c 100644 --- a/crates/registry-thunderid-tooling/examples/contextual-exchange.rs +++ b/crates/registry-thunderid-tooling/examples/contextual-exchange.rs @@ -14,6 +14,7 @@ use std::sync::{ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use p256::elliptic_curve::Generate as _; use registry_platform_authcommon::client_assertion::{ sign_client_assertion, ClientAssertionRequest, }; @@ -58,8 +59,8 @@ fn random() -> String { bytes.iter().map(|b| format!("{b:02x}")).collect() } fn key(kid: &str) -> (PrivateJwk, String) { - let secret = p256::ecdsa::SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng); - let point = secret.verifying_key().to_encoded_point(false); + let secret = p256::ecdsa::SigningKey::generate(); + let point = secret.verifying_key().to_sec1_point(false); let public = json!({"kty":"EC","crv":"P-256","alg":"ES256","kid":kid,"use":"sig", "x":URL_SAFE_NO_PAD.encode(point.x().expect("x")),"y":URL_SAFE_NO_PAD.encode(point.y().expect("y"))}); let mut private = public.clone(); diff --git a/crates/registry-thunderid-tooling/examples/integration.rs b/crates/registry-thunderid-tooling/examples/integration.rs index 7d33c44b17..c5094c2f55 100644 --- a/crates/registry-thunderid-tooling/examples/integration.rs +++ b/crates/registry-thunderid-tooling/examples/integration.rs @@ -13,6 +13,7 @@ use std::time::Duration; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine as _; +use p256::elliptic_curve::Generate as _; use registry_platform_crypto::PrivateJwk; use registry_platform_httputil::{PrivateKeyJwt, PrivateKeyJwtConfig, TokenProvider}; use registry_thunderid_tooling::bootstrap::{Bootstrap, SystemCommandRunner}; @@ -42,8 +43,8 @@ fn fail(message: &str) -> ! { /// the session state; only the public JWKS is rendered into the /// registration. fn fresh_client_key(kid: &str) -> (PrivateJwk, String) { - let secret = p256::ecdsa::SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng); - let point = secret.verifying_key().to_encoded_point(false); + let secret = p256::ecdsa::SigningKey::generate(); + let point = secret.verifying_key().to_sec1_point(false); let encode = |bytes: &[u8]| URL_SAFE_NO_PAD.encode(bytes); let public_jwks = format!( "{{\"keys\":[{{\"kty\":\"EC\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"kid\":\"{kid}\",\"use\":\"sig\",\"key_ops\":[\"verify\"],\"x\":\"{}\",\"y\":\"{}\"}}]}}", diff --git a/products/breg/wasm-handler-sdk/Cargo.lock b/products/breg/wasm-handler-sdk/Cargo.lock index 5bff350cb2..f9e8cf203c 100644 --- a/products/breg/wasm-handler-sdk/Cargo.lock +++ b/products/breg/wasm-handler-sdk/Cargo.lock @@ -142,9 +142,9 @@ dependencies = [ [[package]] name = "base16ct" -version = "0.2.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "base64" @@ -483,6 +483,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -704,12 +710,16 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" -version = "0.5.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +checksum = "6bb08299a6046833a879ace115697f12fd2bd6c8d3e93c909ffc9e0738e7cae0" dependencies = [ - "generic-array", - "rand_core 0.6.4", + "cpubits", + "ctutils", + "getrandom 0.4.3", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", "subtle", "zeroize", ] @@ -730,16 +740,19 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ + "getrandom 0.4.3", "hybrid-array", + "rand_core 0.10.1", ] [[package]] name = "ctutils" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12218662ccac37d9714f2191252553d67585b97df8c839cbe49e0bde278dc865" +checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" dependencies = [ "cmov", + "subtle", ] [[package]] @@ -791,6 +804,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid 0.9.6", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid 0.10.2", "pem-rfc7468", "zeroize", ] @@ -811,9 +834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid 0.9.6", "crypto-common 0.1.7", - "subtle", ] [[package]] @@ -874,16 +895,17 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecdsa" -version = "0.16.9" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ - "der", - "digest 0.10.7", + "der 0.8.2", + "digest 0.11.3", "elliptic-curve", "rfc6979", - "signature", - "spki", + "signature 3.0.0", + "spki 0.8.1", + "zeroize", ] [[package]] @@ -892,8 +914,8 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "signature 2.2.0", ] [[package]] @@ -919,19 +941,20 @@ checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "elliptic-curve" -version = "0.13.8" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" dependencies = [ "base16ct", "crypto-bigint", - "digest 0.10.7", + "crypto-common 0.2.2", + "digest 0.11.3", "ff", - "generic-array", "group", + "hybrid-array", "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", + "pkcs8 0.11.0", + "rand_core 0.10.1", "sec1", "subtle", "zeroize", @@ -999,11 +1022,11 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "ff" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" dependencies = [ - "rand_core 0.6.4", + "rand_core 0.10.1", "subtle", ] @@ -1160,7 +1183,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", ] [[package]] @@ -1222,12 +1244,12 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "group" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" dependencies = [ "ff", - "rand_core 0.6.4", + "rand_core 0.10.1", "subtle", ] @@ -1353,15 +1375,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "hmac" version = "0.13.0" @@ -1425,7 +1438,9 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ + "subtle", "typenum", + "zeroize", ] [[package]] @@ -1844,7 +1859,7 @@ dependencies = [ "js-sys", "serde", "serde_json", - "signature", + "signature 2.2.0", "zeroize", ] @@ -2235,14 +2250,15 @@ dependencies = [ [[package]] name = "p256" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" dependencies = [ "ecdsa", "elliptic-curve", + "primefield", "primeorder", - "sha2 0.10.9", + "sha2 0.11.1", ] [[package]] @@ -2276,9 +2292,9 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "pem-rfc7468" -version = "0.7.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" dependencies = [ "base64ct", ] @@ -2349,9 +2365,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", ] [[package]] @@ -2360,8 +2376,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.2", + "spki 0.8.1", ] [[package]] @@ -2427,13 +2453,31 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.2", + "ff", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "primeorder" -version = "0.13.6" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" dependencies = [ "elliptic-curve", + "once_cell", + "primefield", + "serdect", + "wnaf", ] [[package]] @@ -2901,10 +2945,10 @@ dependencies = [ "aws-lc-rs", "base64 0.23.1", "ed25519-dalek", - "hmac 0.13.0", + "hmac", "p256", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "registry-platform-canonical-json", "serde", "serde_json", @@ -3067,12 +3111,12 @@ checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "rfc6979" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" dependencies = [ - "hmac 0.12.1", - "subtle", + "crypto-bigint", + "hmac", ] [[package]] @@ -3298,14 +3342,14 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sec1" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct", - "der", - "generic-array", - "pkcs8", + "ctutils", + "der 0.8.2", + "hybrid-array", "subtle", "zeroize", ] @@ -3442,6 +3486,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3492,10 +3546,19 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + [[package]] name = "simd_cesu8" version = "1.2.0" @@ -3570,7 +3633,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" +dependencies = [ + "base64ct", + "der 0.8.2", ] [[package]] @@ -4814,6 +4887,18 @@ dependencies = [ "wasmparser 0.254.2", ] +[[package]] +name = "wnaf" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" +dependencies = [ + "ff", + "group", + "hybrid-array", + "primefield", +] + [[package]] name = "writeable" version = "0.6.4" diff --git a/products/evidence/fuzz/Cargo.lock b/products/evidence/fuzz/Cargo.lock index 04b391d385..c621cbb578 100644 --- a/products/evidence/fuzz/Cargo.lock +++ b/products/evidence/fuzz/Cargo.lock @@ -112,9 +112,9 @@ dependencies = [ [[package]] name = "base16ct" -version = "0.2.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "base64" @@ -320,6 +320,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -346,12 +352,16 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" -version = "0.5.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +checksum = "6bb08299a6046833a879ace115697f12fd2bd6c8d3e93c909ffc9e0738e7cae0" dependencies = [ - "generic-array", - "rand_core 0.6.4", + "cpubits", + "ctutils", + "getrandom 0.4.3", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", "subtle", "zeroize", ] @@ -372,16 +382,19 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ + "getrandom 0.4.3", "hybrid-array", + "rand_core 0.10.1", ] [[package]] name = "ctutils" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12218662ccac37d9714f2191252553d67585b97df8c839cbe49e0bde278dc865" +checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" dependencies = [ "cmov", + "subtle", ] [[package]] @@ -418,6 +431,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid 0.9.6", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid 0.10.2", "pem-rfc7468", "zeroize", ] @@ -438,9 +461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid 0.9.6", "crypto-common 0.1.6", - "subtle", ] [[package]] @@ -480,16 +501,17 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecdsa" -version = "0.16.9" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ - "der", - "digest 0.10.7", + "der 0.8.2", + "digest 0.11.3", "elliptic-curve", "rfc6979", - "signature", - "spki", + "signature 3.0.0", + "spki 0.8.1", + "zeroize", ] [[package]] @@ -498,8 +520,8 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "signature 2.2.0", ] [[package]] @@ -525,19 +547,20 @@ checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "elliptic-curve" -version = "0.13.8" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" dependencies = [ "base16ct", "crypto-bigint", - "digest 0.10.7", + "crypto-common 0.2.2", + "digest 0.11.3", "ff", - "generic-array", "group", + "hybrid-array", "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", + "pkcs8 0.11.0", + "rand_core 0.10.1", "sec1", "subtle", "zeroize", @@ -562,11 +585,11 @@ dependencies = [ [[package]] name = "ff" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" dependencies = [ - "rand_core 0.6.4", + "rand_core 0.10.1", "subtle", ] @@ -639,7 +662,6 @@ checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", - "zeroize", ] [[package]] @@ -687,12 +709,12 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "group" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" dependencies = [ "ff", - "rand_core 0.6.4", + "rand_core 0.10.1", "subtle", ] @@ -702,15 +724,6 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "hmac" version = "0.13.0" @@ -726,7 +739,9 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ + "subtle", "typenum", + "zeroize", ] [[package]] @@ -979,7 +994,7 @@ dependencies = [ "js-sys", "serde", "serde_json", - "signature", + "signature 2.2.0", "zeroize", ] @@ -1171,14 +1186,15 @@ dependencies = [ [[package]] name = "p256" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" dependencies = [ "ecdsa", "elliptic-curve", + "primefield", "primeorder", - "sha2 0.10.9", + "sha2 0.11.1", ] [[package]] @@ -1206,9 +1222,9 @@ dependencies = [ [[package]] name = "pem-rfc7468" -version = "0.7.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" dependencies = [ "base64ct", ] @@ -1231,9 +1247,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", ] [[package]] @@ -1242,8 +1258,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.2", + "spki 0.8.1", ] [[package]] @@ -1285,13 +1311,31 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.2", + "ff", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "primeorder" -version = "0.13.6" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" dependencies = [ "elliptic-curve", + "once_cell", + "primefield", + "serdect", + "wnaf", ] [[package]] @@ -1472,10 +1516,10 @@ dependencies = [ "aws-lc-rs", "base64 0.23.1", "ed25519-dalek", - "hmac 0.13.0", + "hmac", "p256", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "registry-platform-canonical-json", "serde", "serde_json", @@ -1516,12 +1560,12 @@ dependencies = [ [[package]] name = "rfc6979" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" dependencies = [ - "hmac 0.12.1", - "subtle", + "crypto-bigint", + "hmac", ] [[package]] @@ -1630,14 +1674,14 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sec1" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct", - "der", - "generic-array", - "pkcs8", + "ctutils", + "der 0.8.2", + "hybrid-array", "subtle", "zeroize", ] @@ -1715,6 +1759,16 @@ dependencies = [ "unsafe-libyaml-norway", ] +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1755,10 +1809,19 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + [[package]] name = "slab" version = "0.4.12" @@ -1798,7 +1861,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" +dependencies = [ + "base64ct", + "der 0.8.2", ] [[package]] @@ -2163,6 +2236,18 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wnaf" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" +dependencies = [ + "ff", + "group", + "hybrid-array", + "primefield", +] + [[package]] name = "writeable" version = "0.6.4" diff --git a/products/platform/CHANGELOG.md b/products/platform/CHANGELOG.md index 16462eb924..c7098b49ea 100644 --- a/products/platform/CHANGELOG.md +++ b/products/platform/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Upgrade the P-256 dependency stack to use `crypto-bigint` 0.7.6, addressing + RUSTSEC-2026-0387. Key generation retains operating-system randomness and + existing SEC1 and JWK formats; signing algorithms and providers are unchanged. + - CFG-SCHEMA-5 clarifies that the shared URL hook block owns fixed-policy delivery. BReg's explicit private-network, TLS-identity and classification controls remain product-owned while using the shared delivery primitives; diff --git a/products/platform/fuzz/Cargo.lock b/products/platform/fuzz/Cargo.lock index 77ed871fb0..8272a887d9 100644 --- a/products/platform/fuzz/Cargo.lock +++ b/products/platform/fuzz/Cargo.lock @@ -97,9 +97,9 @@ dependencies = [ [[package]] name = "base16ct" -version = "0.2.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "base64" @@ -306,6 +306,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -356,12 +362,16 @@ checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crypto-bigint" -version = "0.5.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +checksum = "6bb08299a6046833a879ace115697f12fd2bd6c8d3e93c909ffc9e0738e7cae0" dependencies = [ - "generic-array", - "rand_core 0.6.4", + "cpubits", + "ctutils", + "getrandom 0.4.3", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", "subtle", "zeroize", ] @@ -382,16 +392,19 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ + "getrandom 0.4.3", "hybrid-array", + "rand_core 0.10.1", ] [[package]] name = "ctutils" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12218662ccac37d9714f2191252553d67585b97df8c839cbe49e0bde278dc865" +checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" dependencies = [ "cmov", + "subtle", ] [[package]] @@ -434,6 +447,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid 0.9.6", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" +dependencies = [ + "const-oid 0.10.2", "pem-rfc7468", "zeroize", ] @@ -454,9 +477,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid 0.9.6", "crypto-common 0.1.6", - "subtle", ] [[package]] @@ -490,16 +511,17 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "ecdsa" -version = "0.16.9" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ - "der", - "digest 0.10.7", + "der 0.8.2", + "digest 0.11.3", "elliptic-curve", "rfc6979", - "signature", - "spki", + "signature 3.0.0", + "spki 0.8.1", + "zeroize", ] [[package]] @@ -508,8 +530,8 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "signature 2.2.0", ] [[package]] @@ -535,19 +557,20 @@ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" -version = "0.13.8" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" dependencies = [ "base16ct", "crypto-bigint", - "digest 0.10.7", + "crypto-common 0.2.2", + "digest 0.11.3", "ff", - "generic-array", "group", + "hybrid-array", "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", + "pkcs8 0.11.0", + "rand_core 0.10.1", "sec1", "subtle", "zeroize", @@ -583,11 +606,11 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "ff" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" dependencies = [ - "rand_core 0.6.4", + "rand_core 0.10.1", "subtle", ] @@ -677,7 +700,6 @@ checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", - "zeroize", ] [[package]] @@ -727,12 +749,12 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "group" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" dependencies = [ "ff", - "rand_core 0.6.4", + "rand_core 0.10.1", "subtle", ] @@ -812,15 +834,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "hmac" version = "0.13.0" @@ -871,11 +884,13 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ + "subtle", "typenum", + "zeroize", ] [[package]] @@ -1210,7 +1225,7 @@ dependencies = [ "js-sys", "serde", "serde_json", - "signature", + "signature 2.2.0", "zeroize", ] @@ -1373,14 +1388,15 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "p256" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" dependencies = [ "ecdsa", "elliptic-curve", + "primefield", "primeorder", - "sha2 0.10.9", + "sha2 0.11.1", ] [[package]] @@ -1408,9 +1424,9 @@ dependencies = [ [[package]] name = "pem-rfc7468" -version = "0.7.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" dependencies = [ "base64ct", ] @@ -1433,9 +1449,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", ] [[package]] @@ -1444,8 +1460,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.2", + "spki 0.8.1", ] [[package]] @@ -1498,13 +1524,31 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.2", + "ff", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "primeorder" -version = "0.13.6" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" dependencies = [ "elliptic-curve", + "once_cell", + "primefield", + "serdect", + "wnaf", ] [[package]] @@ -1714,10 +1758,10 @@ dependencies = [ "aws-lc-rs", "base64 0.23.1", "ed25519-dalek", - "hmac 0.13.0", + "hmac", "p256", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "registry-platform-canonical-json", "serde", "serde_json", @@ -1882,12 +1926,12 @@ checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "rfc6979" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" dependencies = [ - "hmac 0.12.1", - "subtle", + "crypto-bigint", + "hmac", ] [[package]] @@ -2046,14 +2090,14 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sec1" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct", - "der", - "generic-array", - "pkcs8", + "ctutils", + "der 0.8.2", + "hybrid-array", "subtle", "zeroize", ] @@ -2142,6 +2186,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2192,10 +2246,19 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + [[package]] name = "simd_cesu8" version = "1.2.0" @@ -2241,7 +2304,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" +dependencies = [ + "base64ct", + "der 0.8.2", ] [[package]] @@ -2875,6 +2948,18 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wnaf" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" +dependencies = [ + "ff", + "group", + "hybrid-array", + "primefield", +] + [[package]] name = "writeable" version = "0.6.3"