rust/bt-csis: Resolve Resolvable Set Identifiers Resolve an RSI against a SIRK with the sih hash. Drop the key accessor and Encodable impl now that nothing outside the module needs the bytes. Bug: 534436497 Test: cargo test Change-Id: Id5e6ffdcc09997db3b300b2d73eb36797e626ebb Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/4161
diff --git a/rust/bt-csis/Cargo.toml b/rust/bt-csis/Cargo.toml index 54b3d09..62e32b3 100644 --- a/rust/bt-csis/Cargo.toml +++ b/rust/bt-csis/Cargo.toml
@@ -5,6 +5,7 @@ license.workspace = true [dependencies] +aes.workspace = true bt-common.workspace = true bt-gatt.workspace = true futures.workspace = true
diff --git a/rust/bt-csis/src/client/event.rs b/rust/bt-csis/src/client/event.rs index 99095bf..8809190 100644 --- a/rust/bt-csis/src/client/event.rs +++ b/rust/bt-csis/src/client/event.rs
@@ -242,7 +242,7 @@ let mut stream = client.take_notification_stream().expect("stream available"); // Values read during discovery are visible before any notification. - assert_eq!(client.sirk().value(), &[0xAA; 16]); + assert_eq!(client.sirk(), SetIdentityResolvingKey::new(SirkType::Plaintext, [0xAA; 16])); assert_eq!(client.size(), Some(CoordinatedSetSize::new(NonZeroU8::new(2).unwrap()))); assert_eq!(client.lock_state(), Some(SetMemberLock::Unlocked)); @@ -299,7 +299,7 @@ [0xBB; 16] )) ); - assert_eq!(client.sirk().value(), &[0xBB; 16]); + assert_eq!(client.sirk(), SetIdentityResolvingKey::new(SirkType::Plaintext, [0xBB; 16])); } #[test]
diff --git a/rust/bt-csis/src/crypto.rs b/rust/bt-csis/src/crypto.rs new file mode 100644 index 0000000..ded25aa --- /dev/null +++ b/rust/bt-csis/src/crypto.rs
@@ -0,0 +1,66 @@ +// Copyright 2026 The Fuchsia Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use aes::cipher::{BlockCipherEncrypt, KeyInit}; + +/// Set Identity Hash (SIH) function as defined in CSIS 1.1 Specification, +/// Section 4.7. `sih(k, r) = e(k, r') mod 2^24` +/// where `r' = padding || r`, padding is 104 bits of 0. +/// The LSO of r becomes the LSO of r'. +pub fn set_identity_hash(key: &[u8; 16], r: &[u8; 3]) -> [u8; 3] { + let mut plaintext = [0u8; 16]; + + // r' = padding || r. LSO of r is LSO of r'. + // Since Bluetooth is little endian, LSO is at index 0. + plaintext[0] = r[0]; + plaintext[1] = r[1]; + plaintext[2] = r[2]; + // The rest of the bytes are already 0 (padding). + + // Bluetooth `e` function uses AES-128 but is LSB-first (little endian). + // The `aes` crate is MSB-first. We need to reverse the input and output. + let mut key_reversed = *key; + key_reversed.reverse(); + plaintext.reverse(); + + let key_block = key_reversed.into(); + let mut block = plaintext.into(); + + let cipher = aes::Aes128::new(&key_block); + cipher.encrypt_block(&mut block); + + // sih keeps the least significant 24 bits of e, the last three octets of + // the MSB-first block, returned LSO first. + [block[15], block[14], block[13]] +} + +/// Sample values from the CSIS v1.1 specification, least significant octet +/// first. +#[cfg(test)] +pub(crate) mod test_vectors { + /// Appendix A.1, `k`. + pub(crate) const SIRK: [u8; 16] = [ + 0xcd, 0xcc, 0x72, 0xdd, 0x86, 0x8c, 0xcd, 0xce, 0x22, 0xfd, 0xa1, 0x21, 0x09, 0x7d, 0x7d, + 0x45, + ]; + /// Appendix A.1, `r`. In MSB-first notation, 0x69f563. + pub(crate) const PRAND: [u8; 3] = [0x63, 0xf5, 0x69]; + /// Appendix A.1, `sih(k, r)`. In MSB-first notation, 0x1948da. + pub(crate) const HASH: [u8; 3] = [0xda, 0x48, 0x19]; + + /// The RSI that a Set Member holding `SIRK` advertises for `PRAND`, which + /// is `hash || prand`. + pub(crate) const RSI: [u8; 6] = [HASH[0], HASH[1], HASH[2], PRAND[0], PRAND[1], PRAND[2]]; +} + +#[cfg(test)] +mod tests { + use super::test_vectors::{HASH, PRAND, SIRK}; + use super::*; + + #[test] + fn set_identity_hash_matches_the_spec_vector() { + assert_eq!(set_identity_hash(&SIRK, &PRAND), HASH); + } +}
diff --git a/rust/bt-csis/src/lib.rs b/rust/bt-csis/src/lib.rs index f5ee452..a7bf80d 100644 --- a/rust/bt-csis/src/lib.rs +++ b/rust/bt-csis/src/lib.rs
@@ -3,4 +3,5 @@ // found in the LICENSE file. pub mod client; +pub mod crypto; pub mod types;
diff --git a/rust/bt-csis/src/types.rs b/rust/bt-csis/src/types.rs index 04ef0d0..ba0b077 100644 --- a/rust/bt-csis/src/types.rs +++ b/rust/bt-csis/src/types.rs
@@ -7,6 +7,8 @@ use bt_common::packet_encoding::{Decodable, Encodable, Error as PacketError}; use bt_common::{decodable_enum, Uuid}; +use crate::crypto::set_identity_hash; + /// 16-bit UUID value for the Coordinated Set Identification Service and its /// characteristics. pub const COORDINATED_SET_IDENTIFICATION_SERVICE_UUID: Uuid = Uuid::from_u16(0x1846); @@ -48,8 +50,71 @@ self.sirk_type } - pub fn value(&self) -> &[u8; 16] { - &self.value + /// Resolves `rsi` with this SIRK. See CSIS v1.1 Section 4.9. + // TODO(b/534436497): Decrypt an encrypted SIRK with the LTK (CSIS v1.1 + // Section 4.6). + pub fn resolve_rsi(&self, rsi: ResolvableSetIdentifier) -> RsiResolution { + if self.sirk_type == SirkType::Encrypted { + return RsiResolution::NeedsDecryption; + } + + if set_identity_hash(&self.value, rsi.prand()) == *rsi.hash() { + RsiResolution::Resolved + } else { + RsiResolution::NotResolved + } + } +} + +/// The outcome of resolving a Resolvable Set Identifier with a SIRK. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RsiResolution { + /// The RSI was generated from this SIRK. + Resolved, + /// The RSI was not generated from this SIRK. + NotResolved, + /// The SIRK is encrypted and has to be decrypted before it can resolve the + /// RSI. + NeedsDecryption, +} + +/// The Resolvable Set Identifier that a Set Member advertises so that a Set +/// Coordinator holding the SIRK can recognize it without connecting. +/// +/// Carried as `hash || prand`, least significant octet first, over six octets. +/// See CSIS v1.1 Section 4.8. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ResolvableSetIdentifier { + hash: [u8; 3], + prand: [u8; 3], +} + +impl ResolvableSetIdentifier { + pub const BYTE_SIZE: usize = 6; + + pub fn hash(&self) -> &[u8; 3] { + &self.hash + } + + pub fn prand(&self) -> &[u8; 3] { + &self.prand + } +} + +impl TryFrom<[u8; 6]> for ResolvableSetIdentifier { + type Error = PacketError; + + /// Rejects `bytes` unless the two most significant bits of its prand are 0 + /// and 1, as Core Spec v5.3 Vol 3, Part H, Section 1.3 requires. + fn try_from(bytes: [u8; 6]) -> Result<Self, Self::Error> { + if (bytes[5] & 0xc0) != 0x40 { + return Err(PacketError::InvalidParameter(format!( + "prand has invalid most significant bits: {:#04x}", + bytes[5] + ))); + } + + Ok(Self { hash: [bytes[0], bytes[1], bytes[2]], prand: [bytes[3], bytes[4], bytes[5]] }) } } @@ -87,23 +152,6 @@ } } -impl Encodable for SetIdentityResolvingKey { - type Error = PacketError; - - fn encoded_len(&self) -> usize { - Self::BYTE_SIZE - } - - fn encode(&self, buf: &mut [u8]) -> Result<(), Self::Error> { - if buf.len() < Self::BYTE_SIZE { - return Err(PacketError::BufferTooSmall); - } - buf[0] = self.sirk_type.into(); - buf[1..17].copy_from_slice(&self.value); - Ok(()) - } -} - /// The Set Member Rank characteristic exposes a numeric value that is unique /// within a Coordinated Set (0x01 to set size). See CSIS v1.1 Section 5.4. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -259,8 +307,13 @@ #[cfg(test)] mod tests { use super::*; + use crate::crypto::test_vectors::{RSI, SIRK}; use assert_matches::assert_matches; + fn rsi(bytes: [u8; 6]) -> ResolvableSetIdentifier { + ResolvableSetIdentifier::try_from(bytes).expect("bytes are a well formed RSI") + } + #[test] fn sirk_decoding() { let mut buf = [0; 17]; @@ -269,18 +322,51 @@ let (res, consumed) = SetIdentityResolvingKey::decode(&buf); assert_eq!(consumed, 17); - let sirk = res.unwrap(); - assert_eq!(sirk.sirk_type(), SirkType::Plaintext); - assert_eq!(sirk.value(), &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); + assert_eq!( + res.unwrap(), + SetIdentityResolvingKey::new( + SirkType::Plaintext, + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + ) + ); } #[test] - fn sirk_encoding() { - let sirk = SetIdentityResolvingKey::new(SirkType::Encrypted, [1; 16]); - let mut buf = [0; 17]; - sirk.encode(&mut buf).unwrap(); - assert_eq!(buf[0], 0x00); - assert_eq!(buf[1..17], [1; 16]); + fn plaintext_sirk_resolves_its_own_rsi() { + let sirk = SetIdentityResolvingKey::new(SirkType::Plaintext, SIRK); + assert_eq!(sirk.resolve_rsi(rsi(RSI)), RsiResolution::Resolved); + + let mut foreign = RSI; + foreign[0] ^= 0xff; + assert_eq!(sirk.resolve_rsi(rsi(foreign)), RsiResolution::NotResolved); + } + + #[test] + fn encrypted_sirk_needs_decryption() { + let sirk = SetIdentityResolvingKey::new(SirkType::Encrypted, SIRK); + assert_eq!(sirk.resolve_rsi(rsi(RSI)), RsiResolution::NeedsDecryption); + } + + #[test] + fn rsi_splits_into_hash_and_prand() { + let rsi = rsi(RSI); + assert_eq!(rsi.hash(), &[RSI[0], RSI[1], RSI[2]]); + assert_eq!(rsi.prand(), &[RSI[3], RSI[4], RSI[5]]); + } + + #[test] + fn rsi_with_malformed_prand_is_rejected() { + // The two most significant bits of prand must be 0 and 1. + for msbs in [0x00, 0x80, 0xc0] { + let mut bytes = RSI; + bytes[5] = msbs | (RSI[5] & 0x3f); + + assert_matches!( + ResolvableSetIdentifier::try_from(bytes), + Err(PacketError::InvalidParameter(_)), + "prand MSBs {msbs:#04x} should be rejected" + ); + } } #[test]
diff --git a/rust/bt-set-coordinator/Cargo.toml b/rust/bt-set-coordinator/Cargo.toml index ad3d9f8..3ba82e4 100644 --- a/rust/bt-set-coordinator/Cargo.toml +++ b/rust/bt-set-coordinator/Cargo.toml
@@ -9,4 +9,3 @@ bt-common.workspace = true bt-gatt.workspace = true thiserror.workspace = true -aes.workspace = true
diff --git a/rust/bt-set-coordinator/src/types.rs b/rust/bt-set-coordinator/src/types.rs index 329bf35..5ca8169 100644 --- a/rust/bt-set-coordinator/src/types.rs +++ b/rust/bt-set-coordinator/src/types.rs
@@ -2,8 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -use aes::cipher::{BlockCipherEncrypt, KeyInit}; -use bt_csis::types::{SetIdentityResolvingKey, SirkType}; +use bt_csis::types::{ResolvableSetIdentifier, RsiResolution, SetIdentityResolvingKey}; use bt_gatt::central::{AdvertisingDatum, ScanResult}; /// Represents a Coordinated Set managed by the Set Coordinator. @@ -21,125 +20,38 @@ /// RSI (in standalone ResolvableSetIdentifier AD Type) and attempting to /// resolve it with this set's SIRK. pub fn matches_scan_result(&self, scan_result: &ScanResult) -> bool { - scan_result.advertised.iter().any(|datum| match datum { - AdvertisingDatum::ResolvableSetIdentifier(rsi) => self.resolves_rsi(rsi), - _ => false, - }) + scan_result + .advertised + .iter() + .filter_map(|datum| match datum { + AdvertisingDatum::ResolvableSetIdentifier(bytes) => { + ResolvableSetIdentifier::try_from(*bytes).ok() + } + _ => None, + }) + .any(|rsi| self.sirk.resolve_rsi(rsi) == RsiResolution::Resolved) } - - /// Resolves an RSI using the Set Identity Resolving Key (SIRK). - /// - /// RSI format (6 bytes): - /// bytes 0..3: hash - /// bytes 3..6: prand - /// - /// See CSIP v1.1 Section 4.8 & Core Spec Vol 3 Part H Section 1.3. - /// An encrypted SIRK cannot resolve RSIs until decrypted. - pub fn resolves_rsi(&self, rsi: &[u8; 6]) -> bool { - if self.sirk.sirk_type() == SirkType::Encrypted { - return false; - } - - // Per CSIP v1.1 Section 4.8, the two most significant bits of prand - // (bits 7 and 6 of rsi[5]) must be 0 and 1 (0x40). - if (rsi[5] & 0xc0) != 0x40 { - return false; - } - - let mut prand = [0u8; 3]; - prand.copy_from_slice(&rsi[3..6]); - - let hash_expected = &rsi[0..3]; - let hash_computed = set_identity_hash(self.sirk.value(), &prand); - - hash_computed == *hash_expected - } -} - -/// Set Identity Hash (SIH) function as defined in CSIS 1.1 Specification, -/// Section 4.7. `sih(k, r) = e(k, r') mod 2^24` -/// where `r' = padding || r`, padding is 104 bits of 0. -/// The LSO of r becomes the LSO of r'. -fn set_identity_hash(key: &[u8; 16], r: &[u8; 3]) -> [u8; 3] { - let mut plaintext = [0u8; 16]; - - // r' = padding || r. LSO of r is LSO of r'. - // Since Bluetooth is little endian, LSO is at index 0. - plaintext[0] = r[0]; - plaintext[1] = r[1]; - plaintext[2] = r[2]; - // The rest of the bytes are already 0 (padding). - - // Bluetooth `e` function uses AES-128 but is LSB-first (little endian). - // The `aes` crate is MSB-first. We need to reverse the input and output. - let mut key_reversed = *key; - key_reversed.reverse(); - plaintext.reverse(); - - let key_block = key_reversed.into(); - let mut block = plaintext.into(); - - let cipher = aes::Aes128::new(&key_block); - cipher.encrypt_block(&mut block); - - let mut output = [0u8; 16]; - output.copy_from_slice(block.as_slice()); - output.reverse(); - - // Output of security function e is truncated to 24 bits by taking the least - // significant 24 bits. LSO is at index 0. - [output[0], output[1], output[2]] } #[cfg(test)] mod tests { use super::*; use bt_common::PeerId; - use bt_csis::types::COORDINATED_SET_IDENTIFICATION_SERVICE_UUID; - use bt_gatt::central::PeerName; + use bt_csis::types::{SirkType, COORDINATED_SET_IDENTIFICATION_SERVICE_UUID}; + use bt_gatt::central::{AdvertisingDatum, PeerName}; - // Test vector from CSIS Spec v1.1 Appendix A.1 - // Given k (SIRK): 0xcd, 0xcc, 0x72, 0xdd, 0x86, 0x8c, 0xcd, 0xce, 0x22, - // 0xfd, 0xa1, 0x21, 0x09, 0x7d, 0x7d, 0x45 (LSO to MSO) Given r - // (prand): 0x63, 0xf5, 0x69 (LSO first; in MSB-first notation 0x69f563) - // Result hash: 0xda, 0x48, 0x19 (LSO first; in MSB-first notation - // 0x1948da) + // Test vector from CSIS Spec v1.1 Appendix A.1, least significant octet + // first. const SAMPLE_KEY: [u8; 16] = [ 0xcd, 0xcc, 0x72, 0xdd, 0x86, 0x8c, 0xcd, 0xce, 0x22, 0xfd, 0xa1, 0x21, 0x09, 0x7d, 0x7d, 0x45, ]; - const SAMPLE_PRAND: [u8; 3] = [0x63, 0xf5, 0x69]; - const SAMPLE_HASH: [u8; 3] = [0xda, 0x48, 0x19]; // RSI = hash || prand. In LSO first: bytes 0..3 are hash, bytes 3..6 are // prand. const SAMPLE_RSI: [u8; 6] = [0xda, 0x48, 0x19, 0x63, 0xf5, 0x69]; #[test] - fn set_identity_hash_test() { - let hash_computed = set_identity_hash(&SAMPLE_KEY, &SAMPLE_PRAND); - assert_eq!(hash_computed, SAMPLE_HASH); - } - - #[test] - fn resolves_rsi() { - let sirk = SetIdentityResolvingKey::new(SirkType::Plaintext, SAMPLE_KEY); - let set = CoordinatedSet::new(sirk); - - assert!(set.resolves_rsi(&SAMPLE_RSI)); - - // Encrypted SIRK cannot resolve RSI - let encrypted_sirk = SetIdentityResolvingKey::new(SirkType::Encrypted, SAMPLE_KEY); - let encrypted_set = CoordinatedSet::new(encrypted_sirk); - assert!(!encrypted_set.resolves_rsi(&SAMPLE_RSI)); - - // Invalid prand MSBs (bit 7 and bit 6 of rsi[5] != 0x40) - let mut invalid_rsi = SAMPLE_RSI; - invalid_rsi[5] = 0x00; // Bit 6 is 0 instead of 1 - assert!(!set.resolves_rsi(&invalid_rsi)); - } - - #[test] fn matches_scan_result_rsi() { let sirk = SetIdentityResolvingKey::new(SirkType::Plaintext, SAMPLE_KEY); let set = CoordinatedSet::new(sirk);