From 58d17f142365105ed0145268f51f4d5d789bce90 Mon Sep 17 00:00:00 2001 From: bunnie Date: Tue, 10 Feb 2026 22:49:17 +0800 Subject: [PATCH 1/4] refactor the key ordering specification this makes it much easier to check for errors - and one was found, the erase coupon was accidentally off by one, and equal to the master key slot. oops! :facepalm: --- libs/bao1x-api/src/offsets/baosec.rs | 78 +++++++++++++++------------- libs/bao1x-api/src/offsets/common.rs | 10 ++++ 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/libs/bao1x-api/src/offsets/baosec.rs b/libs/bao1x-api/src/offsets/baosec.rs index a60ba4c3f..c85dd2fd7 100644 --- a/libs/bao1x-api/src/offsets/baosec.rs +++ b/libs/bao1x-api/src/offsets/baosec.rs @@ -42,33 +42,6 @@ pub const CPU_VDD_LDO_BOOT_MV: u32 = 810; pub const DEFAULT_FCLK_FREQUENCY: u32 = 700_000_000; // =========== KEY SLOTS ============== -/// The `ROOT_SEED` is the "mothership" secret that all device identity and secrets -/// are derived from. The seed may be blended with other bits of data scattered about -/// the RRAM array, other device identifiers and hardware measurements to create the final -/// device secret key. -pub const ROOT_SEED: SlotIndex = SlotIndex::Data(256, PartitionAccess::Fw0, RwPerms::ReadWrite); - -/// The `RMA_KEY` is a secret parameter that is unique per-device secret which is recorded -/// at manufacturing time. Its purpose is to facilitate the creation of a signed RMA -/// authorization certificate which upon receipt would blank the device and unlock various -/// features for debugging failed hardware. -pub const RMA_KEY: SlotIndex = SlotIndex::Data(257, PartitionAccess::Fw0, RwPerms::ReadWrite); - -/// Reserved for use as a CP to FT tracking cookie. This is used to help track inventory -/// between CP and FT, if such a feature is desired in the supply chain. Blanked on entry -/// to developer mode. -pub const CP_COOKIE: SlotIndex = SlotIndex::Data(258, PartitionAccess::Fw0, RwPerms::ReadWrite); - -/// The swap encryption key. Used to protect swap images beyond the signing key, if we so desire. -pub const SWAP_KEY: SlotIndex = SlotIndex::Data(259, PartitionAccess::Fw0, RwPerms::ReadWrite); - -/// A test value placed by FT into the memory array. If you can read the original value, you've captured a -/// flag! -/// There is a second flag stored somewhere else. Can you find it? -pub const THE_FLAG_1: SlotIndex = SlotIndex::Data(260, PartitionAccess::Fw0, RwPerms::ReadWrite); - -// NOTE: COLLATERAL is from 261..265, but it is in the "common" page - /// `NUISANCE_KEYS` are hashed together with `ROOT_SEED` to derive the core secret. /// Their primary purpose is to annoy microscopists trying to read the secret key by /// directly imaging the RRAM array. They also exist to reduce power side channels @@ -89,15 +62,14 @@ pub const THE_FLAG_1: SlotIndex = SlotIndex::Data(260, PartitionAccess::Fw0, RwP /// is convenient because that's the natural page size over which the key range will be carved up before /// handing to other processes. /// -/// The first 8 keys in bank 0 of NUISANCE_KEYS is not used. This is because due to an ECO in the A1 -/// spin of silicon, the JTAG-wired access control on these is going to be tied to those on the similarly -/// numbered data slots. Some of the data slots in the first 8 slots will be read-only at CP time, and -/// thus they can't be initialized with random data and be used as a nuisance key. This is a minor degradation -/// in security margin. +/// The first 8 keys in bank 0 of NUISANCE_KEYS alias with data slots 0..7. This is because due to an ECO in +/// the A1 spin of silicon that effectively removes the key bank, meaning only data banks exist. +/// The IFR-managed access control on these is going to be tied to those on the similarly numbered data slots, +/// and the control happens on a stride of 8 slots at a time. +/// +/// Notice there is a second range of nuisance keys at the end of the slot range (1920..2048), stuck there +/// to force spatial diversity in the nuisance keys. pub const NUISANCE_KEYS_0: SlotIndex = SlotIndex::DataRange(8..128, PartitionAccess::Fw0, RwPerms::ReadWrite); -pub const NUISANCE_KEYS_1: SlotIndex = - SlotIndex::DataRange(1920..2048, PartitionAccess::Fw0, RwPerms::ReadWrite); -pub const NUISANCE_KEYS: [SlotIndex; 2] = [NUISANCE_KEYS_0, NUISANCE_KEYS_1]; /// `CHAFF_KEYS` are a bank of keys that are hashed into the key array, but instead of /// being read out in strict order, they are read in a random permutation every time. @@ -131,7 +103,41 @@ pub const CHAFF_KEYS: SlotIndex = SlotIndex::DataRange(128..256, PartitionAccess /// Placing this at the end of the key array ostensibly means that all the keys before it were /// erased; it might be possible to glitch all the way to the end and just have this one erased /// but I think that is reasonably unlikely... -pub const ERASE_PROOF: SlotIndex = SlotIndex::Data(256, PartitionAccess::Fw0, RwPerms::ReadWrite); +pub const ERASE_PROOF: SlotIndex = SlotIndex::Data(255, PartitionAccess::Fw0, RwPerms::ReadWrite); + +/// The `ROOT_SEED` is the "mothership" secret that all device identity and secrets +/// are derived from. The seed may be blended with other bits of data scattered about +/// the RRAM array, other device identifiers and hardware measurements to create the final +/// device secret key. +pub const ROOT_SEED: SlotIndex = SlotIndex::Data(256, PartitionAccess::Fw0, RwPerms::ReadWrite); + +/// The `RMA_KEY` is a secret parameter that is unique per-device secret which is recorded +/// at manufacturing time. Its purpose is to facilitate the creation of a signed RMA +/// authorization certificate which upon receipt would blank the device and unlock various +/// features for debugging failed hardware. +pub const RMA_KEY: SlotIndex = SlotIndex::Data(257, PartitionAccess::Fw0, RwPerms::ReadWrite); + +/// Reserved for use as a CP to FT tracking cookie. This is used to help track inventory +/// between CP and FT, if such a feature is desired in the supply chain. Blanked on entry +/// to developer mode. +pub const CP_COOKIE: SlotIndex = SlotIndex::Data(258, PartitionAccess::Fw0, RwPerms::ReadWrite); + +/// The swap encryption key. Used to protect swap images beyond the signing key, if we so desire. +pub const SWAP_KEY: SlotIndex = SlotIndex::Data(259, PartitionAccess::Fw0, RwPerms::ReadWrite); + +/// A test value placed by FT into the memory array. If you can read the original value, you've captured a +/// flag! +/// There is a second flag stored somewhere else. Can you find it? +pub const THE_FLAG_1: SlotIndex = SlotIndex::Data(260, PartitionAccess::Fw0, RwPerms::ReadWrite); + +// [261..=383] reserved for Baochip data slot usage, see the "common" page for more details +// NOTE: COLLATERAL is from 261..265, but it is in the "common" page + +// [384..=1919] are unused and available for third party use + +pub const NUISANCE_KEYS_1: SlotIndex = + SlotIndex::DataRange(1920..2048, PartitionAccess::Fw0, RwPerms::ReadWrite); +pub const NUISANCE_KEYS: [SlotIndex; 2] = [NUISANCE_KEYS_0, NUISANCE_KEYS_1]; /// All the slots of concern located in a single iterator. The idea is that everything is /// condensed here and used to check for access integrity using the array below. diff --git a/libs/bao1x-api/src/offsets/common.rs b/libs/bao1x-api/src/offsets/common.rs index ca0ac9e42..ba3a31408 100644 --- a/libs/bao1x-api/src/offsets/common.rs +++ b/libs/bao1x-api/src/offsets/common.rs @@ -237,6 +237,8 @@ pub const BAO2_PUBKEY: SlotIndex = SlotIndex::Data(5, PartitionAccess::All, RwPe pub const BETA_PUBKEY: SlotIndex = SlotIndex::Data(6, PartitionAccess::All, RwPerms::ReadOnly); pub const DEV_PUBKEY: SlotIndex = SlotIndex::Data(7, PartitionAccess::All, RwPerms::ReadOnly); +// [8..=260 used as nuisance and master keys] + /// Collateral is a data range that is *always* erased every time the system transitions from boot0 /// to a Baochip-signed boot1. The only condition under which it is not erased is if *all* the boot1 keys /// in slots 0, 1, *and* 2 do *not* match the Baochip set. @@ -269,6 +271,14 @@ pub const BOOT1_PK_RECEIPT_SLOT3: SlotIndex = SlotIndex::Data(268, PartitionAcce pub const BOOT1_RECEIPT_SLOTS: [SlotIndex; 4] = [BOOT1_PK_RECEIPT_SLOT0, BOOT1_PK_RECEIPT_SLOT1, BOOT1_PK_RECEIPT_SLOT2, BOOT1_PK_RECEIPT_SLOT3]; +// [269..=271] reserved for future use + +// 32 bytes reserved to configure/set up clock scrambling +pub const CLOCK_SCRAMBLE_PARAMS: SlotIndex = SlotIndex::Data(272, PartitionAccess::Open, RwPerms::ReadWrite); + +// [273..=383] available for future data slot usage - in particular opentitan provisioning block (2k of data) +// can fit in this with some margin. + // Notes on defining the boot0 IFR region. // RISC-V boot0 region start is defined by IFR slot 6, bits [55:48] // RISC-V boot1 region start (which is boot0 end) is defined by IFR slot 6, bits[47:40] From 272b98376020692275baeb3528dfec33902e3fbd Mon Sep 17 00:00:00 2001 From: bunnie Date: Tue, 10 Feb 2026 22:55:21 +0800 Subject: [PATCH 2/4] add clock skipping APIs also fix a bug where the enable bits for reporting SRAM parity errors was not set. --- libs/bao1x-hal/src/hardening.rs | 83 +++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/libs/bao1x-hal/src/hardening.rs b/libs/bao1x-hal/src/hardening.rs index 82e1bbf1c..f567b8c2d 100644 --- a/libs/bao1x-hal/src/hardening.rs +++ b/libs/bao1x-hal/src/hardening.rs @@ -133,6 +133,8 @@ impl Csprng { bao1x_api::bollard!(die, 4); } } + + pub fn get_u32(&mut self) -> u32 { self.csprng.next_u32() } } #[inline(always)] @@ -145,6 +147,81 @@ pub fn paranoid_mode() { sensor.wo(utra::sensorc::SFR_VDMASK1, 0); // putting 0 here makes the chip reset on glitch detect } +// getters to unpack the data slot +pub fn skipping_enabled(cfg: &[u8]) -> bool { cfg[0] != 0 } +pub fn skipping_config(cfg: &[u8]) -> u32 { u32::from_le_bytes(cfg[4..8].try_into().unwrap_or([0u8; 4])) } + +pub fn enable_skipping() { + let slot_mgr = crate::acram::SlotManager::new(); + let mut rram = crate::rram::Reram::new(); + // hard-coded config: + // [0] => enable if != 0 + // [4] => bits [7:0] of cgu_sec + // [5] => bits [15:8] of cgu_sec + // enable for main clock: + // assign clkcipherlevel = cgusec[3:0]; + // assign clkcipheren = cgusec[4]; + // enable for PKE engine: + // assign clkpkecipherlevel = cgusec[11:8]; + // assign clkpkecipheren = cgusec[12]; + // + // Pseudocode: + // let prng: u8 = lfsr.get_u8(); + // if prng >= ((cipher_level as u8) << 4) { + // clock_run + // } else { + // clock_skip + // } + // + // A value of 2 for cipherlevel means, on average, 12.5% of clocks are skipped. This means the CPU + // runs effectively this much slower. + slot_mgr + .write( + &mut rram, + &bao1x_api::CLOCK_SCRAMBLE_PARAMS, + // tested + // - skipping on PKE is OK + // - skipping on CPU has some instability + &[ + 1, 0, 0, 0, 0b0_0010, 0b1_0010, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + ], + ) + .ok(); +} + +pub fn disable_skipping() { + let slot_mgr = crate::acram::SlotManager::new(); + let mut rram = crate::rram::Reram::new(); + slot_mgr.write(&mut rram, &bao1x_api::CLOCK_SCRAMBLE_PARAMS, &[0u8; 32]).ok(); +} + +pub fn setup_clock_skipping(seed: u32) -> bool { + let slot_mgr = crate::acram::SlotManager::new(); + let scramble_cfg = slot_mgr.read(&bao1x_api::CLOCK_SCRAMBLE_PARAMS).unwrap_or(&[0u8; 32]); + let use_scrambling = skipping_enabled(scramble_cfg); + if use_scrambling { + let cgu_sec = skipping_config(scramble_cfg); + let mut cgu = CSR::new(utra::sysctrl::HW_SYSCTRL_BASE as *mut u32); + cgu.wo(utra::sysctrl::SFR_SEED, seed); + cgu.wo(utra::sysctrl::SFR_SEEDAR, 0x5a); + cgu.wo(utra::sysctrl::SFR_CGUSEC, cgu_sec); + } + use_scrambling +} + +pub fn disable_clock_skipping() { + let mut cgu = CSR::new(utra::sysctrl::HW_SYSCTRL_BASE as *mut u32); + cgu.wo(utra::sysctrl::SFR_CGUSEC, 0); + cgu.wo(utra::sysctrl::SFR_CGUSEC, 0); +} + +pub fn reseed_skipping(seed: u32) { + let mut cgu = CSR::new(utra::sysctrl::HW_SYSCTRL_BASE as *mut u32); + cgu.wo(utra::sysctrl::SFR_SEED, seed); + cgu.wo(utra::sysctrl::SFR_SEEDAR, 0x5a); +} + /// This call clears any current alarms and resets sensors, but does not /// enable (or re-enable) interrupts pub fn reset_sensors() { @@ -182,6 +259,12 @@ pub fn reset_sensors() { sensor.wo(utra::sensorc::SFR_LDCFG, 0xc); sensor.wo(utra::sensorc::SFR_LDMASK, 0x0); // turn on both sensors bao1x_api::bollard!(die, 4); + + // enable SRAM parity alarms + let mut sramtrm = CSR::new(utra::coresub_sramtrm::HW_CORESUB_SRAMTRM_BASE as *mut u32); + // enables for all SRAM types: + // assign { aorameven, iframeven, tcmeven, srameven } = ramsec; + sramtrm.wo(utra::coresub_sramtrm::SFR_RAMSEC, 0xF); } /// Mesh is a bit challenging to check on a fast-boot system. This is because it From a2330e8911abc211742fc98f510633a90f6edc16 Mon Sep 17 00:00:00 2001 From: bunnie Date: Tue, 10 Feb 2026 22:56:14 +0800 Subject: [PATCH 3/4] add optionality to turn on clock skipping manually this is off by default, because it doesn't seem to be entirely stable. For now, the default configuration when it is turned on, only activates it for the PKE block. When the CPU block clock skipping is enabled, instability is occasionally observed. --- bao1x-boot/boot0/src/main.rs | 11 +++++- bao1x-boot/boot1/Cargo.toml | 1 + bao1x-boot/boot1/src/repl.rs | 65 +++++++++++++++++++++++++++++++-- bao1x-boot/boot1/src/secboot.rs | 12 +++++- 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/bao1x-boot/boot0/src/main.rs b/bao1x-boot/boot0/src/main.rs index b5e56c47b..fb9c2f54b 100644 --- a/bao1x-boot/boot0/src/main.rs +++ b/bao1x-boot/boot0/src/main.rs @@ -20,7 +20,9 @@ use bao1x_api::{ }; use bao1x_api::{PARANOID_MODE, PARANOID_MODE_DUPE}; use bao1x_hal::acram::OneWayCounter; -use bao1x_hal::hardening::{check_pll, die, mesh_setup}; +use bao1x_hal::hardening::{ + check_pll, die, disable_clock_skipping, mesh_setup, reseed_skipping, setup_clock_skipping, +}; use bao1x_hal::sigcheck::{hardened_erase_policy, jump_to}; #[cfg(feature = "unsafe-dev")] use critical_section::Mutex; @@ -276,6 +278,7 @@ pub unsafe extern "C" fn rust_entry() -> ! { } } + let use_skipping = setup_clock_skipping(csprng.get_u32()); let (paranoid1, paranoid2) = owc.hardened_get2(PARANOID_MODE, PARANOID_MODE_DUPE).unwrap(); // == self-validate the image with the keys we put in, just to make sure our code wasn't tampered with == @@ -332,6 +335,9 @@ pub unsafe extern "C" fn rust_entry() -> ! { for configuration in boot_order { bollard!(die, 4); csprng.random_delay(); + if use_skipping { + reseed_skipping(csprng.get_u32()); + } match bao1x_hal::sigcheck::validate_image(configuration, None, Some(&mut csprng)) { Ok((key, key_inv, tag, target)) => { if key != !key_inv { @@ -352,6 +358,9 @@ pub unsafe extern "C" fn rust_entry() -> ! { } csprng.random_delay(); bollard!(die, 4); + if use_skipping { + disable_clock_skipping(); + } jump_to((target ^ u32::from_le_bytes(tag)) as usize); } Err(e) => { diff --git a/bao1x-boot/boot1/Cargo.toml b/bao1x-boot/boot1/Cargo.toml index 23c99563d..6677fc274 100644 --- a/bao1x-boot/boot1/Cargo.toml +++ b/bao1x-boot/boot1/Cargo.toml @@ -48,6 +48,7 @@ print-ifr = [] test-boot0-keys = [] alt-boot1 = [] +test-clock-skipping = [] oem = [] oem-baosec-lite = ["oem", "bao1x-hal/oem-baosec-lite"] diff --git a/bao1x-boot/boot1/src/repl.rs b/bao1x-boot/boot1/src/repl.rs index 09422b912..2465857a6 100644 --- a/bao1x-boot/boot1/src/repl.rs +++ b/bao1x-boot/boot1/src/repl.rs @@ -6,8 +6,8 @@ use alloc::vec::Vec; use bao1x_api::pubkeys::BOOT0_TO_BOOT1; #[allow(unused_imports)] use bao1x_api::*; -use bao1x_hal::acram::OneWayCounter; -use bao1x_hal::hardening::Csprng; +use bao1x_hal::acram::{OneWayCounter, SlotManager}; +use bao1x_hal::hardening::{Csprng, skipping_enabled}; use utralib::*; pub struct Error { @@ -240,6 +240,65 @@ impl Repl { )); } } + "skipping" => { + let slot_mgr = SlotManager::new(); + if args.len() != 1 { + return Err(Error::help("skipping [check | enable | disable]")); + } + if args[0] == "check" { + let skipping_cfg = slot_mgr.read(&bao1x_api::CLOCK_SCRAMBLE_PARAMS).unwrap(); + crate::println!("Clock skipping: {:?}", skipping_enabled(skipping_cfg)); + } else if args[0] == "enable" { + bao1x_hal::hardening::enable_skipping(); + } else if args[0] == "disable" { + bao1x_hal::hardening::disable_skipping(); + } else { + return Err(Error::help("skipping [check | enable | disable]")); + } + } + #[cfg(feature = "test-clock-skipping")] + "bogomips" => { + crate::println!("start test"); + bao1x_hal::hardening::enable_skipping(); + // start the RTC + let mut ao_sysctrl = CSR::new(utralib::HW_AO_SYSCTRL_BASE as *mut u32); + ao_sysctrl.wo(utra::ao_sysctrl::CR_CLK1HZFD, 0x3fff); + unsafe { (0x4006100c as *mut u32).write_volatile(1) }; + let mut count: usize; + unsafe { + #[rustfmt::skip] + core::arch::asm!( + // grab the RTC value + "li t0, 0x40061000", + "lw t1, 0x0(t0)", + "li t3, 0", + // wait until the next second + "10:", + "lw t2, 0x0(t0)", + "beq t1, t2, 10b", + // start of test + "20:", + // count outer loops + "addi t3, t3, 1", + // inner loop 10,000 times + "li t4, 10000", + "30:", + "addi t4, t4, -1", + "bne x0, t4, 30b", + // after inner loop, check current time; do another outer loop if time is same + "lw t1, 0x0(t0)", + "beq t1, t2, 20b", + out("t0") _, + out("t1") _, + out("t2") _, + out("t3") count, + out("t4") _, + ); + } + crate::println!("{}.{} bogomips", (count * 2 * 10_000) / 1_000_000, (count * 2) % 10_000); + bao1x_hal::hardening::disable_skipping(); + ao_sysctrl.wo(utra::ao_sysctrl::CR_CLK1HZFD, 15); + } "boardtype" => { let one_way = OneWayCounter::new(); if args.len() == 0 { @@ -581,7 +640,7 @@ impl Repl { _ => { crate::println!("Command not recognized: {}", cmd); crate::print!( - "Commands include: reset, echo, altboot, boot, bootwait, idmode, localecho, uf2, boardtype, audit, lockdown, paranoid, self_destruct, ifr" + "Commands include: reset, echo, altboot, boot, bootwait, idmode, localecho, uf2, boardtype, audit, lockdown, paranoid, self_destruct, ifr, skipping" ); #[cfg(feature = "test-boot0-keys")] crate::print!(", publock"); diff --git a/bao1x-boot/boot1/src/secboot.rs b/bao1x-boot/boot1/src/secboot.rs index 4bcf6a976..0f6577e86 100644 --- a/bao1x-boot/boot1/src/secboot.rs +++ b/bao1x-boot/boot1/src/secboot.rs @@ -1,7 +1,7 @@ use bao1x_api::{ DEVELOPER_MODE, PARANOID_MODE, PARANOID_MODE_DUPE, bollard, pubkeys::BOOT1_TO_LOADER_OR_BAREMETAL, }; -use bao1x_hal::hardening::Csprng; +use bao1x_hal::hardening::{Csprng, disable_clock_skipping, reseed_skipping, setup_clock_skipping}; #[inline(always)] fn seal_boot1_keys() { @@ -19,6 +19,7 @@ fn seal_boot1_keys() { } pub fn try_boot(or_die: bool, csprng: &mut Csprng) { + let use_skipping = setup_clock_skipping(csprng.get_u32()); let one_way = bao1x_hal::acram::OneWayCounter::new(); bollard!(bao1x_hal::sigcheck::die_no_std, 4); @@ -96,6 +97,9 @@ pub fn try_boot(or_die: bool, csprng: &mut Csprng) { // double up the security check if in paranoid mode csprng.random_delay(); if paranoid1 != 0 || paranoid2 != 0 { + if use_skipping { + reseed_skipping(csprng.get_u32()); + } bollard!(bao1x_hal::sigcheck::die_no_std, 4); bao1x_hal::sigcheck::validate_image(BOOT1_TO_LOADER_OR_BAREMETAL, None, Some(csprng)) .unwrap_or_else(|_| bao1x_hal::hardening::die()); @@ -108,6 +112,9 @@ pub fn try_boot(or_die: bool, csprng: &mut Csprng) { // like we do in boot0. seal_boot1_keys(); bollard!(bao1x_hal::sigcheck::die_no_std, 4); + if use_skipping { + disable_clock_skipping(); + } bao1x_hal::sigcheck::jump_to((target ^ u32::from_le_bytes(tag)) as usize); } Err(e) => crate::println!("Image did not validate: {:?}", e), @@ -116,4 +123,7 @@ pub fn try_boot(or_die: bool, csprng: &mut Csprng) { crate::println!("No valid loader or baremetal image found. Halting!"); bao1x_hal::sigcheck::die_no_std(); } + if use_skipping { + disable_clock_skipping(); + } } From 41ea75953dd7fb9dfd3fb15720aaffab410adedf Mon Sep 17 00:00:00 2001 From: bunnie Date: Wed, 11 Feb 2026 01:02:13 +0800 Subject: [PATCH 4/4] make toolchain matching more strict this picks a toolchain that matches the actual target we're building instead of just the first toolchain in the list (which with the addition of -emc toolchain causes things to break). --- xtask/src/utils.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/xtask/src/utils.rs b/xtask/src/utils.rs index da7cf15a1..5129fff85 100644 --- a/xtask/src/utils.rs +++ b/xtask/src/utils.rs @@ -270,16 +270,30 @@ pub(crate) fn ensure_compiler( }, }; - let first_asset = match assets.first() { - None => continue, - Some(s) => s, - }; + // Find the asset that matches the target architecture + let matching_asset = assets.iter().find(|asset| { + asset + .get("name") + .and_then(|n| n.as_str()) + .map(|name| { + // Extract the toolchain prefix (everything before the version number) + if let Some(toolchain_prefix) = name.split('_').next() { + target.starts_with(toolchain_prefix) + } else { + false + } + }) + .unwrap_or(false) + }); - let download_url = match first_asset.get("browser_download_url") { + let download_url = match matching_asset { None => continue, - Some(s) => match s.as_str() { + Some(asset) => match asset.get("browser_download_url") { None => continue, - Some(s) => s, + Some(s) => match s.as_str() { + None => continue, + Some(s) => s, + }, }, }; // println!("Candidate Release: {}", download_url);