ast10x0-hace: eliminate remaining panic_is_possible in digest/HMAC path

The no_panics_test scans the compiled ELF and fails if any panic path is
reachable. Two root causes remained in the SHA-2 / HMAC code linked into
the hace_sha256 binary:

- div_by_zero: chunks_exact(4).zip(..) in load_iv / digest_from_context.
  ChunksExact stores its chunk size as a runtime field, so Zip::new emits
  a len / chunk_size division the optimizer cannot prove non-zero.
  Replaced with index-stride loops over length-proven [u8; 4] arrays.

- copy_from_slice len_mismatch_fail: the staging copies in
  HaceDigest::update and HaceHmacCtx::update copy between two range-sliced
  &[u8] of equal-by-construction length the optimizer cannot prove equal.
  Replaced with zip element-wise copies (no length assert, no division).

Also hardened the AES IV copies (AesCipher::crypt, AesSkin CBC chaining)
to use <[u8; AES_BLOCK]>::try_from array assignment instead of
copy_from_slice, per the project no-panic patterns.

no_panics_test now PASSES; nm shows no panic_is_possible, len_mismatch_fail,
or div_by_zero symbols.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
diff --git a/target/ast10x0/peripherals/hace/aes.rs b/target/ast10x0/peripherals/hace/aes.rs
index 4c906d1..19ca4a4 100644
--- a/target/ast10x0/peripherals/hace/aes.rs
+++ b/target/ast10x0/peripherals/hace/aes.rs
@@ -106,9 +106,18 @@
         // Engine context: IV at [0..16) for CBC, key at [16..16+keylen).
         self.ctx.ctx = [0u8; 64];
         if let Some(iv) = iv {
-            self.ctx.ctx[..AES_BLOCK].copy_from_slice(iv);
+            // Length-proven array assignment: `iv` is `&[u8; AES_BLOCK]`, so cast
+            // the destination prefix to `&mut [u8; AES_BLOCK]` and copy as fixed
+            // arrays. `copy_from_slice` would keep a length-mismatch panic branch.
+            if let Some(dst) = self.ctx.ctx.get_mut(..AES_BLOCK) {
+                if let Ok(dst) = <&mut [u8; AES_BLOCK]>::try_from(dst) {
+                    *dst = *iv;
+                }
+            }
         }
-        self.ctx.ctx[AES_BLOCK..AES_BLOCK + key.len()].copy_from_slice(key);
+        if let Some(dst) = self.ctx.ctx.get_mut(AES_BLOCK..AES_BLOCK + key.len()) {
+            dst.copy_from_slice(key);
+        }
 
         // DMA safety (D3): copy caller input into the .ram_nc staging buffer.
         // The HACE engine reads/writes by physical address; if the caller's
@@ -116,7 +125,9 @@
         // would silently read/write stale physical RAM. Staging through
         // ctx.data_in / ctx.data_out (both inside the .ram_nc CryptoContext)
         // guarantees the DMA addresses are always in non-cacheable SRAM.
-        self.ctx.data_in[..input.len()].copy_from_slice(input);
+        if let Some(dst) = self.ctx.data_in.get_mut(..input.len()) {
+            dst.copy_from_slice(input);
+        }
 
         // SG descriptors: addr = .ram_nc staging buffers, len = bytes | HACE_SG_LAST.
         let in_ptr = ptr_to_u32(self.ctx.data_in.as_ptr())?;
@@ -164,11 +175,11 @@
                         .ok_or(HaceError::InvalidInput)?,
                 );
             // Scrub staging buffers so plaintext/ciphertext doesn't linger.
-            self.ctx.data_in[..n].fill(0);
-            self.ctx.data_out[..n].fill(0);
+            if let Some(s) = self.ctx.data_in.get_mut(..n) { s.fill(0); }
+            if let Some(s) = self.ctx.data_out.get_mut(..n) { s.fill(0); }
             Ok(())
         } else {
-            self.ctx.data_in[..input.len()].fill(0);
+            if let Some(s) = self.ctx.data_in.get_mut(..input.len()) { s.fill(0); }
             Err(HaceError::Timeout)
         }
     }
@@ -371,7 +382,9 @@
         // Advance IV to last ciphertext block so sequential encrypt() calls
         // form a correct CBC chain instead of reusing the original IV.
         if let Some(last) = ct.get(N - AES_BLOCK..) {
-            self.iv.copy_from_slice(last);
+            if let Ok(last) = <[u8; AES_BLOCK]>::try_from(last) {
+                self.iv = last;
+            }
         }
         Ok(ct)
     }
@@ -383,7 +396,9 @@
             .cbc_decrypt(self.key.as_slice(), &self.iv, &ciphertext, &mut pt)?;
         // Advance IV to last ciphertext block (CBC decrypt chaining).
         if let Some(last) = ciphertext.get(N - AES_BLOCK..) {
-            self.iv.copy_from_slice(last);
+            if let Ok(last) = <[u8; AES_BLOCK]>::try_from(last) {
+                self.iv = last;
+            }
         }
         Ok(pt)
     }
diff --git a/target/ast10x0/peripherals/hace/digest.rs b/target/ast10x0/peripherals/hace/digest.rs
index 19fc07c..e0b43b7 100644
--- a/target/ast10x0/peripherals/hace/digest.rs
+++ b/target/ast10x0/peripherals/hace/digest.rs
@@ -45,8 +45,11 @@
 
     fn digest_from_context(ctx: &HashContext) -> Self::Digest {
         let mut out = [0u32; SHA256_DIGEST_SIZE / 4];
-        for (i, chunk) in ctx.digest[..SHA256_DIGEST_SIZE].chunks_exact(4).enumerate() {
-            out[i] = u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
+        for (i, dst) in out.iter_mut().enumerate() {
+            let off = i * 4;
+            if let Some(chunk) = ctx.digest.get(off..off + 4) {
+                *dst = u32::from_ne_bytes(<[u8; 4]>::try_from(chunk).unwrap_or([0u8; 4]));
+            }
         }
         Digest::new(out)
     }
@@ -62,8 +65,11 @@
 
     fn digest_from_context(ctx: &HashContext) -> Self::Digest {
         let mut out = [0u32; SHA384_DIGEST_SIZE / 4];
-        for (i, chunk) in ctx.digest[..SHA384_DIGEST_SIZE].chunks_exact(4).enumerate() {
-            out[i] = u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
+        for (i, dst) in out.iter_mut().enumerate() {
+            let off = i * 4;
+            if let Some(chunk) = ctx.digest.get(off..off + 4) {
+                *dst = u32::from_ne_bytes(<[u8; 4]>::try_from(chunk).unwrap_or([0u8; 4]));
+            }
         }
         Digest::new(out)
     }
@@ -79,8 +85,11 @@
 
     fn digest_from_context(ctx: &HashContext) -> Self::Digest {
         let mut out = [0u32; SHA512_DIGEST_SIZE / 4];
-        for (i, chunk) in ctx.digest[..SHA512_DIGEST_SIZE].chunks_exact(4).enumerate() {
-            out[i] = u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
+        for (i, dst) in out.iter_mut().enumerate() {
+            let off = i * 4;
+            if let Some(chunk) = ctx.digest.get(off..off + 4) {
+                *dst = u32::from_ne_bytes(<[u8; 4]>::try_from(chunk).unwrap_or([0u8; 4]));
+            }
         }
         Digest::new(out)
     }
@@ -193,7 +202,11 @@
         // 4. Zero bufcnt and digcnt
         let iv = T::iv();
         self.ctx.method = T::HASH_CMD;
-        load_iv(&mut self.ctx.digest[..iv.len() * 4], iv)?;
+        let iv_bytes = iv.len().saturating_mul(4);
+        match self.ctx.digest.get_mut(..iv_bytes) {
+            Some(dst) => load_iv(dst, iv)?,
+            None => return Err(HaceError::InvalidInput),
+        }
         self.ctx.block_size = T::BLOCK_SIZE as u32;
         self.ctx.bufcnt = 0;
         self.ctx.digcnt = [0; 2];
@@ -252,7 +265,14 @@
             let src = input
                 .get(offset..offset.saturating_add(chunk_len))
                 .ok_or(HaceError::InvalidInput)?;
-            dst.copy_from_slice(src);
+            // Element-wise copy instead of `copy_from_slice`: `dst` and `src` are
+            // both `chunk_len` long by construction, but the optimizer cannot
+            // prove `dst.len() == src.len()` through the two `get`/`get_mut`
+            // range slices, so `copy_from_slice` would keep its length-mismatch
+            // panic branch. The zip-copy is provably panic-free.
+            for (d, s) in dst.iter_mut().zip(src.iter()) {
+                *d = *s;
+            }
 
             self.ctx.bufcnt += chunk_len as u32;
             offset += chunk_len;
diff --git a/target/ast10x0/peripherals/hace/helpers.rs b/target/ast10x0/peripherals/hace/helpers.rs
index 3e38631..c4e6ce8 100644
--- a/target/ast10x0/peripherals/hace/helpers.rs
+++ b/target/ast10x0/peripherals/hace/helpers.rs
@@ -33,18 +33,30 @@
         128 + 112 - index
     };
 
-    ctx.buffer[bufcnt] = 0x80;
-    ctx.buffer[bufcnt + 1..bufcnt + padlen].fill(0);
+    ctx.buffer
+        .get_mut(bufcnt)
+        .map(|b| *b = 0x80)
+        .unwrap_or(());
+    ctx.buffer
+        .get_mut(bufcnt + 1..bufcnt + padlen)
+        .map(|s| s.fill(0))
+        .unwrap_or(());
 
     if block_size == 64 {
         let bits = (ctx.digcnt[0] << 3).to_be_bytes();
-        ctx.buffer[bufcnt + padlen..bufcnt + padlen + 8].copy_from_slice(&bits);
+        if let Some(dst) = ctx.buffer.get_mut(bufcnt + padlen..bufcnt + padlen + 8) {
+            dst.copy_from_slice(&bits);
+        }
         ctx.bufcnt += (padlen + 8) as u32;
     } else {
         let low = (ctx.digcnt[0] << 3).to_be_bytes();
         let high = ((ctx.digcnt[1] << 3) | (ctx.digcnt[0] >> 61)).to_be_bytes();
-        ctx.buffer[bufcnt + padlen..bufcnt + padlen + 8].copy_from_slice(&high);
-        ctx.buffer[bufcnt + padlen + 8..bufcnt + padlen + 16].copy_from_slice(&low);
+        if let Some(dst) = ctx.buffer.get_mut(bufcnt + padlen..bufcnt + padlen + 8) {
+            dst.copy_from_slice(&high);
+        }
+        if let Some(dst) = ctx.buffer.get_mut(bufcnt + padlen + 8..bufcnt + padlen + 16) {
+            dst.copy_from_slice(&low);
+        }
         ctx.bufcnt += (padlen + 16) as u32;
     }
 }
@@ -58,10 +70,18 @@
         return Err(HaceError::InvalidInput);
     }
 
+    // Index-based copy (no `chunks_exact`): the `ChunksExact` iterator stores its
+    // chunk size as a runtime field, so zipping it makes the optimizer emit a
+    // `len / chunk_size` division it cannot prove is non-zero (a `div_by_zero`
+    // panic path). Iterating word-by-word with a const stride and a length-proven
+    // `&mut [u8; 4]` keeps the copy panic-free.
     for (i, word) in iv_words.iter().enumerate() {
-        let bytes = word.to_ne_bytes();
         let off = i * 4;
-        digest[off..off + 4].copy_from_slice(&bytes);
+        if let Some(dst) = digest.get_mut(off..off + 4) {
+            if let Ok(dst) = <&mut [u8; 4]>::try_from(dst) {
+                *dst = word.to_ne_bytes();
+            }
+        }
     }
 
     Ok(())
diff --git a/target/ast10x0/peripherals/hace/hmac.rs b/target/ast10x0/peripherals/hace/hmac.rs
index 9cfbcf6..d82f6de 100644
--- a/target/ast10x0/peripherals/hace/hmac.rs
+++ b/target/ast10x0/peripherals/hace/hmac.rs
@@ -92,7 +92,9 @@
             return Err(HaceError::InvalidInput);
         }
         let mut bytes = [0u8; HMAC_KEY_CAP];
-        bytes[..key.len()].copy_from_slice(key);
+        if let Some(dst) = bytes.get_mut(..key.len()) {
+            dst.copy_from_slice(key);
+        }
         Ok(Self {
             bytes,
             len: key.len(),
@@ -101,7 +103,7 @@
 
     #[inline]
     fn as_slice(&self) -> &[u8] {
-        &self.bytes[..self.len]
+        self.bytes.get(..self.len).unwrap_or(&[])
     }
 }
 
@@ -179,24 +181,28 @@
                     // `ctx.buffer[..key_len].copy_from_slice(key_bytes)` first.
                     let key_nc: &[u8] = unsafe {
                         let buf = &mut *HMAC_KEY_NC.0.get();
-                        buf[..k.len()].copy_from_slice(k);
-                        &buf[..k.len()]
+                        if let Some(dst) = buf.get_mut(..k.len()) {
+                            dst.copy_from_slice(k);
+                        }
+                        buf.get(..k.len()).unwrap_or(&[])
                     };
                     let kh = one_shot!($inner, $algo, pb, key_nc);
                     let hb = kh.as_bytes();
-                    k0[..hb.len()].copy_from_slice(hb);
+                    if let Some(dst) = k0.get_mut(..hb.len()) {
+                        dst.copy_from_slice(hb);
+                    }
                 } else {
-                    k0[..k.len()].copy_from_slice(k);
+                    if let Some(dst) = k0.get_mut(..k.len()) {
+                        dst.copy_from_slice(k);
+                    }
                 }
 
                 let mut ipad = [0u8; 128];
                 let mut opad = [0u8; 128];
                 ipad[..$b].copy_from_slice(&k0[..$b]);
                 opad[..$b].copy_from_slice(&k0[..$b]);
-                for i in 0..$b {
-                    ipad[i] ^= 0x36;
-                    opad[i] ^= 0x5c;
-                }
+                ipad[..$b].iter_mut().for_each(|b| *b ^= 0x36);
+                opad[..$b].iter_mut().for_each(|b| *b ^= 0x5c);
 
                 Ok(HaceHmacCtx {
                     ipad,
@@ -221,7 +227,17 @@
                 if end > HMAC_MSG_CAP {
                     return Err(HaceError::InvalidInput);
                 }
-                self.msg[self.msg_len..end].copy_from_slice(input);
+                let dst = self
+                    .msg
+                    .get_mut(self.msg_len..end)
+                    .ok_or(HaceError::InvalidInput)?;
+                // Element-wise copy: `dst` is `end - self.msg_len == input.len()`
+                // long, but the optimizer cannot prove that through the range
+                // slice, so `copy_from_slice` would retain its length-mismatch
+                // panic branch. The zip-copy is provably panic-free.
+                for (d, s) in dst.iter_mut().zip(input.iter()) {
+                    *d = *s;
+                }
                 self.msg_len = end;
                 Ok(())
             }
@@ -243,8 +259,8 @@
                     // SAFETY: same single-threaded exclusivity contract.
                     let mut dd = unsafe { HaceDigest::<$inner>::from_device(&mut dev) };
                     let mut op = dd.init($algo)?;
-                    op.update(&self.ipad[..b])?;
-                    op.update(&self.msg[..self.msg_len])?;
+                    op.update(self.ipad.get(..b).unwrap_or(&[]))?;
+                    op.update(self.msg.get(..self.msg_len).unwrap_or(&[]))?;
                     op.finalize()?
                 };
                 let inner_bytes = inner.as_bytes();
@@ -256,7 +272,7 @@
                 // SAFETY: same contract.
                 let mut dd = unsafe { HaceDigest::<$inner>::from_device(&mut dev) };
                 let mut op = dd.init($algo)?;
-                op.update(&self.opad[..b])?;
+                op.update(self.opad.get(..b).unwrap_or(&[]))?;
                 op.update(inner_bytes)?;
                 op.finalize()
             }