mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Report biometric type in user verification responses (#60948)
## What changed Add `biometricType` to `userVerification/verify` responses as context alongside the signed `proof`. Successful Touch ID protected signing reports `"fingerprint"`; an unknown method reports `null`. Responses from older servers that omit the field remain supported. Carry the native method through the verification provider and app-server protocol, and update the experimental protocol exports and API documentation. ## Testing Add coverage for responses without `biometricType` and for returning `"fingerprint"` separately from the proof. Update existing verification coverage to check the `null` case. GitOrigin-RevId: c82e84a325a4d9ab1e5262e5100ffc1a043bd97e
This commit is contained in:
BIN
Binary file not shown.
@@ -59,6 +59,7 @@ const EXPERIMENTAL_CLIENT_METHOD_DEPENDENCY_TYPES: &[&str] = &[
|
||||
"ThreadSearchOccurrence",
|
||||
"ThreadSearchTextRange",
|
||||
"TurnSettingsUpdateStatus",
|
||||
"UserVerificationBiometricType",
|
||||
"UserVerificationProof",
|
||||
"UserVerificationCancellationReason",
|
||||
"UserVerificationErrorDetails",
|
||||
|
||||
@@ -152,6 +152,16 @@ pub struct UserVerificationVerifyParams {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct UserVerificationVerifyResponse {
|
||||
pub proof: UserVerificationProof,
|
||||
/// Native-reported authentication method for this signature. Older servers omit it.
|
||||
/// This is approval context, not part of the signed proof.
|
||||
pub biometric_type: Option<UserVerificationBiometricType>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
pub enum UserVerificationBiometricType {
|
||||
Fingerprint,
|
||||
}
|
||||
|
||||
/// Cancels a native verification RPC issued on this connection, not an elicitation.
|
||||
|
||||
@@ -2,6 +2,23 @@ use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn verification_response_accepts_older_servers_without_biometric_method() {
|
||||
assert_eq!(
|
||||
serde_json::from_value::<UserVerificationVerifyResponse>(json!({
|
||||
"proof": {"credentialId": "credential", "signature": "signature"}
|
||||
}))
|
||||
.unwrap(),
|
||||
UserVerificationVerifyResponse {
|
||||
proof: UserVerificationProof {
|
||||
credential_id: "credential".into(),
|
||||
signature: "signature".into(),
|
||||
},
|
||||
biometric_type: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enrollment_response_accepts_older_servers_without_public_metadata() {
|
||||
for response in [
|
||||
|
||||
@@ -196,6 +196,10 @@ prompt to close. The original RPC completes independently, with
|
||||
cannot roll back completed effects. It remains effective while a proof waits for
|
||||
outbound queue capacity, but cannot retract a response already enqueued.
|
||||
|
||||
`biometricType` reports the native method used for that successful signature when known.
|
||||
It is `"fingerprint"` for a successful Touch ID protected signing operation and `null`
|
||||
when unknown. Older servers omit it. It is context, not an additional signed proof field.
|
||||
|
||||
Canceling or resolving an elicitation does not itself stop a separate
|
||||
`userVerification/verify` RPC. Clients must cancel that RPC separately and discard
|
||||
late proofs after the approval is canceled or resolved. Only one native worker
|
||||
@@ -283,7 +287,7 @@ ChatGPT account identity.
|
||||
| `userVerification/status` | `{}` | `{credentialId, unavailableReason, unavailableMessage}` |
|
||||
| `userVerification/enroll` | `{}` | `{credentialId, algorithm?, publicKey?}` |
|
||||
| `userVerification/delete` | `{}` | `{}` |
|
||||
| `userVerification/verify` | `{challenge, title, description}` | `{proof: {credentialId, signature}}` |
|
||||
| `userVerification/verify` | `{challenge, title, description}` | `{proof: {credentialId, signature}, biometricType}` |
|
||||
| `userVerification/cancel` | `{requestId}` | `{}` |
|
||||
|
||||
Status reads local readiness without prompting or contacting a backend. A null
|
||||
|
||||
@@ -199,6 +199,13 @@ async fn run(
|
||||
NativeOperation::Verify(request) => {
|
||||
let proof = provider.verify(&request, &guard).map_err(native_error)?;
|
||||
rpc::UserVerificationVerifyResponse {
|
||||
biometric_type: proof.biometric_type.map(|biometric_type| {
|
||||
match biometric_type {
|
||||
native::UserVerificationBiometricType::Fingerprint => {
|
||||
rpc::UserVerificationBiometricType::Fingerprint
|
||||
}
|
||||
}
|
||||
}),
|
||||
proof: rpc::UserVerificationProof {
|
||||
credential_id: proof.credential_id,
|
||||
signature: proof.signature,
|
||||
|
||||
@@ -82,6 +82,7 @@ async fn user_verification_cancel_remains_effective_until_proof_enqueue_and_clea
|
||||
let cancellation = context.cancellation.clone();
|
||||
outgoing.register_request_context(context).await;
|
||||
let response = UserVerificationVerifyResponse {
|
||||
biometric_type: None,
|
||||
proof: UserVerificationProof {
|
||||
credential_id: "AQ".into(),
|
||||
signature: "Ag".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_reports_native_method_separately_from_proof() -> Result<()> {
|
||||
let mut h = Harness::new(ConnectionOrigin::Stdio, || true).await?;
|
||||
h.initialize("test-local-ui", /*opt_in*/ true).await;
|
||||
h.start_verify().await?;
|
||||
h.provider.released.store(/*val*/ true, Ordering::Release);
|
||||
let OutgoingMessage::Response(response) = h.response().await else {
|
||||
panic!("successful native verification must return its proof")
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(response.result)?,
|
||||
json!({
|
||||
"proof": {"credentialId": "credential", "signature": "signature"},
|
||||
"biometricType": "fingerprint"
|
||||
})
|
||||
);
|
||||
h.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_verification_rpc_enroll_returns_public_registration_metadata() -> Result<()> {
|
||||
let mut h = Harness::new(ConnectionOrigin::Stdio, || true).await?;
|
||||
|
||||
@@ -99,6 +99,7 @@ impl native::UserVerificationProvider for BlockingProvider {
|
||||
Ok(native::UserVerificationProof {
|
||||
credential_id: "credential".into(),
|
||||
signature: "signature".into(),
|
||||
biometric_type: Some(native::UserVerificationBiometricType::Fingerprint),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ impl native::UserVerificationProvider for Provider {
|
||||
Ok(native::UserVerificationProof {
|
||||
credential_id: "credential".into(),
|
||||
signature: "signature".into(),
|
||||
biometric_type: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -140,7 +141,7 @@ async fn signing_uses_decoded_bytes_without_a_pending_elicitation() {
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(response.payload).unwrap(),
|
||||
json!({"proof": {"credentialId": "credential", "signature": "signature"}})
|
||||
json!({"proof": {"credentialId": "credential", "signature": "signature"}, "biometricType": null})
|
||||
);
|
||||
assert_eq!(*provider.signed.lock().unwrap(), vec![vec![1, 2, 3]]);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,13 @@ pub struct UserVerificationProof {
|
||||
pub credential_id: String,
|
||||
/// Unpadded base64url of an ASN.1 DER ECDSA signature over the challenge, hashed once.
|
||||
pub signature: String,
|
||||
/// Method that authorized this signing operation, when the provider can establish it.
|
||||
pub biometric_type: Option<UserVerificationBiometricType>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum UserVerificationBiometricType {
|
||||
Fingerprint,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UserVerificationProof {
|
||||
|
||||
@@ -13,6 +13,7 @@ mod platform_macos;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod unsupported;
|
||||
|
||||
pub use credential::UserVerificationBiometricType;
|
||||
pub use credential::UserVerificationKeyCreation;
|
||||
pub use credential::UserVerificationKeyDeletion;
|
||||
pub use credential::UserVerificationKeyInfo;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use super::error;
|
||||
use super::key_protection;
|
||||
use crate::UserVerificationBiometricType;
|
||||
use crate::UserVerificationError;
|
||||
use crate::UserVerificationFailureReason;
|
||||
use crate::UserVerificationKeyCreation;
|
||||
@@ -274,6 +275,10 @@ impl UserVerificationProvider for NativeProvider {
|
||||
Ok(UserVerificationProof {
|
||||
credential_id: credential.credential_id,
|
||||
signature: URL_SAFE_NO_PAD.encode(signature?),
|
||||
// The key's biometric-only policy authorized this successful signature.
|
||||
// Reading hardware capability before successful signing is not sufficient.
|
||||
biometric_type: (unsafe { context.biometryType() } == LABiometryType::TouchID)
|
||||
.then_some(UserVerificationBiometricType::Fingerprint),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user