@@ -612,7 +612,7 @@ mod test {
612612 UvUpdate ,
613613 } ;
614614
615- use super :: { pin_uv_auth_token_len_valid, user_verification, Error } ;
615+ use super :: { pin_uv_auth_token_len_valid, user_verification, CtapError , Error } ;
616616 const TIMEOUT : Duration = Duration :: from_secs ( 1 ) ;
617617
618618 #[ test]
@@ -1680,4 +1680,216 @@ mod test {
16801680 let recv = recv_handle. await . expect ( "Failed to join update thread" ) ;
16811681 assert ! ( recv. is_empty( ) ) ;
16821682 }
1683+
1684+ #[ tokio:: test]
1685+ async fn persistent_token_self_heals_on_rejection ( ) {
1686+ use crate :: management:: CredentialManagement ;
1687+
1688+ let mut channel = MockChannel :: new ( ) ;
1689+ let token = vec ! [ 0x5A ; 32 ] ;
1690+ let device_identifier = [ 0x42 ; 16 ] ;
1691+ let aaguid = [ 0x07 ; 16 ] ;
1692+
1693+ let store = Arc :: new ( MemoryPersistentTokenStore :: new ( ) ) ;
1694+ store
1695+ . put (
1696+ & "stale" . to_string ( ) ,
1697+ & PersistentTokenRecord {
1698+ persistent_token : token. clone ( ) ,
1699+ pin_uv_auth_protocol : Ctap2PinUvAuthProtocol :: One ,
1700+ device_identifier,
1701+ aaguid,
1702+ } ,
1703+ )
1704+ . await ;
1705+ channel. set_persistent_token_store ( store. clone ( ) ) ;
1706+
1707+ // The device still computes encIdentifier under our token (a PIN change cleared the
1708+ // token's permissions, not its bytes), so recognition matches but the op is rejected.
1709+ let info = pcmr_get_info (
1710+ & [
1711+ ( "uv" , true ) ,
1712+ ( "pinUvAuthToken" , true ) ,
1713+ ( "perCredMgmtRO" , true ) ,
1714+ ] ,
1715+ & token,
1716+ device_identifier,
1717+ aaguid,
1718+ ) ;
1719+ let info_resp = || CborResponse :: new_success_from_slice ( to_vec ( & info) . unwrap ( ) . as_slice ( ) ) ;
1720+
1721+ let pin_protocol = PinUvAuthProtocolOne :: new ( ) ;
1722+ // The credMgmt request is identical on reuse and re-mint (same token, same data).
1723+ let mut expected_credmgmt = Ctap2CredentialManagementRequest :: new_get_credential_metadata ( ) ;
1724+ expected_credmgmt
1725+ . calculate_and_set_uv_auth ( & pin_protocol, & token)
1726+ . unwrap ( ) ;
1727+ let expected_credmgmt_cbor = CborRequest :: try_from ( & expected_credmgmt) . unwrap ( ) ;
1728+
1729+ // Iteration 1: getInfo, recognize + reuse, device rejects with PINAuthInvalid.
1730+ channel. push_command_pair (
1731+ CborRequest :: new ( Ctap2CommandCode :: AuthenticatorGetInfo ) ,
1732+ info_resp ( ) ,
1733+ ) ;
1734+ channel. push_command_pair (
1735+ expected_credmgmt_cbor. clone ( ) ,
1736+ CborResponse {
1737+ status_code : CtapError :: PINAuthInvalid ,
1738+ data : None ,
1739+ } ,
1740+ ) ;
1741+
1742+ // Iteration 2: getInfo, mint via UV (keyAgreement, getUvToken pcmr), then success.
1743+ channel. push_command_pair (
1744+ CborRequest :: new ( Ctap2CommandCode :: AuthenticatorGetInfo ) ,
1745+ info_resp ( ) ,
1746+ ) ;
1747+ let key_agreement_req = CborRequest :: try_from (
1748+ & Ctap2ClientPinRequest :: new_get_key_agreement ( Ctap2PinUvAuthProtocol :: One ) ,
1749+ )
1750+ . unwrap ( ) ;
1751+ let key_agreement_resp = CborResponse :: new_success_from_slice (
1752+ to_vec ( & Ctap2ClientPinResponse {
1753+ key_agreement : Some ( get_key_agreement ( ) ) ,
1754+ pin_uv_auth_token : None ,
1755+ pin_retries : None ,
1756+ power_cycle_state : None ,
1757+ uv_retries : None ,
1758+ } )
1759+ . unwrap ( )
1760+ . as_slice ( ) ,
1761+ ) ;
1762+ channel. push_command_pair ( key_agreement_req, key_agreement_resp) ;
1763+
1764+ let ( public_key, shared_secret) = pin_protocol. encapsulate ( & get_key_agreement ( ) ) . unwrap ( ) ;
1765+ let uv_token_req =
1766+ CborRequest :: try_from ( & Ctap2ClientPinRequest :: new_get_uv_token_with_perm (
1767+ Ctap2PinUvAuthProtocol :: One ,
1768+ public_key,
1769+ Ctap2AuthTokenPermissionRole :: PERSISTENT_CREDENTIAL_MANAGEMENT_READ_ONLY ,
1770+ None ,
1771+ ) )
1772+ . unwrap ( ) ;
1773+ // The device re-grants the same persistent token (bytes unchanged).
1774+ let encrypted_token = pin_protocol. encrypt ( & shared_secret, & token) . unwrap ( ) ;
1775+ let uv_token_resp = CborResponse :: new_success_from_slice (
1776+ to_vec ( & Ctap2ClientPinResponse {
1777+ key_agreement : None ,
1778+ pin_uv_auth_token : Some ( ByteBuf :: from ( encrypted_token) ) ,
1779+ pin_retries : None ,
1780+ power_cycle_state : None ,
1781+ uv_retries : None ,
1782+ } )
1783+ . unwrap ( )
1784+ . as_slice ( ) ,
1785+ ) ;
1786+ channel. push_command_pair ( uv_token_req, uv_token_resp) ;
1787+
1788+ // CBOR map {0x01: 3, 0x02: 20}: existingResidentCredentialsCount and max remaining.
1789+ let metadata_resp = CborResponse :: new_success_from_slice ( & [ 0xA2 , 0x01 , 0x03 , 0x02 , 0x14 ] ) ;
1790+ channel. push_command_pair ( expected_credmgmt_cbor, metadata_resp) ;
1791+
1792+ let mut recv = channel. get_ux_update_receiver ( ) ;
1793+ let recv_handle = tokio:: task:: spawn ( async move {
1794+ assert_eq ! ( recv. recv( ) . await , Ok ( UvUpdate :: PresenceRequired ) ) ;
1795+ recv
1796+ } ) ;
1797+
1798+ let metadata = channel. get_credential_metadata ( TIMEOUT ) . await . unwrap ( ) ;
1799+ assert_eq ! ( metadata. existing_resident_credentials_count, 3 ) ;
1800+
1801+ // The stale record was evicted and replaced by a freshly minted one.
1802+ let listed = store. list ( ) . await ;
1803+ assert_eq ! ( listed. len( ) , 1 ) ;
1804+ assert ! ( listed. iter( ) . all( |( id, _) | id != "stale" ) ) ;
1805+ assert_eq ! ( listed[ 0 ] . 1 . device_identifier, device_identifier) ;
1806+
1807+ let recv = recv_handle. await . expect ( "Failed to join update thread" ) ;
1808+ assert ! ( recv. is_empty( ) ) ;
1809+ }
1810+
1811+ #[ tokio:: test]
1812+ async fn pin_change_evicts_persistent_record ( ) {
1813+ use crate :: pin:: PinManagement ;
1814+
1815+ let mut channel = MockChannel :: new ( ) ;
1816+ let token = vec ! [ 0x5A ; 32 ] ;
1817+ let device_identifier = [ 0x42 ; 16 ] ;
1818+ let aaguid = [ 0x07 ; 16 ] ;
1819+
1820+ let store = Arc :: new ( MemoryPersistentTokenStore :: new ( ) ) ;
1821+ store
1822+ . put (
1823+ & "to-evict" . to_string ( ) ,
1824+ & PersistentTokenRecord {
1825+ persistent_token : token. clone ( ) ,
1826+ pin_uv_auth_protocol : Ctap2PinUvAuthProtocol :: One ,
1827+ device_identifier,
1828+ aaguid,
1829+ } ,
1830+ )
1831+ . await ;
1832+ channel. set_persistent_token_store ( store. clone ( ) ) ;
1833+
1834+ // Set-PIN path (clientPin=false): no current-PIN prompt.
1835+ let info = pcmr_get_info (
1836+ & [ ( "clientPin" , false ) , ( "perCredMgmtRO" , true ) ] ,
1837+ & token,
1838+ device_identifier,
1839+ aaguid,
1840+ ) ;
1841+ channel. push_command_pair (
1842+ CborRequest :: new ( Ctap2CommandCode :: AuthenticatorGetInfo ) ,
1843+ CborResponse :: new_success_from_slice ( to_vec ( & info) . unwrap ( ) . as_slice ( ) ) ,
1844+ ) ;
1845+
1846+ let key_agreement_req = CborRequest :: try_from (
1847+ & Ctap2ClientPinRequest :: new_get_key_agreement ( Ctap2PinUvAuthProtocol :: One ) ,
1848+ )
1849+ . unwrap ( ) ;
1850+ let key_agreement_resp = CborResponse :: new_success_from_slice (
1851+ to_vec ( & Ctap2ClientPinResponse {
1852+ key_agreement : Some ( get_key_agreement ( ) ) ,
1853+ pin_uv_auth_token : None ,
1854+ pin_retries : None ,
1855+ power_cycle_state : None ,
1856+ uv_retries : None ,
1857+ } )
1858+ . unwrap ( )
1859+ . as_slice ( ) ,
1860+ ) ;
1861+ channel. push_command_pair ( key_agreement_req, key_agreement_resp) ;
1862+
1863+ let pin_protocol = PinUvAuthProtocolOne :: new ( ) ;
1864+ let ( public_key, shared_secret) = pin_protocol. encapsulate ( & get_key_agreement ( ) ) . unwrap ( ) ;
1865+ let mut padded_new_pin = "1234" . as_bytes ( ) . to_vec ( ) ;
1866+ padded_new_pin. resize ( 64 , 0x00 ) ;
1867+ let new_pin_enc = pin_protocol
1868+ . encrypt ( & shared_secret, & padded_new_pin)
1869+ . unwrap ( ) ;
1870+ let uv_auth_param = pin_protocol
1871+ . authenticate ( & shared_secret, & new_pin_enc)
1872+ . unwrap ( ) ;
1873+ let set_pin_req = CborRequest :: try_from ( & Ctap2ClientPinRequest :: new_set_pin (
1874+ pin_protocol. version ( ) ,
1875+ & new_pin_enc,
1876+ public_key,
1877+ & uv_auth_param,
1878+ ) )
1879+ . unwrap ( ) ;
1880+ let set_pin_resp = CborResponse :: new_success_from_slice (
1881+ to_vec ( & Ctap2ClientPinResponse :: default ( ) )
1882+ . unwrap ( )
1883+ . as_slice ( ) ,
1884+ ) ;
1885+ channel. push_command_pair ( set_pin_req, set_pin_resp) ;
1886+
1887+ channel
1888+ . change_pin ( "1234" . to_string ( ) , TIMEOUT )
1889+ . await
1890+ . unwrap ( ) ;
1891+
1892+ // The connected device's record is evicted after a successful PIN change.
1893+ assert ! ( store. list( ) . await . is_empty( ) ) ;
1894+ }
16831895}
0 commit comments