Add passive_roaming_validate_mic option.

This option does two things:

1) In case the passive-roaming agreement is not state-less, it will
   trigger the validation of MIC (this was already implemented at the
   roaming-session retrieval, but never used).
2) On PRStartAns, it will return the NwkSKey / FNwkSIntKey to the
   requester (such that the MIC validation can be performed).

For state-less passive-roaming, it is recommended to leave this option
set to false, such that no session-keys are exposed.
This commit is contained in:
Orne Brocaar 2024-05-27 11:20:30 +01:00
parent 04ffcf88a1
commit 52a08acf81
7 changed files with 42 additions and 2 deletions

View File

@ -293,6 +293,7 @@ async fn _handle_pr_start_req_data(
let region_common_name = CommonName::from_str(&pl.ul_meta_data.rf_region)?;
let region_config_id = region::get_region_config_id(region_common_name)?;
let dr = pl.ul_meta_data.data_rate.unwrap_or_default();
let validate_mic = roaming::get_passive_roaming_validate_mic(sender_id)?;
let mut ufs = UplinkFrameSet {
uplink_set_id: Uuid::new_v4(),
@ -318,7 +319,7 @@ async fn _handle_pr_start_req_data(
let kek_label = roaming::get_passive_roaming_kek_label(sender_id)?;
let ds = d.get_device_session()?;
let nwk_s_key = if ds.mac_version().to_string().starts_with("1.0") {
let nwk_s_key = if validate_mic && ds.mac_version().to_string().starts_with("1.0") {
Some(keywrap::wrap(
&kek_label,
AES128Key::from_slice(&ds.nwk_s_enc_key)?,
@ -327,7 +328,7 @@ async fn _handle_pr_start_req_data(
None
};
let f_nwk_s_int_key = if ds.mac_version().to_string().starts_with("1.0") {
let f_nwk_s_int_key = if validate_mic && ds.mac_version().to_string().starts_with("1.0") {
None
} else {
Some(keywrap::wrap(

View File

@ -155,6 +155,25 @@ pub fn get_passive_roaming_kek_label(net_id: NetID) -> Result<String> {
))
}
pub fn get_passive_roaming_validate_mic(net_id: NetID) -> Result<bool> {
let conf = config::get();
for s in &conf.roaming.servers {
if s.net_id == net_id {
return Ok(s.passive_roaming_validate_mic);
}
}
if conf.roaming.default.enabled {
return Ok(conf.roaming.default.passive_roaming_validate_mic);
}
Err(anyhow!(
"Passive-roaming mic-check for net_id {} does not exist",
net_id
))
}
pub fn is_enabled() -> bool {
let conf = config::get();
conf.roaming.default.enabled || !conf.roaming.servers.is_empty()

View File

@ -802,6 +802,13 @@ pub fn run() {
#
# If set, the session-keys will be encrypted using the given KEK.
passive_roaming_kek_label="{{roaming.default.passive_roaming_kek_label}}"
# Passive-roaming validate MIC.
#
# If set ChirpStack will validate the MIC (for non-stateless roaming
# agreements). As well it means it will expose the NwkSKey / FNwkSIntKey
# on PRStartAns.
passive_roaming_validate_mic={{roaming.default.passive_roaming_validate_mic}}
# Server.
#
@ -846,6 +853,13 @@ pub fn run() {
# #
# # If set, the session-keys will be encrypted using the given KEK.
# passive_roaming_kek_label=""
# # Passive-roaming validate MIC.
# #
# # If set ChirpStack will validate the MIC (for non-stateless roaming
# # agreements). As well it means it will expose the NwkSKey / FNwkSIntKey
# # on PRStartAns.
# passive_roaming_validate_mic=false
#
# # Server.
# #
@ -878,6 +892,7 @@ pub fn run() {
async_timeout="{{ this.async_timeout }}"
passive_roaming_lifetime="{{ this.passive_roaming_lifetime }}"
passive_roaming_kek_label="{{ this.passive_roaming_kek_label }}"
passive_roaming_validate_mic={{ this.passive_roaming_validate_mic }}
server="{{ this.server }}"
use_target_role_suffix="{{ this.use_target_role_suffix }}"
ca_cert="{{ this.ca_cert }}"

View File

@ -487,6 +487,7 @@ pub struct RoamingServer {
#[serde(with = "humantime_serde")]
pub passive_roaming_lifetime: Duration,
pub passive_roaming_kek_label: String,
pub passive_roaming_validate_mic: bool,
pub server: String,
pub use_target_role_suffix: bool,
pub ca_cert: String,
@ -504,6 +505,7 @@ pub struct RoamingServerDefault {
#[serde(with = "humantime_serde")]
pub passive_roaming_lifetime: Duration,
pub passive_roaming_kek_label: String,
pub passive_roaming_validate_mic: bool,
pub server: String,
pub use_target_role_suffix: bool,
pub ca_cert: String,

View File

@ -182,6 +182,7 @@ async fn test_sns_uplink() {
// Set roaming agreement.
conf.roaming.servers.push(config::RoamingServer {
net_id: NetID::from_str("000202").unwrap(),
passive_roaming_validate_mic: true,
server: fns_mock.url("/"),
..Default::default()
});

View File

@ -200,6 +200,7 @@ impl Data {
session_id: sess_id.as_bytes().to_vec(),
net_id: net_id.to_vec(),
dev_addr: self.mac_payload.fhdr.devaddr.to_vec(),
validate_mic: roaming::get_passive_roaming_validate_mic(net_id)?,
lifetime: {
let lt = pr_start_ans.lifetime.unwrap_or_default() as i64;
if lt == 0 {

View File

@ -172,6 +172,7 @@ impl JoinRequest {
let sess = internal::PassiveRoamingDeviceSession {
session_id: sess_id.as_bytes().to_vec(),
net_id: self.home_net_id.unwrap().to_vec(),
validate_mic: roaming::get_passive_roaming_validate_mic(self.home_net_id.unwrap())?,
dev_addr: pr_start_ans.dev_addr.clone(),
dev_eui: self.join_request.dev_eui.to_vec(),
lifetime: {