Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@

## Unreleased

### vta-service 0.13.18 — step-up approve-request minted as 0.2; inbound stays bilingual

The deferred follow-up to 0.13.17 (#870): the minted step-up approve-request
moves from `auth/step-up/approve-request/0.1` to `/0.2`. Receivers were
migrated first — vta-mobile-core (#871) and the browser plugin
(OpenVTC/vta-browser-plugin#103) accept both request minors, and the webvh
control plane accepts both approve-response minors
(affinidi/affinidi-webvh-service#147) — so this is the producer-side cutover.

- `mint_pending_step_up` emits the `/0.2` type URI and the 0.2 camelCase
`acceptableEvidence` spelling (`didSigned`; 0.1 said `did-signed`). That
spelling is the **only** payload difference between the minors — same
required members, same optional hints, same ttl semantics. The signing is
unchanged from #870: `eddsa-jcs-2022`, `assertionMethod`, `{vta_did}#key-0`,
proof last over the complete document including `payload.ext` (the embedded
Cierge `authorizationContext` carriage is unaffected and still covered by
the proof). The DIDComm push type follows the document to `/0.2`.
- **Inbound stays bilingual.** The approve-response dispatcher keeps accepting
0.1 and 0.2 (approvers in the field answer with either during the
transition), and the DIDComm router's canonical step-up-approve registration
now accepts the `/0.2` request URI beside `/0.1` and the legacy
`vta/step-up/*/1.0`, echoing the caller's own minor in the response type.
- The stored `PendingStepUp.acceptable_evidence` record keeps the internal
kebab canonical form — it is state, not wire — so in-flight pending
step-ups from 0.13.17 remain consumable across the deploy.
- New integration test: the gate's minted 0.2 document verifies end-to-end
(VTA proof via `di_proof`, issuer == proof VM DID) and a 0.1-flavored
signed approve-response completes the 0.2-minted step-up, with the ack
echoing the approver's 0.1 family.

### vta-service 0.13.17 — the step-up approve-request is signed (spec: proof REQUIRED)

Part of the ecosystem-wide "signed request legs" push
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vta-service/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "vta-service"
description = "Service for Verifiable Trust Agents operating in Verifiable Trust Communities"
version = "0.13.17"
version = "0.13.18"
edition.workspace = true
publish.workspace = true
authors.workspace = true
Expand Down
29 changes: 23 additions & 6 deletions vta-service/src/messaging/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1680,16 +1680,30 @@ pub(crate) const STEP_UP_APPROVE_RESPONSE_TYPE: &str =
/// registered on the DIDComm router alongside the legacy `vta/step-up/*` URI
/// (issue #517). A spec-conformant caller that targets the canonical
/// `spec/auth/step-up/approve-request/0.1` is now handled too; the response
/// echoes the request's version family (canonical request → canonical
/// response), so neither the legacy plugin nor a spec-0.2 caller breaks.
/// echoes the request's version family AND minor version (0.1 request →
/// 0.1 response, 0.2 → 0.2), so neither the legacy plugin nor a spec caller
/// on either minor breaks. **Kept for inbound compat** during the 0.1→0.2
/// transition — approvers in the field still send 0.1.
pub(crate) const STEP_UP_APPROVE_REQUEST_CANONICAL: &str =
"https://trusttasks.org/spec/auth/step-up/approve-request/0.1";

/// The 0.2 flavor of [`STEP_UP_APPROVE_REQUEST_CANONICAL`] — same payload
/// shape (this request body carries no renamed enum values), so one handler
/// serves both minors and only the echoed response type differs.
pub(crate) const STEP_UP_APPROVE_REQUEST_CANONICAL_0_2: &str =
"https://trusttasks.org/spec/auth/step-up/approve-request/0.2";

/// Canonical Trust Task registry URI for the step-up approval response,
/// emitted when the request arrived on [`STEP_UP_APPROVE_REQUEST_CANONICAL`].
pub(crate) const STEP_UP_APPROVE_RESPONSE_CANONICAL: &str =
"https://trusttasks.org/spec/auth/step-up/approve-response/0.1";

/// The 0.2 response flavor, emitted when the request arrived on
/// [`STEP_UP_APPROVE_REQUEST_CANONICAL_0_2`]. (The webvh control plane —
/// the relying party for this flow — accepts both response minors.)
pub(crate) const STEP_UP_APPROVE_RESPONSE_CANONICAL_0_2: &str =
"https://trusttasks.org/spec/auth/step-up/approve-response/0.2";

/// Request body for [`handle_step_up_approve`]. The `rpDid` alias accepts a
/// spec-conformant (lowerCamelCase) producer; the legacy `rp_did` keeps the
/// existing plugin working (issue #517).
Expand Down Expand Up @@ -1734,10 +1748,13 @@ pub async fn handle_step_up_approve(
}
};

// Echo the version family of the inbound request so a canonical
// (`spec/auth/step-up/…`) caller gets a canonical response and the legacy
// (`vta/step-up/…/1.0`) plugin gets the legacy response.
let response_type = if message.typ == STEP_UP_APPROVE_REQUEST_CANONICAL {
// Echo the version family (and minor) of the inbound request so a
// canonical (`spec/auth/step-up/…`) caller gets the matching canonical
// response and the legacy (`vta/step-up/…/1.0`) plugin gets the legacy
// response.
let response_type = if message.typ == STEP_UP_APPROVE_REQUEST_CANONICAL_0_2 {
STEP_UP_APPROVE_RESPONSE_CANONICAL_0_2
} else if message.typ == STEP_UP_APPROVE_REQUEST_CANONICAL {
STEP_UP_APPROVE_RESPONSE_CANONICAL
} else {
STEP_UP_APPROVE_RESPONSE_TYPE
Expand Down
1 change: 1 addition & 0 deletions vta-service/src/messaging/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@ pub async fn dispatch(
// ── Step-up approval (always) ────────────────────────────────────
if t == handlers::STEP_UP_APPROVE_REQUEST_TYPE
|| t == handlers::STEP_UP_APPROVE_REQUEST_CANONICAL
|| t == handlers::STEP_UP_APPROVE_REQUEST_CANONICAL_0_2
{
return finish(handlers::handle_step_up_approve(ctx, msg, Extension(vta_state)).await);
}
Expand Down
32 changes: 24 additions & 8 deletions vta-service/src/trust_tasks/step_up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
//! pending step-up, dispatches on `evidence.kind`, and elevates the session
//! lands alongside it.
//!
//! The *request* leg (`auth/step-up/approve-request/0.1`, minted by
//! The *request* leg (`auth/step-up/approve-request/0.2`, minted by
//! [`mint_pending_step_up`]) is **signed by this VTA** — `eddsa-jcs-2022`,
//! `assertionMethod`, issuer DID == the proof's `verificationMethod` DID —
//! the same shape as `task-consent` ([`super::consent_request`]) and the
Expand Down Expand Up @@ -538,8 +538,11 @@ async fn load_step_up_signing_secret(state: &AppState, vta_did: &str) -> Result<
}

/// Mint a pending step-up and build the **signed**
/// `auth/step-up/approve-request/0.1` document the AAL1 caller hands to its
/// approver (wallet / VTA).
/// `auth/step-up/approve-request/0.2` document the AAL1 caller hands to its
/// approver (wallet / VTA). 0.2 differs from 0.1 only in the type URI and the
/// `acceptableEvidence` spelling (`did-signed` → `didSigned`); every fielded
/// receiver (vta-mobile-core, the browser plugin) accepts both request
/// flavors, so the mint moved cleanly to 0.2 (#870's deferred follow-up).
///
/// A fresh challenge is bound server-side to the caller's
/// `{session_id, subject, targetAcr=aal2, acceptableEvidence}` via the
Expand Down Expand Up @@ -574,7 +577,11 @@ async fn mint_pending_step_up(
// the reason-only form.
authorization_context: Option<&Value>,
) -> Result<Value, ()> {
// The *stored* pending record keeps the kebab canonical form
// (`did-signed`) that `vti_common::auth::step_up` documents — it's internal
// state, not wire. The 0.2 wire spelling is camelCase (`didSigned`).
let acceptable = vec!["did-signed".to_string(), "webauthn".to_string()];
let acceptable_wire = vec!["didSigned".to_string(), "webauthn".to_string()];

// 256 bits of challenge entropy (two UUIDv4s) — comfortably over the spec's
// ≥128-bit / ≥16-char minimum, using deps already present.
Expand Down Expand Up @@ -604,7 +611,7 @@ async fn mint_pending_step_up(

let mut doc = json!({
"id": format!("urn:uuid:{}", Uuid::new_v4()),
"type": "https://trusttasks.org/spec/auth/step-up/approve-request/0.1",
"type": "https://trusttasks.org/spec/auth/step-up/approve-request/0.2",
"issuer": vta_did,
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": {
Expand All @@ -613,7 +620,7 @@ async fn mint_pending_step_up(
"challenge": challenge,
"reason": reason,
"targetAcr": STEP_UP_TARGET_ACR,
"acceptableEvidence": acceptable,
"acceptableEvidence": acceptable_wire,
"ttl": STEP_UP_TTL_SECS,
},
});
Expand Down Expand Up @@ -734,10 +741,12 @@ fn step_up_denied_response() -> Response {
}

/// Trust Task `type` of a step-up approve-request (also the DIDComm message
/// `type` used when pushing one to an approver).
/// `type` used when pushing one to an approver). 0.2 — matches the minted
/// document; both fielded approver stacks (vta-mobile-core #871, the browser
/// plugin) accept 0.1 and 0.2 request URIs.
#[cfg(feature = "didcomm")]
const STEP_UP_APPROVE_REQUEST_TYPE: &str =
"https://trusttasks.org/spec/auth/step-up/approve-request/0.1";
"https://trusttasks.org/spec/auth/step-up/approve-request/0.2";

/// Pure route selection for a delegated push: given the approver DID and the
/// VTA's configured mediator, pick the mediator to forward through.
Expand Down Expand Up @@ -1392,13 +1401,18 @@ mod tests {
assert_eq!(v["requiredAcr"], "aal2");
assert_eq!(
v["approveRequest"]["type"],
"https://trusttasks.org/spec/auth/step-up/approve-request/0.1"
"https://trusttasks.org/spec/auth/step-up/approve-request/0.2"
);
assert_eq!(v["approveRequest"]["issuer"], vta_did);
assert_eq!(v["approveRequest"]["recipient"], "did:key:zHolder");
assert_eq!(v["approveRequest"]["payload"]["sessionId"], "sess-9");
assert_eq!(v["approveRequest"]["payload"]["targetAcr"], "aal2");
assert_eq!(v["approveRequest"]["payload"]["reason"], "rotate keys");
// 0.2 wire spelling: camelCase `didSigned` (0.1 said `did-signed`).
assert_eq!(
v["approveRequest"]["payload"]["acceptableEvidence"],
json!(["didSigned", "webauthn"])
);
let challenge = v["approveRequest"]["payload"]["challenge"]
.as_str()
.expect("challenge string");
Expand Down Expand Up @@ -1427,6 +1441,8 @@ mod tests {
// self-approval recorded the subject as its own authorized approver.
assert_eq!(pending.approver, "did:key:zHolder");
assert_eq!(pending.target_acr, "aal2");
// The stored record keeps the internal kebab canonical form even
// though the 0.2 wire says `didSigned` (it's state, not wire).
assert_eq!(
pending.acceptable_evidence,
vec!["did-signed".to_string(), "webauthn".to_string()]
Expand Down
7 changes: 6 additions & 1 deletion vta-service/tests/api_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,12 @@ async fn acl_mutation_requires_step_up() {
let ar = &body["approveRequest"];
assert_eq!(
ar["type"],
"https://trusttasks.org/spec/auth/step-up/approve-request/0.1"
"https://trusttasks.org/spec/auth/step-up/approve-request/0.2"
);
// 0.2 wire spelling of the evidence enum (0.1 said `did-signed`).
assert_eq!(
ar["payload"]["acceptableEvidence"],
json!(["didSigned", "webauthn"])
);
assert_eq!(ar["recipient"], "did:key:z6MkAdmin");
assert_eq!(ar["payload"]["targetAcr"], "aal2");
Expand Down
Loading