mirror of
https://github.com/openai/codex.git
synced 2026-09-15 12:08:01 +00:00
Return public key metadata from user verification enrollment (#44877)
## Why The trusted UI host needs the local credential's public metadata to complete backend registration. `userVerification/enroll` previously returned only `credentialId`. ## What changed - Return `algorithm` and `publicKey` for newly created or reused credentials. The algorithm is `ecdsaP256Sha256X962`; the public key is unpadded base64url SPKI-DER. - Keep both fields optional in the protocol for compatibility with older app-servers, while current servers populate both. - Document caller-owned backend registration and revocation, including checking metadata, signing an enrollment challenge with `userVerification/verify`, matching credential IDs, and preserving the authenticated account throughout registration. ## Testing Add protocol coverage for absent or null metadata and populated-response round trips. Add an RPC assertion for enrollment metadata and extend the local enrollment test to check metadata when creating and reusing a key. GitOrigin-RevId: f0726e8c430e27559e1a01ba2ea635993cbeba09
This commit is contained in:
Binary file not shown.
@@ -109,11 +109,19 @@ pub struct UserVerificationStatusResponse {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct UserVerificationEnrollParams {}
|
||||
|
||||
/// Public metadata for a created or reused local credential. The caller completes
|
||||
/// backend registration; this response does not establish server enrollment.
|
||||
/// Older app-servers omit the metadata fields; callers must check both before registration.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct UserVerificationEnrollResponse {
|
||||
pub credential_id: String,
|
||||
// TODO: Make both metadata fields required after the experimental/alpha rollout
|
||||
// guarantees them on the oldest supported app-server version.
|
||||
pub algorithm: Option<String>,
|
||||
/// Unpadded base64url of the SubjectPublicKeyInfo DER encoding.
|
||||
pub public_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
|
||||
@@ -2,6 +2,35 @@ use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn enrollment_response_accepts_older_servers_without_public_metadata() {
|
||||
for response in [
|
||||
json!({"credentialId": "credential"}),
|
||||
json!({"credentialId": "credential", "algorithm": null, "publicKey": null}),
|
||||
] {
|
||||
assert_eq!(
|
||||
serde_json::from_value::<UserVerificationEnrollResponse>(response).unwrap(),
|
||||
UserVerificationEnrollResponse {
|
||||
credential_id: "credential".into(),
|
||||
algorithm: None,
|
||||
public_key: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
let response = UserVerificationEnrollResponse {
|
||||
credential_id: "credential".into(),
|
||||
algorithm: Some("ecdsaP256Sha256X962".into()),
|
||||
public_key: Some("public-key".into()),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::from_value::<UserVerificationEnrollResponse>(
|
||||
serde_json::to_value(&response).unwrap()
|
||||
)
|
||||
.unwrap(),
|
||||
response
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_readiness_retains_credential_during_biometric_unavailability() {
|
||||
let status = UserVerificationStatusResponse {
|
||||
|
||||
@@ -65,7 +65,7 @@ ChatGPT account identity.
|
||||
| Method | Params | Result |
|
||||
| --- | --- | --- |
|
||||
| `userVerification/status` | `{}` | `{credentialId, unavailableReason, unavailableMessage}` |
|
||||
| `userVerification/enroll` | `{}` | `{credentialId}` |
|
||||
| `userVerification/enroll` | `{}` | `{credentialId, algorithm?, publicKey?}` |
|
||||
| `userVerification/delete` | `{}` | `{}` |
|
||||
| `userVerification/verify` | `{challenge, title, description}` | `{proof: {credentialId, signature}}` |
|
||||
| `userVerification/cancel` | `{requestId}` | `{}` |
|
||||
@@ -74,9 +74,17 @@ Status reads local readiness without prompting or contacting a backend. A null
|
||||
`unavailableReason` means local checks passed, not that registration is valid.
|
||||
Unsupported platforms and missing account identity are reported in the status
|
||||
response's `unavailableReason` field.
|
||||
The initial enrollment creates or reuses the local key only. Backend
|
||||
registration and revocation are integration TODOs; local success is not server
|
||||
enrollment. Deletion currently removes that local key synchronously.
|
||||
Enrollment creates or reuses the local key and returns its public metadata. The
|
||||
`publicKey` is unpadded base64url SPKI-DER; `algorithm` is `ecdsaP256Sha256X962`.
|
||||
During the experimental rollout, `algorithm` and `publicKey` are optional for
|
||||
compatibility with older app-servers. Current servers populate both fields;
|
||||
callers must check that both are present and non-null before backend registration.
|
||||
The trusted UI host owns backend registration: obtain an enrollment challenge,
|
||||
sign it with `userVerification/verify`, check that the proof's `credentialId`
|
||||
matches this response, and submit the public metadata and proof to the backend.
|
||||
Local success is not server enrollment. The caller must preserve the authenticated
|
||||
account across this flow and reconcile uncertain registration before retrying.
|
||||
Deletion removes the local key; the caller owns backend revocation.
|
||||
Enrollment and deletion coordinate credential lifecycle; callers do not issue
|
||||
separate generate or rotate commands. Identity comes from the authenticated
|
||||
account; this API exposes no caller-selected scope.
|
||||
|
||||
@@ -181,10 +181,12 @@ async fn run(
|
||||
}
|
||||
NativeOperation::Enroll => {
|
||||
let key = provider.ensure_key(&guard).map_err(native_error)?;
|
||||
// TODO: start enrollment, sign proof of possession, then finish registration.
|
||||
// This implementation establishes the local key only.
|
||||
// The trusted caller owns backend registration and can use verify
|
||||
// to sign the enrollment challenge with this local credential.
|
||||
rpc::UserVerificationEnrollResponse {
|
||||
credential_id: key.credential.credential_id,
|
||||
algorithm: Some(key.credential.algorithm),
|
||||
public_key: Some(key.credential.public_key),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -11,6 +11,26 @@ use serde_json::json;
|
||||
use std::sync::atomic::Ordering;
|
||||
use tokio::time::timeout;
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_verification_rpc_enroll_returns_public_registration_metadata() -> Result<()> {
|
||||
let mut h = Harness::new(ConnectionOrigin::Stdio, || true).await?;
|
||||
h.initialize("test-local-ui", /*opt_in*/ true).await;
|
||||
h.send(/*id*/ 1, "userVerification/enroll", json!({})).await;
|
||||
let OutgoingMessage::Response(response) = h.response().await else {
|
||||
panic!("local enrollment must return public metadata")
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(response.result)?,
|
||||
json!({
|
||||
"credentialId": "credential",
|
||||
"algorithm": "ecdsaP256Sha256X962",
|
||||
"publicKey": "public-key"
|
||||
})
|
||||
);
|
||||
h.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_verification_rpc_auth_revision_discards_native_proof() -> Result<()> {
|
||||
let mut h = Harness::new(ConnectionOrigin::Stdio, || true).await?;
|
||||
|
||||
@@ -57,9 +57,18 @@ impl native::UserVerificationProvider for BlockingProvider {
|
||||
}
|
||||
fn ensure_key(
|
||||
&self,
|
||||
_guard: &native::UserVerificationRequestGuard,
|
||||
guard: &native::UserVerificationRequestGuard,
|
||||
) -> Result<native::UserVerificationKeyCreation, native::UserVerificationError> {
|
||||
unreachable!("this test must not create keys")
|
||||
guard.check()?;
|
||||
self.calls.fetch_add(/*val*/ 1, Ordering::SeqCst);
|
||||
Ok(native::UserVerificationKeyCreation {
|
||||
created: false,
|
||||
credential: native::UserVerificationKeyInfo {
|
||||
credential_id: "credential".into(),
|
||||
algorithm: "ecdsaP256Sha256X962".into(),
|
||||
public_key: "public-key".into(),
|
||||
},
|
||||
})
|
||||
}
|
||||
fn delete(
|
||||
&self,
|
||||
|
||||
@@ -87,7 +87,11 @@ async fn local_enrollment_reuses_key_and_status_and_delete_have_no_signing_effec
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(response.payload).unwrap(),
|
||||
json!({"credentialId": "credential"})
|
||||
json!({
|
||||
"credentialId": "credential",
|
||||
"algorithm": "ecdsaP256Sha256X962",
|
||||
"publicKey": "public-key"
|
||||
})
|
||||
);
|
||||
}
|
||||
let response = run(
|
||||
|
||||
Reference in New Issue
Block a user