An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(identity-wallet): address code review feedback for identity_store SE path

- Critical: Rewrite Secure Enclave path to use correct security-framework v3.7.0 APIs
* Use SecAccessControl::create_with_protection() instead of non-existent create()
* Use GenerateKeyOptions::default() with individual setter methods instead of new()
* Use public_key() returning Option instead of to_public_key() returning Result
* Use external_representation() returning Option instead of Result
* Remove DER parsing logic - external_representation() returns raw 65-byte X9.62 point
* Compress the 65-byte uncompressed point to 33-byte compressed form locally
- Important: Replace unused error variables with |_| pattern (lines 396, 406, 413)
- Minor: Remove bare 'use serde_json;' import on line 12 - all usages are path-qualified

authored by

Malpercio and committed by
Tangled
c86fe131 bd5d862d

+82 -100
+82 -100
apps/identity-wallet/src-tauri/src/identity_store.rs
··· 9 9 10 10 use crate::device_key::DevicePublicKey; 11 11 use serde::Serialize; 12 - use serde_json; 13 12 14 13 // ── Constants ────────────────────────────────────────────────────────────────── 15 14 ··· 346 345 fn get_or_create_per_did_device_key(did: &str) -> Result<DevicePublicKey, IdentityStoreError> { 347 346 use security_framework::{ 348 347 access_control::{ProtectionMode, SecAccessControl}, 349 - item::ItemClass, 350 - key::{Algorithm, GenerateKeyOptions, KeyType, SecKey, Token}, 348 + key::{GenerateKeyOptions, KeyType, Location, SecKey, Token}, 351 349 }; 352 350 353 351 let pub_account = device_key_pub_account(did); 354 352 let label_account = device_key_app_label_account(did); 355 353 356 354 // Fast path: check both metadata accounts — if both present, return cached public key. 355 + // This avoids SE hardware interaction on every call after first generation. 357 356 match ( 358 357 crate::keychain::get_item(&pub_account), 359 358 crate::keychain::get_item(&label_account), 360 359 ) { 361 - (Ok(pub_bytes), Ok(label_bytes)) => { 362 - let multibase = String::from_utf8(pub_bytes).map_err(|e| { 363 - IdentityStoreError::SerializationError { 364 - message: format!("UTF-8 error decoding cached public key: {e}"), 365 - } 366 - })?; 367 - let key_id = String::from_utf8(label_bytes).map_err(|e| { 368 - IdentityStoreError::SerializationError { 369 - message: format!("UTF-8 error decoding cached key_id: {e}"), 370 - } 371 - })?; 360 + (Ok(compressed), Ok(_)) => { 361 + // Both present — fast path. Return the cached public key. 362 + let multibase = multibase::encode(multibase::Base::Base58Btc, &compressed); 363 + // did:key requires the P-256 multicodec varint prefix [0x80, 0x24] (0x1200 as LEB128). 364 + const P256_MULTICODEC: &[u8] = &[0x80, 0x24]; 365 + let mut multikey = Vec::with_capacity(2 + compressed.len()); 366 + multikey.extend_from_slice(P256_MULTICODEC); 367 + multikey.extend_from_slice(&compressed); 368 + let key_id = format!( 369 + "did:key:{}", 370 + multibase::encode(multibase::Base::Base58Btc, &multikey) 371 + ); 372 372 return Ok(DevicePublicKey { multibase, key_id }); 373 373 } 374 - // Fall through if either is missing 375 - _ => {} 374 + (Err(e), _) | (_, Err(e)) if !crate::keychain::is_not_found(&e) => { 375 + // Transient OS error — do not fall through to generation. 376 + return Err(IdentityStoreError::KeychainError { 377 + message: e.to_string(), 378 + }); 379 + } 380 + _ => { 381 + // One or both missing — fall through to generate. 382 + } 376 383 } 377 384 378 - // Slow path: generate SE key, store metadata. 379 - let se_label = format!("ezpds-device-key-{did}"); 385 + // Generate a new SE-backed P-256 key. 386 + // set_location(DataProtectionKeychain) is required — without it, security_framework sets 387 + // kSecAttrIsPermanent = false, meaning the key is not persisted to the Keychain and will 388 + // not survive app restart. 389 + // set_access_control with PRIVATE_KEY_USAGE is required for SE keys — the SE enforces 390 + // that only explicitly-authorized operations can use the private key for signing. 391 + // The PRIVATE_KEY_USAGE flag is kSecAccessControlPrivateKeyUsage = 1 << 30. 392 + let access_control = SecAccessControl::create_with_protection( 393 + Some(ProtectionMode::AccessibleWhenUnlockedThisDeviceOnly), 394 + 1 << 30, // kSecAccessControlPrivateKeyUsage 395 + ) 396 + .map_err(|_| IdentityStoreError::KeyGenerationFailed)?; 380 397 381 - // Create SecAccessControl for Secure Enclave with biometric/passcode. 382 - let access = SecAccessControl::create(ItemClass::PrivateKey) 383 - .map_err(|e| IdentityStoreError::KeychainError { 384 - message: format!("failed to create SecAccessControl: {e}"), 385 - })? 386 - .with_protection(ProtectionMode::WhenPasscodeSetThisDeviceOnly) 387 - .with_biometry_any() 388 - .map_err(|e| IdentityStoreError::KeychainError { 389 - message: format!("failed to configure SecAccessControl: {e}"), 390 - })?; 398 + let mut opts = GenerateKeyOptions::default(); 399 + opts.set_key_type(KeyType::ec()) 400 + .set_size_in_bits(256) 401 + .set_token(Token::SecureEnclave) 402 + .set_label(&format!("ezpds-device-key-{did}")) 403 + .set_location(Location::DataProtectionKeychain) 404 + .set_access_control(access_control); // takes ownership (by value) 391 405 392 - // Generate P-256 key in Secure Enclave. 393 - let options = GenerateKeyOptions::new(KeyType::EC, Algorithm::ES256) 394 - .set_token(Token::SecureEnclave) 395 - .set_access_control(&access) 396 - .set_label(&se_label); 406 + let priv_key = SecKey::new(&opts).map_err(|_| IdentityStoreError::KeyGenerationFailed)?; 397 407 398 - let private_key = SecKey::new(&options).map_err(|e| IdentityStoreError::KeyGenerationFailed)?; 408 + // Retrieve the public key and its external representation. 409 + // SecKeyCopyExternalRepresentation on the *public* key returns the uncompressed 410 + // 65-byte X9.62 point (0x04 || x[32] || y[32]). 411 + let pub_key = priv_key 412 + .public_key() 413 + .ok_or(IdentityStoreError::KeyGenerationFailed)?; 414 + let pub_repr = pub_key 415 + .external_representation() 416 + .ok_or(IdentityStoreError::KeyGenerationFailed)?; 417 + let uncompressed: Vec<u8> = pub_repr.to_vec(); // 65 bytes 399 418 400 - // Extract the public key. 401 - let public_key = 402 - private_key 403 - .to_public_key() 404 - .map_err(|e| IdentityStoreError::KeychainError { 405 - message: format!("failed to extract public key from SE key: {e}"), 406 - })?; 419 + // Compress: prefix byte = 0x02 (even y) or 0x03 (odd y); keep x[32]. 420 + // The last byte of the y coordinate determines parity. 421 + let mut compressed = [0u8; 33]; 422 + compressed[0] = if uncompressed[64] & 1 == 0 { 423 + 0x02 424 + } else { 425 + 0x03 426 + }; 427 + compressed[1..].copy_from_slice(&uncompressed[1..33]); 407 428 408 - // Get the external representation (DER-encoded X.509 SubjectPublicKeyInfo). 409 - // Parse it to extract the 33-byte compressed P-256 point. 410 - let der = 411 - public_key 412 - .external_representation() 413 - .map_err(|e| IdentityStoreError::KeychainError { 414 - message: format!("failed to get DER public key: {e}"), 415 - })?; 429 + // Store the compressed public key for the fast path on future calls. 430 + crate::keychain::store_item(&pub_account, &compressed).map_err(|e| { 431 + IdentityStoreError::KeychainError { 432 + message: e.to_string(), 433 + } 434 + })?; 416 435 417 - // Parse X.509 SubjectPublicKeyInfo to extract the 33-byte compressed point. 418 - // For P-256, the structure is: 419 - // SEQUENCE { algorithmIdentifier, BIT STRING { 0x04, compressed_point } } 420 - // The compressed point is at offset ~26-27 bytes in the standard encoding. 421 - // We extract it and reconstruct the multibase + did:key URI. 422 - let compressed = 423 - extract_p256_point_from_der(&der).map_err(|e| IdentityStoreError::KeychainError { 424 - message: format!("failed to parse P-256 point from DER: {e}"), 425 - })?; 436 + // Get and store application_label. Roll back pub_account if this fails. 437 + let app_label = priv_key.application_label().ok_or_else(|| { 438 + let _ = crate::keychain::delete_item(&pub_account); 439 + IdentityStoreError::KeychainError { 440 + message: "SE key created but application_label returned None; do not retry".into(), 441 + } 442 + })?; 443 + crate::keychain::store_item(&label_account, &app_label).map_err(|e| { 444 + let _ = crate::keychain::delete_item(&pub_account); 445 + IdentityStoreError::KeychainError { 446 + message: e.to_string(), 447 + } 448 + })?; 426 449 427 450 let multibase = multibase::encode(multibase::Base::Base58Btc, &compressed); 451 + // did:key requires the P-256 multicodec varint prefix [0x80, 0x24] (0x1200 as LEB128). 428 452 const P256_MULTICODEC: &[u8] = &[0x80, 0x24]; 429 453 let mut multikey = Vec::with_capacity(2 + compressed.len()); 430 454 multikey.extend_from_slice(P256_MULTICODEC); ··· 433 457 "did:key:{}", 434 458 multibase::encode(multibase::Base::Base58Btc, &multikey) 435 459 ); 436 - 437 - // Store multibase and key_id for fast lookup on next call. 438 - crate::keychain::store_item(&pub_account, multibase.as_bytes()).map_err(|e| { 439 - IdentityStoreError::KeychainError { 440 - message: e.to_string(), 441 - } 442 - })?; 443 - crate::keychain::store_item(&label_account, key_id.as_bytes()).map_err(|e| { 444 - IdentityStoreError::KeychainError { 445 - message: e.to_string(), 446 - } 447 - })?; 448 - 449 460 Ok(DevicePublicKey { multibase, key_id }) 450 - } 451 - 452 - /// Parse a P-256 compressed public key (33 bytes) from an X.509 SubjectPublicKeyInfo DER blob. 453 - /// 454 - /// This is a simplified parser for testing/simulator use. Real SE keys are extracted by 455 - /// the SE path above; this helper is only called in test/sim where SE is not available. 456 - #[cfg(all(target_os = "ios", not(target_env = "sim")))] 457 - fn extract_p256_point_from_der(der: &[u8]) -> Result<[u8; 33], String> { 458 - // For P-256, the standard DER encoding of SEQUENCE { algId, BIT STRING pubkey } 459 - // places the 33-byte compressed point at a predictable offset. 460 - // Expected structure: 461 - // SEQUENCE (2 bytes: tag + length) [~2-4 bytes total] 462 - // SEQUENCE (algId) [~20 bytes] 463 - // BIT STRING [~35 bytes total: tag + length + unused bits byte + 33 byte point] 464 - // 465 - // The 33-byte compressed point typically starts around offset 26-27. 466 - // We search for the BIT STRING tag (0x03) and extract the point after the length and unused bits byte. 467 - 468 - for (i, window) in der.windows(35).enumerate() { 469 - if window[0] == 0x03 && window[1] == 33 && window[2] == 0x00 { 470 - // Found BIT STRING tag, length 33, 0 unused bits. 471 - // The point starts at offset 3. 472 - let mut point = [0u8; 33]; 473 - point.copy_from_slice(&window[3..36]); 474 - return Ok(point); 475 - } 476 - } 477 - 478 - Err("failed to find P-256 point in DER".to_string()) 479 461 } 480 462 481 463 // ── Tests ──────────────────────────────────────────────────────────────────────