Skip to content

Commit 63ff711

Browse files
committed
fix(notary): validate federation profile scopes
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
1 parent 75c1069 commit 63ff711

5 files changed

Lines changed: 79 additions & 12 deletions

File tree

‎crates/registry-notary-core/src/config/federation.rs‎

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,15 @@ impl FederationConfig {
214214
validate_federation_https_url("federation.peers[].allowed_purposes[]", purpose)?;
215215
}
216216
for profile in &peer.allowed_profiles {
217-
if !profile_ids.contains(profile.as_str()) {
218-
return invalid_federation(
219-
"federation.peers[].allowed_profiles must reference an evaluation profile",
220-
);
221-
}
217+
let profile_config = self
218+
.evaluation_profiles
219+
.iter()
220+
.find(|candidate| candidate.id == profile.as_str())
221+
.ok_or_else(|| EvidenceConfigError::InvalidFederationConfig {
222+
reason: "federation.peers[].allowed_profiles must reference an evaluation profile"
223+
.to_string(),
224+
})?;
225+
validate_federation_peer_profile_scopes(evidence, peer, profile_config)?;
222226
}
223227
}
224228
Ok(())
@@ -273,6 +277,44 @@ fn validate_federation_claim_inputs(
273277
Ok(())
274278
}
275279

280+
fn validate_federation_peer_profile_scopes(
281+
evidence: &EvidenceConfig,
282+
peer: &FederationPeerConfig,
283+
profile: &FederationEvaluationProfileConfig,
284+
) -> Result<(), EvidenceConfigError> {
285+
let available_scopes: HashSet<&str> =
286+
peer.evaluation_scopes.iter().map(String::as_str).collect();
287+
let mut pending = vec![profile.claim_id.as_str()];
288+
let mut visited = HashSet::new();
289+
while let Some(claim_id) = pending.pop() {
290+
if !visited.insert(claim_id) {
291+
continue;
292+
}
293+
let claim = evidence
294+
.claims
295+
.iter()
296+
.find(|candidate| candidate.id == claim_id)
297+
.ok_or_else(|| EvidenceConfigError::InvalidFederationConfig {
298+
reason: format!(
299+
"federation evaluation profile '{}' references an incomplete claim dependency closure",
300+
profile.id
301+
),
302+
})?;
303+
if let Some(scope) = claim
304+
.required_scopes
305+
.iter()
306+
.find(|scope| !available_scopes.contains(scope.as_str()))
307+
{
308+
return invalid_federation(format!(
309+
"federation peer '{}' evaluation_scopes must include required scope '{}' for profile '{}' claim '{}'",
310+
peer.node_id, scope, profile.id, claim.id
311+
));
312+
}
313+
pending.extend(claim.depends_on.iter().map(String::as_str));
314+
}
315+
Ok(())
316+
}
317+
276318
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
277319
#[serde(deny_unknown_fields)]
278320
pub struct FederationSigningConfig {

‎crates/registry-notary-core/src/config/tests/infrastructure.rs‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ pub(super) fn valid_federation_config() -> StandaloneRegistryNotaryConfig {
285285
"https://purpose.example.gov/social-protection/service-delivery".to_string(),
286286
],
287287
allowed_profiles: vec!["disability_status_predicate".to_string()],
288-
evaluation_scopes: vec!["civil_registry:evidence_verification".to_string()],
288+
evaluation_scopes: vec!["registry:consult:test-source".to_string()],
289289
..FederationPeerConfig::default()
290290
}],
291291
evaluation_profiles: vec![FederationEvaluationProfileConfig {
@@ -350,6 +350,25 @@ pub(super) fn federation_profile_rejects_relay_inputs_unavailable_from_subject()
350350
);
351351
}
352352

353+
#[test]
354+
pub(super) fn federation_peer_rejects_profile_with_missing_dependency_scope() {
355+
let mut config = valid_federation_config();
356+
let mut dependency = config.evidence.claims[0].clone();
357+
dependency.id = "disability-status-dependency".to_string();
358+
dependency.required_scopes = vec!["registry:consult:dependency".to_string()];
359+
config.evidence.claims[0].depends_on = vec![dependency.id.clone()];
360+
config.evidence.claims.push(dependency);
361+
362+
let reason = expect_federation_error(&config);
363+
364+
assert!(
365+
reason.contains(
366+
"evaluation_scopes must include required scope 'registry:consult:dependency'"
367+
),
368+
"unexpected: {reason}"
369+
);
370+
}
371+
353372
#[test]
354373
pub(super) fn federation_signing_key_must_reference_active_named_signing_key() {
355374
let mut config = valid_federation_config();
@@ -404,7 +423,7 @@ allowed_purposes:
404423
allowed_profiles:
405424
- disability_status_predicate
406425
evaluation_scopes:
407-
- civil_registry:evidence_verification
426+
- registry:consult:test-source
408427
"#,
409428
)
410429
.expect("private-network peer YAML parses");

‎docs/site/src/content/docs/spec/rs-terms.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ The registry stack comprises four formal products. Product names are Title Case.
8989

9090
**wallet**: A holder-owned application that stores credentials and presents them to verifiers. Registry Notary issues credentials; the wallet that stores, unlocks, and presents them is a separate component owned by the holder.
9191

92-
**subject access**: Registry Notary authorization pattern where an authenticated OIDC principal is the protected requester for configured claim evaluation. Registry Notary has two subject access modes: `subject_access` (direct subject access), where the authenticated principal is the subject, and `delegated_subject_access` (delegated subject access), where the authenticated principal is the requester and a configured dependent target can be evaluated only when scoped authorization details name the same target, relationship, and proof claim. Subject access never supplies claim evidence; every allowed claim remains registry-backed. Registry Stack product term.
92+
**subject access**: Registry Notary authorization pattern where an authenticated OIDC principal is the protected requester for configured claim evaluation. Registry Notary has two subject access modes: `subject_bound` (direct subject access), where the authenticated principal is the subject, and `delegated_subject_access` (delegated subject access), where the authenticated principal is the requester and a configured dependent target can be evaluated only when scoped authorization details name the same target, relationship, and proof claim. Subject access never supplies claim evidence; every allowed claim remains registry-backed. Registry Stack product term.
9393

9494
**delegated subject access**: Registry Notary authorization mode `delegated_subject_access`. The requester is derived from the authenticated principal, the request target and scoped authorization details identify the same configured dependent subject, the relationship is derived from scoped authorization details, and the dependent claim can continue only after the compiler-pinned Relay proof consultation evaluates to boolean `true`. This is distinct from static-peer delegated evaluation under Notary federation. Registry Stack product term.
9595

‎products/notary/docs/federated-evaluation-operator-guide.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,12 @@ The local `peers` block is authoritative. Manifest metadata helps partners
112112
configure each other, but it does not grant access.
113113

114114
`evaluation_scopes` are the scopes assigned to the authenticated federation
115-
principal when authorizing the selected local claim. They do not grant registry
116-
or Relay source access. `max_claim_result_age_seconds` bounds the age of the
117-
local claim result's `issued_at` timestamp.
115+
principal when authorizing the selected local claim. For every allowed profile,
116+
the list must include each `required_scopes` entry from the selected claim and
117+
its dependency closure. Registry Notary rejects incomplete peer policy at
118+
startup. These scopes do not grant registry or Relay source access.
119+
`max_claim_result_age_seconds` bounds the age of the local claim result's
120+
`issued_at` timestamp.
118121

119122
The federation endpoint can select only a claim with
120123
`evidence_mode.type: registry_backed`. The verified federation request `jti`

‎products/notary/specs/federated-evaluation-mvp-spec.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,10 @@ until every applicable item below is satisfied and reviewed.
534534
- Registry Notary rejects startup config when `node_id` and `issuer` do not
535535
satisfy the MVP binding rule.
536536
- Registry Notary rejects startup config when a federation evaluation profile
537-
references a `registry_backed` claim.
537+
does not reference a `registry_backed` claim.
538+
- Registry Notary rejects startup config when a peer's `evaluation_scopes`
539+
omit a `required_scopes` entry from an allowed profile's selected claim or
540+
dependency closure.
538541
- `POST /federation/v1/evaluations` is not mounted unless federation is enabled.
539542
- Request bodies over `inbound_body_limit_bytes` are rejected before full body
540543
buffering and before JWT parsing.

0 commit comments

Comments
 (0)