mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
## 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
197 lines
6.3 KiB
Rust
197 lines
6.3 KiB
Rust
//! Verifies local key lifecycle, signing, and cancellation without transport concerns.
|
|
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use std::sync::Mutex;
|
|
use std::sync::atomic::AtomicBool;
|
|
use std::sync::atomic::Ordering;
|
|
|
|
#[derive(Default)]
|
|
struct Provider {
|
|
key: Mutex<Option<native::UserVerificationKeyInfo>>,
|
|
signed: Mutex<Vec<Vec<u8>>>,
|
|
deactivate_on_sign: Option<Arc<AtomicBool>>,
|
|
}
|
|
|
|
impl native::UserVerificationProvider for Provider {
|
|
fn status(
|
|
&self,
|
|
_guard: &native::UserVerificationRequestGuard,
|
|
) -> Result<native::UserVerificationStatus, native::UserVerificationError> {
|
|
let credential = self.key.lock().unwrap().clone();
|
|
Ok(native::UserVerificationStatus {
|
|
unavailable_reason: credential
|
|
.is_none()
|
|
.then_some(native::UserVerificationUnavailableReason::CredentialMissing),
|
|
unavailable_message: credential.is_none().then(|| "No key".into()),
|
|
credential,
|
|
})
|
|
}
|
|
fn ensure_key(
|
|
&self,
|
|
guard: &native::UserVerificationRequestGuard,
|
|
) -> Result<native::UserVerificationKeyCreation, native::UserVerificationError> {
|
|
guard.check()?;
|
|
let mut key = self.key.lock().unwrap();
|
|
let created = key.is_none();
|
|
let credential = key
|
|
.get_or_insert_with(|| native::UserVerificationKeyInfo {
|
|
credential_id: "credential".into(),
|
|
algorithm: "ecdsaP256Sha256X962".into(),
|
|
public_key: "public-key".into(),
|
|
})
|
|
.clone();
|
|
Ok(native::UserVerificationKeyCreation {
|
|
created,
|
|
credential,
|
|
})
|
|
}
|
|
fn delete(
|
|
&self,
|
|
guard: &native::UserVerificationRequestGuard,
|
|
) -> Result<native::UserVerificationKeyDeletion, native::UserVerificationError> {
|
|
guard.check()?;
|
|
Ok(native::UserVerificationKeyDeletion {
|
|
deleted_credential_id: self.key.lock().unwrap().take().map(|key| key.credential_id),
|
|
})
|
|
}
|
|
fn verify(
|
|
&self,
|
|
request: &native::UserVerificationRequest,
|
|
guard: &native::UserVerificationRequestGuard,
|
|
) -> Result<native::UserVerificationProof, native::UserVerificationError> {
|
|
guard.check()?;
|
|
self.signed.lock().unwrap().push(request.challenge.clone());
|
|
if let Some(active) = &self.deactivate_on_sign {
|
|
active.store(/*val*/ false, Ordering::Release);
|
|
}
|
|
Ok(native::UserVerificationProof {
|
|
credential_id: "credential".into(),
|
|
signature: "signature".into(),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn local_enrollment_reuses_key_and_status_and_delete_have_no_signing_effects() {
|
|
let provider = Arc::new(Provider::default());
|
|
for _ in 0..2 {
|
|
let response = run(
|
|
NativeOperation::Enroll,
|
|
provider.clone(),
|
|
native::UserVerificationRequestGuard::default(),
|
|
Arc::new(Semaphore::new(/*permits*/ 1)),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
serde_json::to_value(response.payload).unwrap(),
|
|
json!({
|
|
"credentialId": "credential",
|
|
"algorithm": "ecdsaP256Sha256X962",
|
|
"publicKey": "public-key"
|
|
})
|
|
);
|
|
}
|
|
let response = run(
|
|
NativeOperation::Status,
|
|
provider.clone(),
|
|
native::UserVerificationRequestGuard::default(),
|
|
Arc::new(Semaphore::new(/*permits*/ 1)),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
serde_json::to_value(response.payload).unwrap(),
|
|
json!({"credentialId": "credential", "unavailableReason": null, "unavailableMessage": null})
|
|
);
|
|
for _ in 0..2 {
|
|
let response = run(
|
|
NativeOperation::Delete,
|
|
provider.clone(),
|
|
native::UserVerificationRequestGuard::default(),
|
|
Arc::new(Semaphore::new(/*permits*/ 1)),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(serde_json::to_value(response.payload).unwrap(), json!({}));
|
|
}
|
|
assert_eq!(*provider.key.lock().unwrap(), None);
|
|
assert!(provider.signed.lock().unwrap().is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn signing_uses_decoded_bytes_without_a_pending_elicitation() {
|
|
let provider = Arc::new(Provider::default());
|
|
let request = validate(rpc::UserVerificationVerifyParams {
|
|
challenge: "AQID".into(),
|
|
title: "Approve".into(),
|
|
description: "Action".into(),
|
|
})
|
|
.unwrap();
|
|
let response = run(
|
|
NativeOperation::Verify(request),
|
|
provider.clone(),
|
|
native::UserVerificationRequestGuard::default(),
|
|
Arc::new(Semaphore::new(/*permits*/ 1)),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
serde_json::to_value(response.payload).unwrap(),
|
|
json!({"proof": {"credentialId": "credential", "signature": "signature"}})
|
|
);
|
|
assert_eq!(*provider.signed.lock().unwrap(), vec![vec![1, 2, 3]]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn identity_change_during_native_prompt_discards_proof() {
|
|
let active = Arc::new(AtomicBool::new(/*v*/ true));
|
|
let provider = Arc::new(Provider {
|
|
deactivate_on_sign: Some(active.clone()),
|
|
..Default::default()
|
|
});
|
|
let guard = native::UserVerificationRequestGuard::with_activity_check(move || {
|
|
active.load(Ordering::Acquire)
|
|
});
|
|
let response = run(
|
|
NativeOperation::Verify(native::UserVerificationRequest {
|
|
challenge: vec![1],
|
|
title: "Approve".into(),
|
|
description: String::new(),
|
|
}),
|
|
provider,
|
|
guard,
|
|
Arc::new(Semaphore::new(/*permits*/ 1)),
|
|
)
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert_eq!(
|
|
response.data,
|
|
Some(json!({"type": "cancelled", "reason": "interrupted"}))
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn cancelled_operation_cannot_create_a_key() {
|
|
let provider = Arc::new(Provider::default());
|
|
let guard = native::UserVerificationRequestGuard::default();
|
|
guard.cancel();
|
|
let response = run(
|
|
NativeOperation::Enroll,
|
|
provider.clone(),
|
|
guard,
|
|
Arc::new(Semaphore::new(/*permits*/ 1)),
|
|
)
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert_eq!(
|
|
response.data,
|
|
Some(json!({"type": "cancelled", "reason": "interrupted"}))
|
|
);
|
|
assert_eq!(*provider.key.lock().unwrap(), None);
|
|
}
|