1#![allow(clippy::empty_line_after_doc_comments)]
59
60mod clock;
61mod errors;
62mod rng;
63
64pub use clock::MobileClock;
65pub use errors::ChioMobileError;
66pub use rng::MobileRng;
67
68use serde::{Deserialize, Serialize};
69
70use chio_core_types::capability::{
71 attenuation::ScopeHash, crypto_floor::CapabilityCryptoFloor, features::CapabilityNegotiation,
72 token::CapabilityToken,
73};
74use chio_core_types::crypto::{Ed25519Backend, Keypair, PublicKey};
75use chio_core_types::receipt::body::ChioReceiptBody;
76use chio_custody_hw::{
77 verify_app_attest, verify_mobile_receipt_chain, verify_play_integrity,
78 AppAttestVerificationInput, AttestationError, PlayIntegrityVerificationInput,
79};
80use chio_kernel_core::passport_verify::{verify_passport as core_verify_passport, VerifyError};
81use chio_kernel_core::{
82 evaluate_with_full_floor_and_root, sign_receipt as core_sign_receipt,
83 sign_receipt_relaying_trusted_body as core_relay_trusted_body,
84 verify_capability_full_with_root, BudgetRegistry, BudgetSplitError, CapabilityError,
85 CapabilityFeatureContext, Clock, EvaluateInput, FixedClock, Guard, InMemoryBudgetRegistry,
86 PortableToolCallRequest, ReceiptSigningError, Verdict,
87};
88
89#[derive(Debug, Clone)]
100pub struct VerifiedCapability {
101 pub id: String,
102 pub subject_hex: String,
103 pub issuer_hex: String,
104 pub scope_json: String,
105 pub issued_at: u64,
106 pub expires_at: u64,
107 pub evaluated_at: u64,
108}
109
110#[derive(Debug, Clone)]
116pub struct PortablePassportMetadata {
117 pub subject: String,
118 pub issuer_hex: String,
119 pub issued_at: u64,
120 pub expires_at: u64,
121 pub evaluated_at: u64,
122 pub payload_canonical_hex: String,
123}
124
125#[derive(Debug, Deserialize)]
134struct EvaluateRequest {
135 capability: serde_json::Value,
137 trusted_issuers: Vec<String>,
139 request: EvaluateRequestBody,
141 #[serde(default)]
144 now_secs: Option<i64>,
145 #[serde(default)]
149 peer_capabilities: Option<CapabilityNegotiation>,
150 #[serde(default)]
152 direct_root_capability: Option<serde_json::Value>,
153 #[serde(default)]
156 capability_trust_roots: std::collections::BTreeMap<String, ScopeHash>,
157 #[serde(default)]
160 parent_budget_snapshots: Vec<ParentBudgetSnapshot>,
161}
162
163#[derive(Debug, Deserialize)]
165struct VerifyCapabilityRequest {
166 token: serde_json::Value,
168 trusted_issuers: Vec<String>,
170 #[serde(default)]
173 now_secs: Option<i64>,
174 #[serde(default)]
176 peer_capabilities: Option<CapabilityNegotiation>,
177 #[serde(default)]
179 direct_root_capability: Option<serde_json::Value>,
180 #[serde(default)]
182 capability_trust_roots: std::collections::BTreeMap<String, ScopeHash>,
183 #[serde(default)]
186 parent_budget_snapshots: Vec<ParentBudgetSnapshot>,
187}
188
189#[derive(Debug, Clone, Deserialize)]
190struct ParentBudgetSnapshot {
191 parent_token_id: String,
192 parent_share_bps: u16,
193 #[serde(default)]
194 admitted_children: Vec<AdmittedChildBudget>,
195}
196
197#[derive(Debug, Clone, Deserialize)]
198struct AdmittedChildBudget {
199 child_token_id: String,
200 share_bps: u16,
201}
202
203#[derive(Debug, Deserialize)]
205struct EvaluateRequestBody {
206 request_id: String,
207 tool_name: String,
208 server_id: String,
209 agent_id: String,
210 #[serde(default)]
211 arguments: serde_json::Value,
212 #[serde(default)]
213 governed_intent: Option<serde_json::Value>,
214 #[serde(default)]
215 approval_token: Option<serde_json::Value>,
216 #[serde(default)]
217 approval_tokens: Vec<serde_json::Value>,
218 #[serde(default)]
219 threshold_approval_proposal: Option<serde_json::Value>,
220 #[serde(default)]
221 supplemental_authorization: Option<serde_json::Value>,
222}
223
224impl EvaluateRequestBody {
225 fn has_unsupported_authorization_extensions(&self) -> bool {
226 self.governed_intent.is_some()
227 || self.approval_token.is_some()
228 || !self.approval_tokens.is_empty()
229 || self.threshold_approval_proposal.is_some()
230 || self.supplemental_authorization.is_some()
231 }
232}
233
234#[derive(Debug, Serialize)]
236struct EvaluateResponse {
237 verdict: &'static str,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 reason: Option<String>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 matched_grant_index: Option<usize>,
242}
243
244fn decode_hex_argument(label: &str, value: &str) -> Result<Vec<u8>, ChioMobileError> {
249 let trimmed = value.strip_prefix("0x").unwrap_or(value);
250 if trimmed.is_empty() {
251 return Err(ChioMobileError::InvalidHex {
252 message: format!("{label}: value must not be empty"),
253 });
254 }
255 hex::decode(trimmed).map_err(|error| ChioMobileError::InvalidHex {
256 message: format!("{label}: {error}"),
257 })
258}
259
260fn decode_canonical_content_hex(value: &str) -> Result<Vec<u8>, ChioMobileError> {
269 let trimmed = value.strip_prefix("0x").unwrap_or(value);
270 if trimmed.is_empty() {
271 return Ok(Vec::new());
272 }
273 decode_hex_argument("canonical content", value)
274}
275
276fn map_attestation_error(error: AttestationError) -> ChioMobileError {
277 ChioMobileError::AttestationRejected {
278 message: format!("{}: {error}", error.urn()),
279 }
280}
281
282fn chio_hash(bytes: &[u8]) -> [u8; 32] {
283 use sha2::{Digest, Sha256};
284 Sha256::digest(bytes).into()
285}
286
287fn fixed_clock_from_secs(now_secs: i64) -> Option<FixedClock> {
288 if now_secs < 0 {
289 None
290 } else {
291 Some(FixedClock::new(now_secs as u64))
292 }
293}
294
295fn seed_budget_registry(
296 budgets: &mut InMemoryBudgetRegistry,
297 snapshots: &[ParentBudgetSnapshot],
298) -> Result<(), ChioMobileError> {
299 for snapshot in snapshots {
300 budgets
301 .register_parent(snapshot.parent_token_id.clone(), snapshot.parent_share_bps)
302 .map_err(|error| budget_seed_error("parent budget snapshot", &error))?;
303 for child in &snapshot.admitted_children {
304 budgets
305 .try_admit_child(
306 snapshot.parent_token_id.as_str(),
307 child.child_token_id.clone(),
308 child.share_bps,
309 )
310 .map_err(|error| budget_seed_error("admitted child budget snapshot", &error))?;
311 }
312 }
313 Ok(())
314}
315
316fn budget_seed_error(context: &str, error: &BudgetSplitError) -> ChioMobileError {
317 ChioMobileError::InvalidCapability {
318 message: format!("{context}: {error}"),
319 }
320}
321
322fn decode_trusted_issuers(values: &[String]) -> Result<Vec<PublicKey>, ChioMobileError> {
323 values
324 .iter()
325 .map(|hex_str| {
326 PublicKey::from_hex(hex_str).map_err(|error| ChioMobileError::InvalidHex {
327 message: format!("trusted issuer: {error}"),
328 })
329 })
330 .collect()
331}
332
333pub fn evaluate(request_json: String) -> Result<String, ChioMobileError> {
341 let parsed: EvaluateRequest =
342 serde_json::from_str(&request_json).map_err(|error| ChioMobileError::InvalidJson {
343 message: format!("evaluate request: {error}"),
344 })?;
345 if parsed.request.has_unsupported_authorization_extensions() {
346 return Err(ChioMobileError::InvalidCapability {
347 message: "mobile portable evaluation cannot authenticate governed approvals or supplemental authorization".to_string(),
348 });
349 }
350
351 let capability: CapabilityToken =
352 serde_json::from_value(parsed.capability).map_err(|error| {
353 ChioMobileError::InvalidJson {
354 message: format!("capability token: {error}"),
355 }
356 })?;
357 let direct_root_capability = parsed
358 .direct_root_capability
359 .map(serde_json::from_value)
360 .transpose()
361 .map_err(|error| ChioMobileError::InvalidJson {
362 message: format!("direct root capability: {error}"),
363 })?;
364
365 let trusted = decode_trusted_issuers(&parsed.trusted_issuers)?;
366
367 let portable_request = PortableToolCallRequest {
368 request_id: parsed.request.request_id,
369 tool_name: parsed.request.tool_name,
370 server_id: parsed.request.server_id,
371 agent_id: parsed.request.agent_id,
372 arguments: parsed.request.arguments,
373 };
374
375 let fixed_clock: Option<FixedClock> = match parsed.now_secs {
379 Some(secs) if secs > 0 => Some(FixedClock::new(secs as u64)),
380 _ => None,
381 };
382 let mobile_clock = MobileClock::new();
383 let clock: &dyn Clock = match &fixed_clock {
384 Some(c) => c,
385 None => &mobile_clock,
386 };
387
388 let guards: &[&dyn Guard] = &[];
392
393 let peer_profile = parsed
396 .peer_capabilities
397 .clone()
398 .unwrap_or_else(CapabilityNegotiation::t1_default);
399 let trust_root_map = parsed.capability_trust_roots.clone();
400 let trust_resolver = move |issuer: &PublicKey| -> Option<ScopeHash> {
401 trust_root_map.get(&issuer.to_hex()).cloned()
402 };
403
404 let mut budgets = InMemoryBudgetRegistry::new();
408 seed_budget_registry(&mut budgets, &parsed.parent_budget_snapshots)?;
409 let verdict = evaluate_with_full_floor_and_root(
410 EvaluateInput {
411 request: &portable_request,
412 capability: &capability,
413 trusted_issuers: &trusted,
414 clock,
415 guards,
416 session_filesystem_roots: None,
417 },
418 CapabilityCryptoFloor::AllowClassical,
419 &peer_profile,
420 direct_root_capability.as_ref(),
421 &trust_resolver,
422 &mut budgets,
423 );
424
425 let response = match verdict.verdict {
426 Verdict::Allow => EvaluateResponse {
427 verdict: "allow",
428 reason: None,
429 matched_grant_index: verdict.matched_grant_index,
430 },
431 Verdict::Deny => EvaluateResponse {
432 verdict: "deny",
433 reason: verdict.reason,
434 matched_grant_index: verdict.matched_grant_index,
435 },
436 Verdict::PendingApproval => EvaluateResponse {
437 verdict: "deny",
438 reason: Some(
439 "kernel-core returned PendingApproval; mobile FFI treats as fail-closed deny"
440 .to_string(),
441 ),
442 matched_grant_index: verdict.matched_grant_index,
443 },
444 };
445
446 serde_json::to_string(&response).map_err(|error| ChioMobileError::Internal {
447 message: format!("serialize evaluate response: {error}"),
448 })
449}
450
451fn backend_from_seed_hex(signing_seed_hex: &str) -> Result<Ed25519Backend, ChioMobileError> {
454 let seed_bytes = decode_hex_argument("signing seed", signing_seed_hex)?;
455 if seed_bytes.len() != 32 {
456 return Err(ChioMobileError::InvalidHex {
457 message: format!(
458 "signing seed: expected 32-byte Ed25519 seed, got {} bytes",
459 seed_bytes.len()
460 ),
461 });
462 }
463 if seed_bytes.iter().all(|byte| *byte == 0) {
464 return Err(ChioMobileError::WeakEntropy {
465 message: "refusing to sign with an all-zero Ed25519 seed".to_string(),
466 });
467 }
468 let mut seed = [0u8; 32];
469 seed.copy_from_slice(&seed_bytes);
470 let keypair = Keypair::from_seed(&seed);
471 Ok(Ed25519Backend::new(keypair))
472}
473
474fn map_signing_error(error: ReceiptSigningError) -> ChioMobileError {
476 match error {
477 ReceiptSigningError::KernelKeyMismatch => ChioMobileError::KernelKeyMismatch {
478 message: "receipt body kernel_key does not match the public key derived from the signing seed".to_string(),
479 },
480 ReceiptSigningError::ContentHashMismatch { recomputed, claimed } => {
485 ChioMobileError::SigningFailed {
486 message: format!(
487 "receipt content_hash mismatch: body claimed {claimed} but signer recomputed {recomputed} over the canonical content (WYSIWYS refused)"
488 ),
489 }
490 }
491 ReceiptSigningError::SigningFailed(msg) => ChioMobileError::SigningFailed { message: msg },
492 }
493}
494
495pub fn sign_receipt(
515 body_json: String,
516 canonical_content_hex: String,
517 signing_seed_hex: String,
518) -> Result<String, ChioMobileError> {
519 let body: ChioReceiptBody =
520 serde_json::from_str(&body_json).map_err(|error| ChioMobileError::InvalidJson {
521 message: format!("receipt body: {error}"),
522 })?;
523
524 let canonical_content = decode_canonical_content_hex(&canonical_content_hex)?;
525 let backend = backend_from_seed_hex(&signing_seed_hex)?;
526
527 let receipt =
528 core_sign_receipt(body, &backend, &canonical_content).map_err(map_signing_error)?;
529
530 serde_json::to_string(&receipt).map_err(|error| ChioMobileError::Internal {
531 message: format!("serialize signed receipt: {error}"),
532 })
533}
534
535pub fn sign_receipt_relaying_trusted_body(
549 body_json: String,
550 signing_seed_hex: String,
551) -> Result<String, ChioMobileError> {
552 let body: ChioReceiptBody =
553 serde_json::from_str(&body_json).map_err(|error| ChioMobileError::InvalidJson {
554 message: format!("receipt body: {error}"),
555 })?;
556
557 let backend = backend_from_seed_hex(&signing_seed_hex)?;
558
559 let receipt = core_relay_trusted_body(body, &backend).map_err(map_signing_error)?;
560
561 serde_json::to_string(&receipt).map_err(|error| ChioMobileError::Internal {
562 message: format!("serialize signed receipt: {error}"),
563 })
564}
565
566pub fn verify_capability(
572 token_json: String,
573 authority_pub_hex: String,
574) -> Result<VerifiedCapability, ChioMobileError> {
575 let token: CapabilityToken =
576 serde_json::from_str(&token_json).map_err(|error| ChioMobileError::InvalidJson {
577 message: format!("capability token: {error}"),
578 })?;
579
580 let authority =
581 PublicKey::from_hex(&authority_pub_hex).map_err(|error| ChioMobileError::InvalidHex {
582 message: format!("authority public key: {error}"),
583 })?;
584
585 verify_capability_with_parts(
586 token,
587 vec![authority],
588 None,
589 CapabilityNegotiation::t1_default(),
590 None,
591 std::collections::BTreeMap::new(),
592 &[],
593 )
594}
595
596pub fn verify_capability_with_context(
602 request_json: String,
603) -> Result<VerifiedCapability, ChioMobileError> {
604 let parsed: VerifyCapabilityRequest =
605 serde_json::from_str(&request_json).map_err(|error| ChioMobileError::InvalidJson {
606 message: format!("verify capability request: {error}"),
607 })?;
608 let token: CapabilityToken =
609 serde_json::from_value(parsed.token).map_err(|error| ChioMobileError::InvalidJson {
610 message: format!("capability token: {error}"),
611 })?;
612 let direct_root_capability = parsed
613 .direct_root_capability
614 .map(serde_json::from_value)
615 .transpose()
616 .map_err(|error| ChioMobileError::InvalidJson {
617 message: format!("direct root capability: {error}"),
618 })?;
619 let trusted = decode_trusted_issuers(&parsed.trusted_issuers)?;
620 let peer_profile = parsed
621 .peer_capabilities
622 .clone()
623 .unwrap_or_else(CapabilityNegotiation::t1_default);
624
625 verify_capability_with_parts(
626 token,
627 trusted,
628 parsed.now_secs,
629 peer_profile,
630 direct_root_capability,
631 parsed.capability_trust_roots,
632 &parsed.parent_budget_snapshots,
633 )
634}
635
636fn verify_capability_with_parts(
637 token: CapabilityToken,
638 trusted: Vec<PublicKey>,
639 now_secs: Option<i64>,
640 peer_profile: CapabilityNegotiation,
641 direct_root_capability: Option<CapabilityToken>,
642 capability_trust_roots: std::collections::BTreeMap<String, ScopeHash>,
643 parent_budget_snapshots: &[ParentBudgetSnapshot],
644) -> Result<VerifiedCapability, ChioMobileError> {
645 let fixed_clock = now_secs.and_then(fixed_clock_from_secs);
646 let mobile_clock = MobileClock::new();
647 let clock: &dyn Clock = match &fixed_clock {
648 Some(clock) => clock,
649 None => &mobile_clock,
650 };
651 let trust_resolver = |issuer: &PublicKey| -> Option<ScopeHash> {
652 capability_trust_roots.get(&issuer.to_hex()).cloned()
653 };
654 let mut budgets = InMemoryBudgetRegistry::new();
655 seed_budget_registry(&mut budgets, parent_budget_snapshots)?;
656 let verified = verify_capability_full_with_root(
657 &token,
658 &trusted,
659 clock,
660 CapabilityCryptoFloor::AllowClassical,
661 CapabilityFeatureContext {
662 peer: &peer_profile,
663 direct_root: direct_root_capability.as_ref(),
664 },
665 &trust_resolver,
666 &mut budgets,
667 )
668 .map_err(|error| match error {
669 CapabilityError::UntrustedIssuer => ChioMobileError::InvalidCapability {
670 message: "capability issuer is not in the trusted authority set".to_string(),
671 },
672 CapabilityError::InvalidSignature => ChioMobileError::InvalidCapability {
673 message: "capability signature failed to verify".to_string(),
674 },
675 CapabilityError::NotYetValid => ChioMobileError::InvalidCapability {
676 message: "capability is not yet valid".to_string(),
677 },
678 CapabilityError::Expired => ChioMobileError::InvalidCapability {
679 message: "capability has expired".to_string(),
680 },
681 CapabilityError::CryptoFloorRejected(message) => ChioMobileError::InvalidCapability {
682 message: format!("capability crypto floor rejected: {message}"),
683 },
684 CapabilityError::AttenuationViolation(message) => ChioMobileError::InvalidCapability {
685 message: format!("capability rejected by chain binding: {message}"),
686 },
687 CapabilityError::BudgetSplitRejected(err) => ChioMobileError::InvalidCapability {
688 message: format!("capability rejected by sibling-sum budget split: {err}"),
689 },
690 CapabilityError::Internal(msg) => ChioMobileError::Internal {
691 message: format!("capability verification failed: {msg}"),
692 },
693 })?;
694
695 let scope_json =
696 serde_json::to_string(&verified.scope).map_err(|error| ChioMobileError::Internal {
697 message: format!("serialize capability scope: {error}"),
698 })?;
699
700 Ok(VerifiedCapability {
701 id: verified.id,
702 subject_hex: verified.subject_hex,
703 issuer_hex: verified.issuer_hex,
704 scope_json,
705 issued_at: verified.issued_at,
706 expires_at: verified.expires_at,
707 evaluated_at: verified.evaluated_at,
708 })
709}
710
711pub fn verify_passport(
717 envelope_json: String,
718 issuer_pub_hex: String,
719 now_secs: i64,
720) -> Result<PortablePassportMetadata, ChioMobileError> {
721 let issuer =
722 PublicKey::from_hex(&issuer_pub_hex).map_err(|error| ChioMobileError::InvalidHex {
723 message: format!("authority public key: {error}"),
724 })?;
725
726 let fixed_clock: Option<FixedClock> = if now_secs > 0 {
727 Some(FixedClock::new(now_secs as u64))
728 } else {
729 None
730 };
731 let mobile_clock = MobileClock::new();
732 let clock: &dyn Clock = match &fixed_clock {
733 Some(c) => c,
734 None => &mobile_clock,
735 };
736
737 let verified =
738 core_verify_passport(envelope_json.as_bytes(), &[issuer], clock).map_err(|error| {
739 match error {
740 VerifyError::InvalidEnvelope(msg) => ChioMobileError::InvalidPassport {
741 message: format!("invalid envelope: {msg}"),
742 },
743 VerifyError::InvalidSchema => ChioMobileError::InvalidPassport {
744 message: "envelope schema tag does not match portable passport v1".to_string(),
745 },
746 VerifyError::MissingSubject => ChioMobileError::InvalidPassport {
747 message: "envelope subject is empty".to_string(),
748 },
749 VerifyError::InvalidValidityWindow => ChioMobileError::InvalidPassport {
750 message: "envelope validity window is inverted".to_string(),
751 },
752 VerifyError::UntrustedIssuer => ChioMobileError::InvalidPassport {
753 message: "envelope issuer is not in the trusted authority set".to_string(),
754 },
755 VerifyError::InvalidSignature => ChioMobileError::InvalidPassport {
756 message: "envelope signature failed to verify".to_string(),
757 },
758 VerifyError::NotYetValid => ChioMobileError::InvalidPassport {
759 message: "envelope is not yet valid".to_string(),
760 },
761 VerifyError::Expired => ChioMobileError::InvalidPassport {
762 message: "envelope has expired".to_string(),
763 },
764 VerifyError::Internal(msg) => ChioMobileError::Internal {
765 message: format!("passport verification failed: {msg}"),
766 },
767 }
768 })?;
769
770 Ok(PortablePassportMetadata {
771 subject: verified.subject,
772 issuer_hex: verified.issuer.to_hex(),
773 issued_at: verified.issued_at,
774 expires_at: verified.expires_at,
775 evaluated_at: verified.evaluated_at,
776 payload_canonical_hex: hex::encode(&verified.payload_canonical_bytes),
777 })
778}
779
780pub fn attest_app_attest(key_id: String, challenge_hex: String) -> Result<String, ChioMobileError> {
787 let challenge = decode_hex_argument("App Attest challenge", &challenge_hex)?;
788 if key_id.trim().is_empty() {
789 return Err(ChioMobileError::AttestationRejected {
790 message: "App Attest key_id is empty".to_string(),
791 });
792 }
793
794 serde_json::to_string(&serde_json::json!({
795 "schema": "chio.mobile.app-attest.challenge.v1",
796 "platform": "app_attest",
797 "key_id": key_id,
798 "challenge_hex": hex::encode(&challenge),
799 "challenge_sha256": hex::encode(chio_hash(&challenge)),
800 "verifier": "chio-custody-hw::attestation::verify_app_attest",
801 "status": "requires_platform_evidence"
802 }))
803 .map_err(|error| ChioMobileError::Internal {
804 message: format!("serialize App Attest challenge envelope: {error}"),
805 })
806}
807
808pub fn verify_app_attest_evidence(
810 key_id: String,
811 challenge_hex: String,
812 app_id: String,
813 attestation_cbor_hex: String,
814 previous_counter: i64,
815) -> Result<String, ChioMobileError> {
816 let challenge = decode_hex_argument("App Attest challenge", &challenge_hex)?;
817 let attestation_cbor =
818 decode_hex_argument("App Attest attestation object", &attestation_cbor_hex)?;
819 let previous_counter = if previous_counter < 0 {
820 None
821 } else {
822 let counter = u32::try_from(previous_counter).map_err(|error| {
823 ChioMobileError::AttestationRejected {
824 message: format!("App Attest previous_counter: {error}"),
825 }
826 })?;
827 Some(counter)
828 };
829 let verified = verify_app_attest(AppAttestVerificationInput {
830 attestation_cbor: &attestation_cbor,
831 key_id: &key_id,
832 challenge: &challenge,
833 app_id: &app_id,
834 previous_counter,
835 production: true,
836 allow_development_fixture: false,
837 })
838 .map_err(map_attestation_error)?;
839
840 serde_json::to_string(&serde_json::json!({
841 "schema": "chio.mobile.attestation-evidence.v1",
842 "platform": "app_attest",
843 "key_id": verified.key_id,
844 "app_id": verified.app_id,
845 "counter": verified.counter,
846 "challenge_hash": verified.challenge_hash_hex,
847 "app_id_hash": verified.app_id_hash_hex,
848 "credential_public_key_sha256": verified.credential_public_key_sha256_hex,
849 "apple_root_sha256": verified.root_fingerprint_sha256_hex
850 }))
851 .map_err(|error| ChioMobileError::Internal {
852 message: format!("serialize App Attest evidence envelope: {error}"),
853 })
854}
855
856pub fn attest_play_integrity(nonce_hex: String) -> Result<String, ChioMobileError> {
862 let nonce = decode_hex_argument("Play Integrity nonce", &nonce_hex)?;
863 serde_json::to_string(&serde_json::json!({
864 "schema": "chio.mobile.play-integrity.challenge.v1",
865 "platform": "play_integrity",
866 "nonce_hex": hex::encode(&nonce),
867 "nonce_sha256": hex::encode(chio_hash(&nonce)),
868 "verifier": "chio-custody-hw::attestation::verify_play_integrity",
869 "status": "requires_platform_evidence"
870 }))
871 .map_err(|error| ChioMobileError::Internal {
872 message: format!("serialize Play Integrity challenge envelope: {error}"),
873 })
874}
875
876pub fn verify_play_integrity_evidence(
878 token: String,
879 expected_nonce: String,
880 expected_package_name: String,
881 expected_audience: String,
882 jwks_json: String,
883) -> Result<String, ChioMobileError> {
884 let verified = verify_play_integrity(PlayIntegrityVerificationInput {
885 token: &token,
886 expected_nonce: &expected_nonce,
887 expected_package_name: &expected_package_name,
888 expected_audience: &expected_audience,
889 jwks_json: &jwks_json,
890 allow_caller_supplied_jwks: false,
891 })
892 .map_err(map_attestation_error)?;
893
894 serde_json::to_string(&serde_json::json!({
895 "schema": "chio.mobile.attestation-evidence.v1",
896 "platform": "play_integrity",
897 "package_name": verified.package_name,
898 "nonce": verified.nonce,
899 "app_recognition_verdict": verified.app_recognition_verdict,
900 "device_recognition_verdict": verified.device_recognition_verdict
901 }))
902 .map_err(|error| ChioMobileError::Internal {
903 message: format!("serialize Play Integrity evidence envelope: {error}"),
904 })
905}
906
907pub fn verify_mobile_receipt(
913 receipt_json: String,
914 evidence_json: String,
915) -> Result<String, ChioMobileError> {
916 let _: serde_json::Value =
917 serde_json::from_str(&receipt_json).map_err(|error| ChioMobileError::InvalidJson {
918 message: format!("mobile receipt: {error}"),
919 })?;
920 let _: serde_json::Value =
921 serde_json::from_str(&evidence_json).map_err(|error| ChioMobileError::InvalidJson {
922 message: format!("mobile attestation evidence: {error}"),
923 })?;
924
925 let verified = verify_mobile_receipt_chain(&receipt_json, &evidence_json)
926 .map_err(map_attestation_error)?;
927 serde_json::to_string(&serde_json::json!({
928 "schema": "chio.mobile.receipt-verification.v1",
929 "status": "shape_only",
930 "receipt_kind": "trace_observation",
931 "boundary_class": "detect_only",
932 "result": "observed",
933 "authoritative": false,
934 "authorized": false,
935 "receipt_schema": verified.receipt_schema,
936 "evidence_schema": verified.evidence_schema,
937 "platform": verified.platform
938 }))
939 .map_err(|error| ChioMobileError::Internal {
940 message: format!("serialize mobile receipt verification: {error}"),
941 })
942}
943
944uniffi::include_scaffolding!("chio_kernel_mobile");