mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
Add user-verification provider abstractions and RPC adapters (#43547)
## What changed - Introduce `codex-user-verification` with a provider interface for credential status, creation, deletion, and challenge signing. Include typed errors, shared cancellation guards, and hashed account-user key namespaces. - Add P-256 public-key encoding as unpadded base64url SPKI DER, derive credential IDs from its SHA-256 digest, and redact proof fields in debug output. - Add app-server helpers to validate challenge and display-text bounds and map provider errors to typed RPC errors without exposing provider diagnostics. The platform implementation reports verification as unsupported. App-server requests still return typed unavailability, with the message updated to mention build or account availability. ## Testing Add tests for credential encoding and signature verification, invalid curve points, cancellation across guard clones, stable and distinct account namespaces, and invalid challenge or display values. Update the app-server unavailability test for the revised message. GitOrigin-RevId: fe4a4eb37c68d7fdc547704e76aa257abf3e9c81
This commit is contained in:
12
codex-rs/Cargo.lock
generated
12
codex-rs/Cargo.lock
generated
@@ -2225,6 +2225,7 @@ dependencies = [
|
||||
"codex-state",
|
||||
"codex-thread-store",
|
||||
"codex-tools",
|
||||
"codex-user-verification",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-cli",
|
||||
@@ -4769,6 +4770,17 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-user-verification"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"p256",
|
||||
"pretty_assertions",
|
||||
"sha2 0.10.9",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-utils-absolute-path"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -101,6 +101,7 @@ members = [
|
||||
"otel",
|
||||
"otel-trace-websocket",
|
||||
"tui",
|
||||
"user-verification",
|
||||
"tools",
|
||||
"v8-poc",
|
||||
"websocket-client",
|
||||
@@ -295,6 +296,7 @@ codex-utils-sleep-inhibitor = { path = "utils/sleep-inhibitor" }
|
||||
codex-utils-stream-parser = { path = "utils/stream-parser" }
|
||||
codex-utils-string = { path = "utils/string" }
|
||||
codex-utils-template = { path = "utils/template" }
|
||||
codex-user-verification = { path = "user-verification" }
|
||||
codex-v8-poc = { path = "v8-poc" }
|
||||
codex-workload-identity = { path = "workload-identity" }
|
||||
codex-windows-sandbox = { path = "windows-sandbox-rs" }
|
||||
|
||||
@@ -63,6 +63,7 @@ codex-shell-command = { workspace = true }
|
||||
codex-skills = { workspace = true }
|
||||
codex-skills-extension = { workspace = true }
|
||||
codex-utils-cli = { workspace = true }
|
||||
codex-user-verification = { workspace = true }
|
||||
codex-utils-pty = { workspace = true }
|
||||
codex-backend-client = { workspace = true }
|
||||
codex-file-search = { workspace = true }
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
//! Dispatch boundary for experimental verification APIs.
|
||||
//! Native operations are introduced separately from the public contract.
|
||||
//! Local verification value conversion; provider dispatch follows in the next change.
|
||||
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::UserVerificationErrorDetails;
|
||||
use codex_app_server_protocol::UserVerificationUnavailableReason;
|
||||
#[allow(dead_code)]
|
||||
#[path = "user_verification_adapter.rs"]
|
||||
mod adapter;
|
||||
|
||||
pub(crate) fn unavailable() -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: -32603,
|
||||
message: "User verification is not available in this build.".into(),
|
||||
data: serde_json::to_value(UserVerificationErrorDetails::Unavailable {
|
||||
reason: UserVerificationUnavailableReason::ProviderUnavailable,
|
||||
})
|
||||
.ok(),
|
||||
}
|
||||
}
|
||||
pub(crate) use adapter::unavailable;
|
||||
|
||||
130
codex-rs/app-server/src/user_verification_adapter.rs
Normal file
130
codex-rs/app-server/src/user_verification_adapter.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
//! Validates local verification requests and maps native results into the app-server API.
|
||||
//! Native diagnostics stay private; clients receive bounded input errors and typed reasons.
|
||||
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use codex_app_server_protocol as rpc;
|
||||
use codex_user_verification as native;
|
||||
|
||||
pub(super) fn validate(
|
||||
params: rpc::UserVerificationVerifyParams,
|
||||
) -> Result<native::UserVerificationRequest, rpc::JSONRPCErrorError> {
|
||||
let invalid = || {
|
||||
error(
|
||||
rpc::UserVerificationErrorDetails::InvalidRequest {
|
||||
reason: rpc::UserVerificationInvalidRequestReason::InvalidParams,
|
||||
},
|
||||
"Invalid verification challenge or display text.",
|
||||
)
|
||||
};
|
||||
if params.challenge.len() > 5462
|
||||
|| params.title.is_empty()
|
||||
|| params.title.len() > 256
|
||||
|| params.description.len() > 4096
|
||||
{
|
||||
return Err(invalid());
|
||||
}
|
||||
let challenge = URL_SAFE_NO_PAD
|
||||
.decode(¶ms.challenge)
|
||||
.map_err(|_| invalid())?;
|
||||
if challenge.is_empty() || challenge.len() > 4096 {
|
||||
return Err(invalid());
|
||||
}
|
||||
Ok(native::UserVerificationRequest {
|
||||
challenge,
|
||||
title: params.title,
|
||||
description: params.description,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn unavailable() -> rpc::JSONRPCErrorError {
|
||||
error(
|
||||
rpc::UserVerificationErrorDetails::Unavailable {
|
||||
reason: rpc::UserVerificationUnavailableReason::ProviderUnavailable,
|
||||
},
|
||||
"User verification is not available in this build or account.",
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn unavailable_reason(
|
||||
reason: native::UserVerificationUnavailableReason,
|
||||
) -> rpc::UserVerificationUnavailableReason {
|
||||
match reason {
|
||||
native::UserVerificationUnavailableReason::CredentialMissing => {
|
||||
rpc::UserVerificationUnavailableReason::CredentialMissing
|
||||
}
|
||||
native::UserVerificationUnavailableReason::BiometricsUnavailable => {
|
||||
rpc::UserVerificationUnavailableReason::BiometricsUnavailable
|
||||
}
|
||||
native::UserVerificationUnavailableReason::ProviderUnavailable => {
|
||||
rpc::UserVerificationUnavailableReason::ProviderUnavailable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn native_error(value: native::UserVerificationError) -> rpc::JSONRPCErrorError {
|
||||
let (details, message) = match value {
|
||||
native::UserVerificationError::Unavailable { reason, .. } => (
|
||||
rpc::UserVerificationErrorDetails::Unavailable {
|
||||
reason: unavailable_reason(reason),
|
||||
},
|
||||
"Local user verification is unavailable.",
|
||||
),
|
||||
native::UserVerificationError::Cancelled { reason, .. } => (
|
||||
rpc::UserVerificationErrorDetails::Cancelled {
|
||||
reason: match reason {
|
||||
native::UserVerificationCancellationReason::UserCancelled => {
|
||||
rpc::UserVerificationCancellationReason::UserCancelled
|
||||
}
|
||||
native::UserVerificationCancellationReason::Interrupted => {
|
||||
rpc::UserVerificationCancellationReason::Interrupted
|
||||
}
|
||||
},
|
||||
},
|
||||
"User verification was cancelled.",
|
||||
),
|
||||
native::UserVerificationError::Failed { reason, .. } => (
|
||||
rpc::UserVerificationErrorDetails::Failed {
|
||||
reason: match reason {
|
||||
native::UserVerificationFailureReason::AuthenticationFailed => {
|
||||
rpc::UserVerificationFailureReason::AuthenticationFailed
|
||||
}
|
||||
native::UserVerificationFailureReason::Timeout => {
|
||||
rpc::UserVerificationFailureReason::Timeout
|
||||
}
|
||||
native::UserVerificationFailureReason::ProviderError => {
|
||||
rpc::UserVerificationFailureReason::ProviderError
|
||||
}
|
||||
},
|
||||
},
|
||||
"User verification failed.",
|
||||
),
|
||||
};
|
||||
error(details, message)
|
||||
}
|
||||
|
||||
pub(super) fn error(
|
||||
data: rpc::UserVerificationErrorDetails,
|
||||
message: &str,
|
||||
) -> rpc::JSONRPCErrorError {
|
||||
rpc::JSONRPCErrorError {
|
||||
code: if matches!(
|
||||
data,
|
||||
rpc::UserVerificationErrorDetails::InvalidRequest { .. }
|
||||
) {
|
||||
-32602
|
||||
} else {
|
||||
-32603
|
||||
},
|
||||
message: message.into(),
|
||||
data: Some(
|
||||
serde_json::to_value(data).unwrap_or_else(
|
||||
|_| serde_json::json!({"type": "failed", "reason": "providerError"}),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "user_verification_adapter_tests.rs"]
|
||||
mod tests;
|
||||
33
codex-rs/app-server/src/user_verification_adapter_tests.rs
Normal file
33
codex-rs/app-server/src/user_verification_adapter_tests.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
//! Validates the wire-to-native boundary before any provider work can start.
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn invalid_challenge_and_display_values_are_rejected_before_native_work() {
|
||||
for (challenge, title, description) in [
|
||||
("".into(), "Approve".into(), String::new()),
|
||||
("AQ==".into(), "Approve".into(), String::new()),
|
||||
(
|
||||
URL_SAFE_NO_PAD.encode(vec![0; 4097]),
|
||||
"Approve".into(),
|
||||
String::new(),
|
||||
),
|
||||
("AQ".into(), String::new(), String::new()),
|
||||
("AQ".into(), "é".repeat(129), String::new()),
|
||||
("AQ".into(), "Approve".into(), "x".repeat(4097)),
|
||||
] {
|
||||
let error = validate(rpc::UserVerificationVerifyParams {
|
||||
challenge,
|
||||
title,
|
||||
description,
|
||||
})
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
error.data,
|
||||
Some(json!({"type": "invalidRequest", "reason": "invalidParams"}))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ async fn user_verification_without_provider_returns_typed_unavailability() -> Re
|
||||
response.error,
|
||||
JSONRPCErrorError {
|
||||
code: -32603,
|
||||
message: "User verification is not available in this build.".into(),
|
||||
message: "User verification is not available in this build or account.".into(),
|
||||
data: Some(json!({"type": "unavailable", "reason": "providerUnavailable"})),
|
||||
}
|
||||
);
|
||||
|
||||
6
codex-rs/user-verification/BUILD.bazel
Normal file
6
codex-rs/user-verification/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "user-verification",
|
||||
crate_name = "codex_user_verification",
|
||||
)
|
||||
21
codex-rs/user-verification/Cargo.toml
Normal file
21
codex-rs/user-verification/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "codex-user-verification"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = { workspace = true }
|
||||
p256 = { version = "0.13", default-features = false, features = ["arithmetic", "pkcs8", "std"] }
|
||||
sha2 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
p256 = { version = "0.13", features = ["ecdsa"] }
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
86
codex-rs/user-verification/src/credential.rs
Normal file
86
codex-rs/user-verification/src/credential.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
//! Public credential encoding and results; private keys never cross this boundary.
|
||||
|
||||
use crate::UserVerificationError;
|
||||
use crate::UserVerificationFailureReason;
|
||||
use crate::UserVerificationUnavailableReason;
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use p256::pkcs8::EncodePublicKey as _;
|
||||
use sha2::Digest as _;
|
||||
use sha2::Sha256;
|
||||
|
||||
/// Validated display text and exact challenge bytes supplied by the trusted calling UI.
|
||||
#[derive(Clone)]
|
||||
pub struct UserVerificationRequest {
|
||||
pub challenge: Vec<u8>,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct UserVerificationKeyInfo {
|
||||
pub credential_id: String,
|
||||
pub algorithm: String,
|
||||
/// Unpadded base64url of the P-256 SubjectPublicKeyInfo DER encoding.
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
impl UserVerificationKeyInfo {
|
||||
pub fn from_sec1_public_key(bytes: &[u8]) -> Result<Self, UserVerificationError> {
|
||||
let public_key = p256::PublicKey::from_sec1_bytes(bytes)
|
||||
.map_err(|_| invalid_public_key())?
|
||||
.to_public_key_der()
|
||||
.map_err(|_| invalid_public_key())?;
|
||||
let bytes = public_key.as_bytes();
|
||||
Ok(Self {
|
||||
credential_id: URL_SAFE_NO_PAD.encode(Sha256::digest(bytes)),
|
||||
algorithm: "ecdsaP256Sha256X962".to_string(),
|
||||
public_key: URL_SAFE_NO_PAD.encode(bytes),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_public_key() -> UserVerificationError {
|
||||
UserVerificationError::Failed {
|
||||
reason: UserVerificationFailureReason::ProviderError,
|
||||
message: "could not encode the user-verification public key".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct UserVerificationKeyCreation {
|
||||
pub created: bool,
|
||||
pub credential: UserVerificationKeyInfo,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct UserVerificationKeyDeletion {
|
||||
pub deleted_credential_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct UserVerificationStatus {
|
||||
pub credential: Option<UserVerificationKeyInfo>,
|
||||
pub unavailable_reason: Option<UserVerificationUnavailableReason>,
|
||||
pub unavailable_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct UserVerificationProof {
|
||||
pub credential_id: String,
|
||||
/// Unpadded base64url of an ASN.1 DER ECDSA signature over the challenge, hashed once.
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UserVerificationProof {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("UserVerificationProof")
|
||||
.field("credential_id", &"[REDACTED]")
|
||||
.field("signature", &"[REDACTED]")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "credential_tests.rs"]
|
||||
mod tests;
|
||||
43
codex-rs/user-verification/src/credential_tests.rs
Normal file
43
codex-rs/user-verification/src/credential_tests.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
//! Checks that the exported credential interoperates with DER-based service verification.
|
||||
|
||||
use super::*;
|
||||
use p256::ecdsa::Signature;
|
||||
use p256::ecdsa::SigningKey;
|
||||
use p256::ecdsa::VerifyingKey;
|
||||
use p256::ecdsa::signature::Signer as _;
|
||||
use p256::ecdsa::signature::Verifier as _;
|
||||
use p256::pkcs8::DecodePublicKey as _;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn exported_spki_verifies_the_exact_challenge_signature() {
|
||||
let key = SigningKey::from_bytes((&[7_u8; 32]).into()).expect("valid signing key");
|
||||
let info = UserVerificationKeyInfo::from_sec1_public_key(
|
||||
key.verifying_key()
|
||||
.to_encoded_point(/*compress*/ false)
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("encode public key");
|
||||
let der = URL_SAFE_NO_PAD.decode(&info.public_key).expect("base64url");
|
||||
let verifier = VerifyingKey::from_public_key_der(&der).expect("valid SPKI DER");
|
||||
let challenge = b"the exact server-issued challenge";
|
||||
let signature: Signature = key.sign(challenge);
|
||||
let signature = Signature::from_der(signature.to_der().as_bytes()).expect("DER signature");
|
||||
verifier
|
||||
.verify(challenge, &signature)
|
||||
.expect("valid signature");
|
||||
assert!(verifier.verify(b"different challenge", &signature).is_err());
|
||||
assert_eq!(
|
||||
info,
|
||||
UserVerificationKeyInfo {
|
||||
credential_id: URL_SAFE_NO_PAD.encode(Sha256::digest(&der)),
|
||||
algorithm: "ecdsaP256Sha256X962".to_string(),
|
||||
public_key: URL_SAFE_NO_PAD.encode(der),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_curve_point_is_rejected() {
|
||||
assert!(UserVerificationKeyInfo::from_sec1_public_key(&[4_u8; 65]).is_err());
|
||||
}
|
||||
42
codex-rs/user-verification/src/error.rs
Normal file
42
codex-rs/user-verification/src/error.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
//! Stable error categories, with provider diagnostics kept outside public error messages.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum UserVerificationUnavailableReason {
|
||||
CredentialMissing,
|
||||
BiometricsUnavailable,
|
||||
ProviderUnavailable,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum UserVerificationCancellationReason {
|
||||
UserCancelled,
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum UserVerificationFailureReason {
|
||||
AuthenticationFailed,
|
||||
Timeout,
|
||||
ProviderError,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error, PartialEq, Eq)]
|
||||
pub enum UserVerificationError {
|
||||
#[error("user verification is unavailable: {message}")]
|
||||
Unavailable {
|
||||
reason: UserVerificationUnavailableReason,
|
||||
message: String,
|
||||
},
|
||||
#[error("user verification was cancelled: {message}")]
|
||||
Cancelled {
|
||||
reason: UserVerificationCancellationReason,
|
||||
message: String,
|
||||
},
|
||||
#[error("user verification failed: {message}")]
|
||||
Failed {
|
||||
reason: UserVerificationFailureReason,
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
51
codex-rs/user-verification/src/guard.rs
Normal file
51
codex-rs/user-verification/src/guard.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! Cancellation and caller-owned identity checks for queued native operations.
|
||||
|
||||
use crate::UserVerificationCancellationReason;
|
||||
use crate::UserVerificationError;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
/// Invalidates queued work and suppresses late results. Native providers observe cancellation
|
||||
/// during authentication and request dismissal of their active OS prompt.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct UserVerificationRequestGuard {
|
||||
cancelled: Arc<AtomicBool>,
|
||||
activity_check: Option<Arc<dyn Fn() -> bool + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl UserVerificationRequestGuard {
|
||||
/// The callback checks a captured identity; it must be nonblocking and must not prompt.
|
||||
pub fn with_activity_check(activity_check: impl Fn() -> bool + Send + Sync + 'static) -> Self {
|
||||
Self {
|
||||
cancelled: Arc::default(),
|
||||
activity_check: Some(Arc::new(activity_check)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
self.cancelled.store(/*val*/ true, Ordering::Release);
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
if self.activity_check.as_ref().is_some_and(|check| !check()) {
|
||||
self.cancel();
|
||||
}
|
||||
!self.cancelled.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn check(&self) -> Result<(), UserVerificationError> {
|
||||
if self.is_active() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(UserVerificationError::Cancelled {
|
||||
reason: UserVerificationCancellationReason::Interrupted,
|
||||
message: "the verification operation is no longer active".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "guard_tests.rs"]
|
||||
mod tests;
|
||||
32
codex-rs/user-verification/src/guard_tests.rs
Normal file
32
codex-rs/user-verification/src/guard_tests.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
//! Checks cancellation shared across queued operations and captured-identity callbacks.
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn identity_change_permanently_cancels_all_guard_clones() {
|
||||
let identity_matches = Arc::new(AtomicBool::new(/*v*/ true));
|
||||
let activity = Arc::clone(&identity_matches);
|
||||
let guard =
|
||||
UserVerificationRequestGuard::with_activity_check(move || activity.load(Ordering::Acquire));
|
||||
let queued = guard.clone();
|
||||
assert!(queued.check().is_ok());
|
||||
identity_matches.store(/*val*/ false, Ordering::Release);
|
||||
assert_eq!(
|
||||
queued.check(),
|
||||
Err(UserVerificationError::Cancelled {
|
||||
reason: UserVerificationCancellationReason::Interrupted,
|
||||
message: "the verification operation is no longer active".to_string(),
|
||||
})
|
||||
);
|
||||
identity_matches.store(/*val*/ true, Ordering::Release);
|
||||
assert!(!guard.is_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelling_one_clone_invalidates_queued_work() {
|
||||
let guard = UserVerificationRequestGuard::default();
|
||||
let queued = guard.clone();
|
||||
guard.cancel();
|
||||
assert!(!queued.is_active());
|
||||
}
|
||||
25
codex-rs/user-verification/src/key_namespace.rs
Normal file
25
codex-rs/user-verification/src/key_namespace.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
//! Keychain labels isolate account-user identities within the fixed plugin-service scope.
|
||||
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use sha2::Digest as _;
|
||||
use sha2::Sha256;
|
||||
|
||||
/// An opaque account-user namespace. App-server authenticates and selects the identity.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct UserVerificationKeyNamespace {
|
||||
pub(crate) label: String,
|
||||
}
|
||||
|
||||
impl UserVerificationKeyNamespace {
|
||||
pub fn new(account_user_id: &str) -> Self {
|
||||
let identity = URL_SAFE_NO_PAD.encode(Sha256::digest(account_user_id.as_bytes()));
|
||||
Self {
|
||||
label: format!("com.openai.codex.user-verification.plugin-service.v1.{identity}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "key_namespace_tests.rs"]
|
||||
mod tests;
|
||||
12
codex-rs/user-verification/src/key_namespace_tests.rs
Normal file
12
codex-rs/user-verification/src/key_namespace_tests.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! Account-user identities share one service scope without exposing raw identifiers.
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn namespaces_are_stable_and_separate_accounts() {
|
||||
let first = UserVerificationKeyNamespace::new("account-user-one");
|
||||
assert_eq!(first, UserVerificationKeyNamespace::new("account-user-one"));
|
||||
assert_ne!(first, UserVerificationKeyNamespace::new("account-user-two"));
|
||||
assert!(!first.label.contains("account-user-one"));
|
||||
}
|
||||
70
codex-rs/user-verification/src/lib.rs
Normal file
70
codex-rs/user-verification/src/lib.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
//! Device credentials and signing, independent of RPC routing, UI, and backend registration.
|
||||
|
||||
mod credential;
|
||||
mod error;
|
||||
mod guard;
|
||||
mod key_namespace;
|
||||
mod unsupported;
|
||||
|
||||
pub use credential::UserVerificationKeyCreation;
|
||||
pub use credential::UserVerificationKeyDeletion;
|
||||
pub use credential::UserVerificationKeyInfo;
|
||||
pub use credential::UserVerificationProof;
|
||||
pub use credential::UserVerificationRequest;
|
||||
pub use credential::UserVerificationStatus;
|
||||
pub use error::UserVerificationCancellationReason;
|
||||
pub use error::UserVerificationError;
|
||||
pub use error::UserVerificationFailureReason;
|
||||
pub use error::UserVerificationUnavailableReason;
|
||||
pub use guard::UserVerificationRequestGuard;
|
||||
pub use key_namespace::UserVerificationKeyNamespace;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Performs local credential operations for one captured account-user identity.
|
||||
/// Implementations never perform network registration. Blocking implementations must run off
|
||||
/// the async executor and check the guard after waiting, before effects, and before returning.
|
||||
pub trait UserVerificationProvider: Send + Sync {
|
||||
/// Reads local readiness without creating credentials or prompting for authentication.
|
||||
fn status(
|
||||
&self,
|
||||
guard: &UserVerificationRequestGuard,
|
||||
) -> Result<UserVerificationStatus, UserVerificationError>;
|
||||
|
||||
/// Creates a protected key only if none exists. Success does not mean server enrollment.
|
||||
fn ensure_key(
|
||||
&self,
|
||||
guard: &UserVerificationRequestGuard,
|
||||
) -> Result<UserVerificationKeyCreation, UserVerificationError>;
|
||||
|
||||
/// Removes the local key idempotently. Backend revocation belongs to the caller.
|
||||
fn delete(
|
||||
&self,
|
||||
guard: &UserVerificationRequestGuard,
|
||||
) -> Result<UserVerificationKeyDeletion, UserVerificationError>;
|
||||
|
||||
/// Authenticates and signs 1–4096 challenge bytes without interpreting an elicitation.
|
||||
/// The caller owns approval UI, request correlation, and the captured identity check.
|
||||
fn verify(
|
||||
&self,
|
||||
request: &UserVerificationRequest,
|
||||
guard: &UserVerificationRequestGuard,
|
||||
) -> Result<UserVerificationProof, UserVerificationError>;
|
||||
}
|
||||
|
||||
/// Reports whether this build contains a native provider, independently of local readiness.
|
||||
pub fn platform_supported() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Probes biometric hardware without reading credentials, prompting, or checking enrollment.
|
||||
/// This performs local OS work; async callers should run it off their executor during setup.
|
||||
pub fn device_supported() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn platform_provider(
|
||||
namespace: UserVerificationKeyNamespace,
|
||||
) -> Arc<dyn UserVerificationProvider> {
|
||||
let _ = namespace.label;
|
||||
Arc::new(unsupported::UnsupportedProvider)
|
||||
}
|
||||
53
codex-rs/user-verification/src/unsupported.rs
Normal file
53
codex-rs/user-verification/src/unsupported.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
//! Typed unavailable behavior until a native provider is available for this platform.
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub(crate) struct UnsupportedProvider;
|
||||
|
||||
fn unavailable() -> UserVerificationError {
|
||||
UserVerificationError::Unavailable {
|
||||
reason: UserVerificationUnavailableReason::ProviderUnavailable,
|
||||
message: "this platform does not support user verification".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
impl UserVerificationProvider for UnsupportedProvider {
|
||||
fn status(
|
||||
&self,
|
||||
guard: &UserVerificationRequestGuard,
|
||||
) -> Result<UserVerificationStatus, UserVerificationError> {
|
||||
guard.check()?;
|
||||
Ok(UserVerificationStatus {
|
||||
credential: None,
|
||||
unavailable_reason: Some(UserVerificationUnavailableReason::ProviderUnavailable),
|
||||
unavailable_message: Some(
|
||||
"this platform does not support user verification".to_string(),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn ensure_key(
|
||||
&self,
|
||||
guard: &UserVerificationRequestGuard,
|
||||
) -> Result<UserVerificationKeyCreation, UserVerificationError> {
|
||||
guard.check()?;
|
||||
Err(unavailable())
|
||||
}
|
||||
|
||||
fn delete(
|
||||
&self,
|
||||
guard: &UserVerificationRequestGuard,
|
||||
) -> Result<UserVerificationKeyDeletion, UserVerificationError> {
|
||||
guard.check()?;
|
||||
Err(unavailable())
|
||||
}
|
||||
|
||||
fn verify(
|
||||
&self,
|
||||
_request: &UserVerificationRequest,
|
||||
guard: &UserVerificationRequestGuard,
|
||||
) -> Result<UserVerificationProof, UserVerificationError> {
|
||||
guard.check()?;
|
||||
Err(unavailable())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user