diff --git a/crates/agent-identity-conformance/src/dvls.rs b/crates/agent-identity-conformance/src/dvls.rs index 790cd11d0..445b86e2a 100644 --- a/crates/agent-identity-conformance/src/dvls.rs +++ b/crates/agent-identity-conformance/src/dvls.rs @@ -436,7 +436,7 @@ pub(crate) struct CertificateRecord { serial_number: Option, #[serde(rename = "validFrom", deserialize_with = "timestamp")] pub(crate) not_before: OffsetDateTime, - #[serde(rename = "validUntil", deserialize_with = "timestamp")] + #[serde(rename = "validTo", deserialize_with = "timestamp")] pub(crate) not_after: OffsetDateTime, /// The issuing root's thumbprint. #[serde(rename = "issuerThumbprint")] @@ -447,7 +447,7 @@ pub(crate) struct CertificateRecord { /// The current certificate, as an identity's summary shows it. #[derive(Clone, Debug, PartialEq, Deserialize)] pub(crate) struct CurrentCertificate { - #[serde(rename = "validUntil", deserialize_with = "timestamp")] + #[serde(rename = "validTo", deserialize_with = "timestamp")] pub(crate) not_after: OffsetDateTime, /// The issuing root's thumbprint. #[serde(rename = "issuerThumbprint")] @@ -670,7 +670,7 @@ pub(crate) struct RootSummary { #[serde(default, deserialize_with = "optional_timestamp")] valid_from: Option, #[serde(default, deserialize_with = "optional_timestamp")] - valid_until: Option, + valid_to: Option, } #[derive(Clone, Debug, PartialEq, Deserialize)] @@ -1209,7 +1209,7 @@ mod tests { "enrollmentTokenName": "test", "createdOn": "2026-09-25T12:00:00Z", "isRenewalRequested": false, - "certificate": { "issuerThumbprint": "root-id", "validUntil": "2026-12-24T12:00:00Z" } + "certificate": { "issuerThumbprint": "root-id", "validTo": "2026-12-24T12:00:00Z" } }) } diff --git a/crates/agent-identity-mock/README.md b/crates/agent-identity-mock/README.md index 898e787d3..4a029de41 100644 --- a/crates/agent-identity-mock/README.md +++ b/crates/agent-identity-mock/README.md @@ -121,7 +121,7 @@ Each operation requires the permission the table lists, and a session without it Enums are PascalCase. An identity's `status` is `Active` (an unexpired current or pending certificate), `Grace` (no unexpired certificate, but one within renewal grace), `Expired` (beyond renewal grace) or `Revoked`, as the DVLS intent's "Terminology" defines. A certificate's `status` is `Pending`, `Current` or `Retired`, a token's `status` is `Active`, `Exhausted`, `Expired` or `Revoked` (each winning over those before it), and the rotation's `phase` is `Idle` or `Rotating`. -Timestamps are RFC 3339 UTC with `Z` and named `*On`; certificate and root validity is `validFrom` and `validUntil`. +Timestamps are RFC 3339 UTC with `Z` and named `*On`; certificate and root validity is `validFrom` and `validTo`. A date-time parameter is UTC unless it carries an offset, and keeps its fraction of a second: token expiries, rotation deadlines and last-seen filters compare and return it exactly. List operations take `pageNumber` (default 1), `pageSize` (default 25, at most 100 identities or 500 tokens), `sortOrder` (`1` ascending by default, `0` descending) and `sortField`, and return `{ currentPage, data, pageSize, totalCount, totalPage }`. A page past the last is 400, and page 1 of an empty list is its last page. @@ -134,14 +134,14 @@ An ID that is not a GUID is 404, like an unknown one. | GET | `/api/v1/agent-identity/enrollment-token` | View | Page of every token, revoked ones included; `sortField` is `createdOn` (default), `name` or `expiresOn`, with ties in creation order. | | GET | `/api/v1/agent-identity/enrollment-token/{id}` | View | `AgentIdentityEnrollmentToken`, without the full token; 404 when unknown. | | POST | `/api/v1/agent-identity/enrollment-token/{id}/revoke` | Manage | 204, idempotent, or 404 when unknown. The token stays listed and readable, with `status` `Revoked`. It can still replay its own existing current key without side effects, while a new key gets `token_invalid`. | -| GET | `/api/v1/agent-identity` | View | Page of identities in creation order (`sortField` is `createdOn` only). Query: `view=Summary\|Full`, repeated `metadataKey`, repeated `status`, repeated `enrollmentTokenId`, `issuerThumbprint` (the current certificate's issuer), `lastSeenFrom` and `lastSeenTo` (inclusive), and `search` (case-insensitive friendly-name substring); filters combine with AND. The Summary view carries `id`, `friendlyName`, `status`, `isConnected`, `lastSeenOn`, `certificate { issuerThumbprint, validUntil }` while the identity has a current certificate, and the requested `metadata` keys, omitted when none match. | +| GET | `/api/v1/agent-identity` | View | Page of identities in creation order (`sortField` is `createdOn` only). Query: `view=Summary\|Full`, repeated `metadataKey`, repeated `status`, repeated `enrollmentTokenId`, `issuerThumbprint` (the current certificate's issuer), `lastSeenFrom` and `lastSeenTo` (inclusive), and `search` (case-insensitive friendly-name substring); filters combine with AND. The Summary view carries `id`, `friendlyName`, `status`, `isConnected`, `lastSeenOn`, `certificate { issuerThumbprint, validTo }` while the identity has a current certificate, and the requested `metadata` keys, omitted when none match. | | GET | `/api/v1/agent-identity/{id}` | View | Full view: the Summary members, all `metadata`, `certificates` (newest first), `createdOn`, `enrollmentTokenId`, `enrollmentTokenName`, `isRenewalRequested`, and `revokedOn` once revoked. | | PATCH | `/api/v1/agent-identity/{id}` | Manage | `{ friendlyName? }` (1..255 scalar values without leading or trailing white space); absent or `null` changes nothing; 200 → Full view. | | POST | `/api/v1/agent-identity/{id}/revoke` | Revoke | 204, idempotent; closes live streams PERMISSION_DENIED + `identity_revoked`. | | DELETE | `/api/v1/agent-identity/{id}` | Manage | 204, or 409 unless the identity is revoked. | | POST | `/api/v1/agent-identity/{id}/request-renewal` | Manage | 202; sets the flag and pushes `RenewRequested{reason:"admin"}` to live streams; 409 when the identity is revoked. | | POST | `/api/v1/agent-identity/rotation` | Rotate root | Body `{ deadlineOn? }`, `{}` for the rotation maximum: the latest `notAfter` among the old root's unexpired current or pending certificates of non-revoked identities, or now when there are none. A deadline at or before now is an emergency rotation. 202 → `AgentIdentityRotation`; 409 while rotating, without a root, or with `root_key_unavailable`; 422 for a deadline after the rotation maximum. | -| GET | `/api/v1/agent-identity/rotation` | View | `{ phase, startedOn?, deadlineOn?, oldRoot?, newRoot?, oldRootActiveIdentityCount }`, each root `{ thumbprint, validFrom, validUntil }`; the count includes each non-revoked identity with an unexpired current or pending old-root certificate, and is 0 while `Idle`. | +| GET | `/api/v1/agent-identity/rotation` | View | `{ phase, startedOn?, deadlineOn?, oldRoot?, newRoot?, oldRootActiveIdentityCount }`, each root `{ thumbprint, validFrom, validTo }`; the count includes each non-revoked identity with an unexpired current or pending old-root certificate, and is 0 while `Idle`. | | GET | `/api/v1/agent-identity/settings` | View | `{ authorityId, isAgentChannelEnabled, isAgentChannelAvailable, certificateLifetimeDays, rotationNotificationsPerSecond }`, by default `true`, `true`, `90` and `10`. | | PATCH | `/api/v1/agent-identity/settings` | Manage | `{ isAgentChannelEnabled?, certificateLifetimeDays? (1..365), rotationNotificationsPerSecond? (1..1000) }` → settings. An `isAgentChannelEnabled` that would grow a config past one channel envelope is 400 on that member, and nothing changes. | diff --git a/crates/agent-identity-mock/src/api_admin/mod.rs b/crates/agent-identity-mock/src/api_admin/mod.rs index ded51c55d..0e6eabd1c 100644 --- a/crates/agent-identity-mock/src/api_admin/mod.rs +++ b/crates/agent-identity-mock/src/api_admin/mod.rs @@ -660,7 +660,7 @@ fn identity_view(state: &State, identity: &Identity, now: i64) -> Value { if let Some(current) = identity.current_cert() { view["certificate"] = json!({ "issuerThumbprint": current.issuer, - "validUntil": rfc3339(current.not_after), + "validTo": rfc3339(current.not_after), }); } view @@ -696,7 +696,7 @@ fn certificate_view(cert: &Cert) -> Value { }, "thumbprint": cert.thumbprint, "validFrom": rfc3339(cert.not_before), - "validUntil": rfc3339(cert.not_after), + "validTo": rfc3339(cert.not_after), }) } @@ -875,7 +875,7 @@ fn rotation_view(state: &State, now: i64) -> Value { json!({ "thumbprint": root.thumbprint, "validFrom": rfc3339(root.not_before), - "validUntil": rfc3339(root.not_after), + "validTo": rfc3339(root.not_after), }) }) }; diff --git a/crates/agent-identity-mock/src/api_admin/tests.rs b/crates/agent-identity-mock/src/api_admin/tests.rs index da9812741..ac2324b6a 100644 --- a/crates/agent-identity-mock/src/api_admin/tests.rs +++ b/crates/agent-identity-mock/src/api_admin/tests.rs @@ -974,7 +974,7 @@ async fn token_has_no_delete_operation() -> anyhow::Result<()> { // ==== Identities ==== -/// The `(thumbprint, serialNumber, validFrom, validUntil)` of a DER certificate, as `AgentIdentityCertificate` defines +/// The `(thumbprint, serialNumber, validFrom, validTo)` of a DER certificate, as `AgentIdentityCertificate` defines /// them: the thumbprint is the base64url of the SHA-256 of the DER encoding, and the serial number is hexadecimal. fn certificate_fields(der: &[u8]) -> anyhow::Result<(String, String, String, String)> { use der::Decode as _; @@ -1006,10 +1006,10 @@ fn certificate_fields(der: &[u8]) -> anyhow::Result<(String, String, String, Str async fn get_identity_returns_the_full_view() -> anyhow::Result<()> { let (app, now) = frozen_app()?; let (id, chain) = enroll_with_chain(&app, json!({ "hostname": "host-a" })).await?; - let (thumbprint, serial, valid_from, valid_until) = certificate_fields(&chain[0])?; + let (thumbprint, serial, valid_from, valid_to) = certificate_fields(&chain[0])?; let (root, ..) = certificate_fields(&chain[1])?; // The certificate follows the default 90-day lifetime. - assert_eq!((&valid_from, &valid_until), (&rfc3339(now), &rfc3339(now + 90 * DAY))); + assert_eq!((&valid_from, &valid_to), (&rfc3339(now), &rfc3339(now + 90 * DAY))); let token_id = app.state.lock().await.identities[&id].token_id; let (status, view) = call(&app, Method::GET, &format!("/{id}"), None).await?; @@ -1021,7 +1021,7 @@ async fn get_identity_returns_the_full_view() -> anyhow::Result<()> { "status": "Active", "isConnected": false, "lastSeenOn": rfc3339(now), - "certificate": { "issuerThumbprint": root, "validUntil": valid_until }, + "certificate": { "issuerThumbprint": root, "validTo": valid_to }, "metadata": { "hostname": "host-a" }, "certificates": [{ "issuerThumbprint": root, @@ -1029,7 +1029,7 @@ async fn get_identity_returns_the_full_view() -> anyhow::Result<()> { "status": "Current", "thumbprint": thumbprint, "validFrom": valid_from, - "validUntil": valid_until, + "validTo": valid_to, }], "createdOn": rfc3339(now), "enrollmentTokenId": token_id, @@ -1435,7 +1435,7 @@ async fn rotation_defaults_to_the_rotation_maximum() -> anyhow::Result<()> { json!({ "thumbprint": root.thumbprint, "validFrom": rfc3339(root.not_before), - "validUntil": rfc3339(root.not_after), + "validTo": rfc3339(root.not_after), }) }; let new_root = state.issuing_root().thumbprint.clone(); @@ -1654,7 +1654,7 @@ async fn certificate_lifetime_applies_to_new_certificates() -> anyhow::Result<() assert_eq!((status, &settings["certificateLifetimeDays"]), (200, &json!(7))); let id = enroll(&app, json!({})).await?; let (_, view) = call(&app, Method::GET, &format!("/{id}"), None).await?; - assert_eq!(view["certificate"]["validUntil"], rfc3339(now + 7 * DAY)); + assert_eq!(view["certificate"]["validTo"], rfc3339(now + 7 * DAY)); Ok(()) } @@ -1666,7 +1666,7 @@ async fn leaf_lifetime_fault_overrides_the_setting() -> anyhow::Result<()> { app.state.lock().await.faults.leaf_lifetime_secs = Some(60); let id = enroll(&app, json!({})).await?; let (_, view) = call(&app, Method::GET, &format!("/{id}"), None).await?; - assert_eq!(view["certificate"]["validUntil"], rfc3339(now + 60)); + assert_eq!(view["certificate"]["validTo"], rfc3339(now + 60)); Ok(()) }