[aes] Use sparse encodings for FSMs, trigger alert for invalid states

This commit changes all FSMs inside AES to use sparse state encodings with
a minimum Hamming distance of 3 between states. If any FSM enters an
invalid state, the AES unit locks and an alert is triggered. The cipher
core is prevented from finishing the current operation or starting a new
operation. Writes to the control register are ignored. The AES unit needs
to be reset.

This commit also renames the alert signals. Update errors in the shadow
control register are collected in `recoverable`. More severe errors such as
storage errors in the shadow control register, entering invalid FSM
states trigger a `fatal` alert.

Signed-off-by: Pirmin Vogel <vogelpi@lowrisc.org>
diff --git a/hw/ip/aes/data/aes.hjson b/hw/ip/aes/data/aes.hjson
index 608d8ba..5cf6156 100644
--- a/hw/ip/aes/data/aes.hjson
+++ b/hw/ip/aes/data/aes.hjson
@@ -133,17 +133,24 @@
     }
   ],
   alert_list: [
-    { name: "ctrl_err_update",
+    //{ name: "informative",
+    //  desc: '''
+    //    The informative alert can currently not be triggered.
+    //    The AES unit recovers from such a condition automatically.
+    //    No further action needs to be taken but this should be monitored by the system.
+    //  '''
+    //}
+    { name: "recoverable",
       desc: '''
-        This minor alert is triggered upon detecting an update error in the Control Register.
-        The AES unit recovers from such a condition automatically.
-        No further action needs to be taken but this should be monitored by the system.
+        The recoverable alert is triggered upon detecting an update error in the Control Register.
+        The content of the Control Register is not modified (See Control Register).
+        The AES unit can be recovered from such a condition by restarting the AES operation, i.e., by successfully updating the Control Register.
+        This should be monitored by the system.
       '''
     }
-    { name: "ctrl_err_storage",
+    { name: "fatal",
       desc: '''
-        This major alert is triggered upon detecting a storage error in the Control Register.
-        It is fatal.
+        The fatal alert is triggered i) upon detecting a storage error in the Control Register, or ii) if any internal FSM enters an invalid state.
         The AES unit cannot recover from such an error and needs to be reset.
       '''
     }
@@ -278,7 +285,8 @@
       Control Register. Can only be updated when the AES unit is idle. If the
       AES unit is non-idle, writes to this register are ignored.
       This register is shadowed, meaning two subsequent write operations are required to change its content.
-      If the two write operations try to set a different value, a ctrl_err alert is triggered.
+      If the two write operations try to set a different value, a recoverable alert is triggered (See Status Register).
+      A read operation clears the internal phase tracking: The next write operation is always considered a first write operation of an update sequence. 
       Any write operation to this register will clear the status tracking required for automatic mode (See MANUAL_OPERATION field).
       A write to the Control Register is considered the start of a new message.
       Hence, software needs to provide new key, IV and input data afterwards.
@@ -519,11 +527,21 @@
         tags: ["excl:CsrNonInitTests:CsrExclCheck"]
       }
       { bits: "4",
-        name: "CTRL_ERR_STORAGE",
+        name: "ALERT_RECOVERABLE",
         resval: "0",
         desc:  '''
-          No storage error detected in the Control Register (0).
-          A storage error has been detected in the Control Register and the AES unit needs to be reset (1).
+          No recoverable alert condition has occurred (0).
+          A recoverable alert condition has occurred and AES operation needs to be restarted by successfully updating the Control Register (1).
+          Examples for recoverable alert conditions include update errors in the Control Register.
+        '''
+      }
+      { bits: "5",
+        name: "ALERT_FATAL",
+        resval: "0",
+        desc:  '''
+          No fatal alert condition has occurred (0).
+          A fatal alert condition has occurred and the AES unit needs to be reset (1).
+          Examples for fatal alert conditions include i) storage errors in the Control Register, and ii) if any internal FSM enters an invalid state.
         '''
       }
     ]
diff --git a/hw/ip/aes/doc/_index.md b/hw/ip/aes/doc/_index.md
index 05575bf..aaa4348 100644
--- a/hw/ip/aes/doc/_index.md
+++ b/hw/ip/aes/doc/_index.md
@@ -323,7 +323,7 @@
 If the AES unit is not idle, write operations to {{< regref "CTRL" >}}, the Initial Key registers {{< regref "KEY_SHARE0_0" >}} - {{< regref "KEY_SHARE1_7" >}} and initialization vector (IV) registers {{< regref "IV_0" >}} - {{< regref "IV_3" >}} are ignored.
 
 To initialize the AES unit, software must first provide the configuration to the {{< regref "CTRL_SHADOWED" >}} register.
-Then software must write the initial key to the Initial Key registers {< regref "KEY_SHARE0_0" >}} - {{< regref "KEY_SHARE1_7" >}}.
+Then software must write the initial key to the Initial Key registers {{< regref "KEY_SHARE0_0" >}} - {{< regref "KEY_SHARE1_7" >}}.
 The key is provided in two shares:
 The first share is written to {{< regref "KEY_SHARE0_0" >}} - {{< regref "KEY_SHARE0_7" >}} and the second share is written to {{< regref "KEY_SHARE1_0" >}} - {{< regref "KEY_SHARE1_7" >}}.
 The actual initial key used for encryption corresponds to the value obtained by XORing {{< regref "KEY_SHARE0_0" >}} - {{< regref "KEY_SHARE0_7" >}} with {{< regref "KEY_SHARE1_0" >}} - {{< regref "KEY_SHARE1_7" >}}.
diff --git a/hw/ip/aes/dv/env/aes_env_pkg.sv b/hw/ip/aes/dv/env/aes_env_pkg.sv
index af11bc8..04894e0 100644
--- a/hw/ip/aes/dv/env/aes_env_pkg.sv
+++ b/hw/ip/aes/dv/env/aes_env_pkg.sv
@@ -22,7 +22,7 @@
   `include "dv_macros.svh"
 
   // parameters
-  parameter string LIST_OF_ALERTS[] = {"ctrl_err_update", "ctrl_err_storage"};
+  parameter string LIST_OF_ALERTS[] = {"recoverable", "fatal"};
   parameter uint NUM_ALERTS = 2;
 
   typedef enum int { AES_CFG=0, AES_DATA=1, AES_ERR_INJ=2 } aes_item_type_e;
diff --git a/hw/ip/aes/dv/env/seq_lib/aes_common_vseq.sv b/hw/ip/aes/dv/env/seq_lib/aes_common_vseq.sv
index 449bf09..766717f 100644
--- a/hw/ip/aes/dv/env/seq_lib/aes_common_vseq.sv
+++ b/hw/ip/aes/dv/env/seq_lib/aes_common_vseq.sv
@@ -18,7 +18,7 @@
   endtask
 
   virtual function void shadow_reg_storage_err_post_write();
-    void'(ral.status.ctrl_err_storage.predict(1));
+    void'(ral.status.alert_fatal.predict(1));
   endfunction
 
   // for AES ctrl_shadowed register, the write transaction is valid only if the status is Idle
diff --git a/hw/ip/aes/rtl/aes.sv b/hw/ip/aes/rtl/aes.sv
index b1d6d85..ca5d808 100644
--- a/hw/ip/aes/rtl/aes.sv
+++ b/hw/ip/aes/rtl/aes.sv
@@ -94,8 +94,8 @@
     .entropy_masking_ack_i  ( 1'b1                           ),
     .entropy_masking_i      ( RndCnstMaskingLfsrSeedDefault  ),
 
-    .ctrl_err_update_o      ( alert[0]                       ),
-    .ctrl_err_storage_o     ( alert[1]                       ),
+    .alert_recoverable_o    ( alert[0]                       ),
+    .alert_fatal_o          ( alert[1]                       ),
 
     .reg2hw                 ( reg2hw                         ),
     .hw2reg                 ( hw2reg                         )
@@ -105,10 +105,10 @@
 
   logic [NumAlerts-1:0] alert_test;
   assign alert_test = {
-    reg2hw.alert_test.ctrl_err_storage.q &
-    reg2hw.alert_test.ctrl_err_storage.qe,
-    reg2hw.alert_test.ctrl_err_update.q &
-    reg2hw.alert_test.ctrl_err_update.qe
+    reg2hw.alert_test.fatal.q &
+    reg2hw.alert_test.fatal.qe,
+    reg2hw.alert_test.recoverable.q &
+    reg2hw.alert_test.recoverable.qe
   };
 
   for (genvar i = 0; i < NumAlerts; i++) begin : gen_alert_tx
diff --git a/hw/ip/aes/rtl/aes_cipher_control.sv b/hw/ip/aes/rtl/aes_cipher_control.sv
index 90b0c75..bc37150 100644
--- a/hw/ip/aes/rtl/aes_cipher_control.sv
+++ b/hw/ip/aes/rtl/aes_cipher_control.sv
@@ -35,6 +35,7 @@
   output logic                    key_clear_o,
   input  logic                    data_out_clear_i,
   output logic                    data_out_clear_o,
+  output logic                    alert_o,
 
   // Control signals for masking PRNG
   output logic                    prng_update_o,
@@ -67,8 +68,31 @@
   import aes_pkg::*;
 
   // Types
-  typedef enum logic [2:0] {
-    IDLE, INIT, ROUND, FINISH, CLEAR_S, CLEAR_KD
+  // $ ./sparse-fsm-encode.py -d 3 -m 7 -n 6 \
+  //      -s 31468618 --language=sv
+  //
+  // Hamming distance histogram:
+  //
+  //  0: --
+  //  1: --
+  //  2: --
+  //  3: |||||||||||||||||||| (57.14%)
+  //  4: ||||||||||||||| (42.86%)
+  //  5: --
+  //  6: --
+  //
+  // Minimum Hamming distance: 3
+  // Maximum Hamming distance: 4
+  //
+  localparam int StateWidth = 6;
+  typedef enum logic [StateWidth-1:0] {
+    IDLE     = 6'b111100,
+    INIT     = 6'b101001,
+    ROUND    = 6'b010000,
+    FINISH   = 6'b100010,
+    CLEAR_S  = 6'b011011,
+    CLEAR_KD = 6'b110111,
+    ERROR    = 6'b001110
   } aes_cipher_ctrl_e;
 
   aes_cipher_ctrl_e aes_cipher_ctrl_ns, aes_cipher_ctrl_cs;
@@ -126,6 +150,9 @@
     data_out_clear_d     = data_out_clear_q;
     prng_reseed_done_d   = prng_reseed_done_q | prng_reseed_ack_i;
 
+    // Alert
+    alert_o              = 1'b0;
+
     unique case (aes_cipher_ctrl_cs)
 
       IDLE: begin
@@ -337,13 +364,35 @@
         end
       end
 
-      default: aes_cipher_ctrl_ns = IDLE;
+      ERROR: begin
+        // Terminal error state
+        alert_o = 1'b1;
+      end
+
+      // We should never get here. If we do (e.g. via a malicious glitch), error out immediately.
+      default: begin
+        alert_o            = 1'b1;
+        aes_cipher_ctrl_ns = ERROR;
+      end
     endcase
   end
 
+  // This primitive is used to place a size-only constraint on the
+  // flops in order to prevent FSM state encoding optimizations.
+  logic [StateWidth-1:0] aes_cipher_ctrl_cs_raw;
+  assign aes_cipher_ctrl_cs = aes_cipher_ctrl_e'(aes_cipher_ctrl_cs_raw);
+  prim_flop #(
+    .Width(StateWidth),
+    .ResetValue(StateWidth'(IDLE))
+  ) u_state_regs (
+    .clk_i,
+    .rst_ni,
+    .d_i ( aes_cipher_ctrl_ns     ),
+    .q_o ( aes_cipher_ctrl_cs_raw )
+  );
+
   always_ff @(posedge clk_i or negedge rst_ni) begin : reg_fsm
     if (!rst_ni) begin
-      aes_cipher_ctrl_cs <= IDLE;
       round_q            <= '0;
       num_rounds_q       <= '0;
       crypt_q            <= 1'b0;
@@ -352,7 +401,6 @@
       data_out_clear_q   <= 1'b0;
       prng_reseed_done_q <= 1'b0;
     end else begin
-      aes_cipher_ctrl_cs <= aes_cipher_ctrl_ns;
       round_q            <= round_d;
       num_rounds_q       <= num_rounds_d;
       crypt_q            <= crypt_d;
@@ -387,7 +435,7 @@
       AES_192,
       AES_256
       })
-  `ASSERT(AesControlStateValid, aes_cipher_ctrl_cs inside {
+  `ASSERT(AesControlStateValid, !alert_o |-> aes_cipher_ctrl_cs inside {
       IDLE,
       INIT,
       ROUND,
diff --git a/hw/ip/aes/rtl/aes_cipher_core.sv b/hw/ip/aes/rtl/aes_cipher_core.sv
index b36d549..c252614 100644
--- a/hw/ip/aes/rtl/aes_cipher_core.sv
+++ b/hw/ip/aes/rtl/aes_cipher_core.sv
@@ -127,6 +127,7 @@
   output logic                        key_clear_o,
   input  logic                        data_out_clear_i, // Re-use the cipher core muxes.
   output logic                        data_out_clear_o,
+  output logic                        alert_o,
 
   // Pseudo-random data for register clearing
   input  logic [WidthPRDClearing-1:0] prd_clearing_i,
@@ -456,6 +457,7 @@
     .key_clear_o          ( key_clear_o          ),
     .data_out_clear_i     ( data_out_clear_i     ),
     .data_out_clear_o     ( data_out_clear_o     ),
+    .alert_o              ( alert_o              ),
 
     .prng_update_o        ( prd_masking_upd      ),
     .prng_reseed_req_o    ( prd_masking_rsd_req  ),
diff --git a/hw/ip/aes/rtl/aes_control.sv b/hw/ip/aes/rtl/aes_control.sv
index a8d256f..799f617 100644
--- a/hw/ip/aes/rtl/aes_control.sv
+++ b/hw/ip/aes/rtl/aes_control.sv
@@ -29,6 +29,8 @@
   input  logic                    data_in_clear_i,
   input  logic                    data_out_clear_i,
   input  logic                    prng_reseed_i,
+  input  logic                    alert_fatal_i,
+  output logic                    alert_o,
 
   // I/O register read/write enables
   input  logic [7:0]              key_init_qe_i [2],
@@ -108,8 +110,30 @@
   import aes_pkg::*;
 
   // Types
-  typedef enum logic [2:0] {
-    IDLE, LOAD, UPDATE_PRNG, FINISH, CLEAR
+  // $ ./sparse-fsm-encode.py -d 3 -m 6 -n 6 \
+  //      -s 31468618 --language=sv
+  //
+  // Hamming distance histogram:
+  //
+  //  0: --
+  //  1: --
+  //  2: --
+  //  3: |||||||||||||||||||| (53.33%)
+  //  4: ||||||||||||||| (40.00%)
+  //  5: || (6.67%)
+  //  6: --
+  //
+  // Minimum Hamming distance: 3
+  // Maximum Hamming distance: 5
+  //
+  localparam int StateWidth = 6;
+  typedef enum logic [StateWidth-1:0] {
+    IDLE        = 6'b011101,
+    LOAD        = 6'b110000,
+    UPDATE_PRNG = 6'b001000,
+    FINISH      = 6'b000011,
+    CLEAR       = 6'b111110,
+    ERROR       = 6'b100101
   } aes_ctrl_e;
 
   aes_ctrl_e aes_ctrl_ns, aes_ctrl_cs;
@@ -137,6 +161,7 @@
 
   logic       start_trigger;
   logic       cfg_valid;
+  logic       no_alert;
   logic       start, finish;
   logic       cipher_crypt;
   logic       cipher_out_done;
@@ -175,13 +200,14 @@
                   iv_qe_i[1], iv_qe_i[1], iv_qe_i[0], iv_qe_i[0]};
 
   // The cipher core is only ever allowed to start or finish if the control register holds a valid
-  // configuration.
+  // configuration and if no fatal alert condition occured.
   assign cfg_valid = ~((mode_i == AES_NONE) | ctrl_err_storage_i);
+  assign no_alert  = ~alert_fatal_i;
 
   // If set to start manually, we just wait for the trigger. Otherwise, we start once we have valid
   // data available. If the IV (and counter) is needed, we only start if also the IV (and counter)
   // is ready.
-  assign start = cfg_valid &
+  assign start = cfg_valid & no_alert &
       ( manual_operation_i ? start_trigger                                           :
        (mode_i == AES_ECB) ? (key_init_ready & data_in_new)                          :
        (mode_i == AES_CBC) ? (key_init_ready & data_in_new & iv_ready)               :
@@ -192,7 +218,8 @@
   // If not set to overwrite data, we wait for any previous output data to be read. data_out_read
   // synchronously clears output_valid_q, unless new output data is written in the exact same
   // clock cycle.
-  assign finish = cfg_valid & (manual_operation_i ? 1'b1 : (~output_valid_q | data_out_read));
+  assign finish = cfg_valid & no_alert &
+      (manual_operation_i ? 1'b1 : (~output_valid_q | data_out_read));
 
   // Helper signals for FSM
   assign cipher_crypt  = cipher_crypt_o | cipher_crypt_i;
@@ -238,6 +265,9 @@
     // Control register
     ctrl_we_o = 1'b0;
 
+    // Alert
+    alert_o = 1'b0;
+
     // Pseudo-random number generator control
     prng_data_req_o   = 1'b0;
     prng_reseed_req_o = 1'b0;
@@ -504,17 +534,32 @@
         end
       end
 
-      default: aes_ctrl_ns = IDLE;
+      ERROR: begin
+        // Terminal error state
+        alert_o = 1'b1;
+      end
+
+      // We should never get here. If we do (e.g. via a malicious glitch), error out immediately.
+      default: begin
+        alert_o     = 1'b1;
+        aes_ctrl_ns = ERROR;
+      end
     endcase
   end
 
-  always_ff @(posedge clk_i or negedge rst_ni) begin : reg_fsm
-    if (!rst_ni) begin
-      aes_ctrl_cs <= IDLE;
-    end else begin
-      aes_ctrl_cs <= aes_ctrl_ns;
-    end
-  end
+  // This primitive is used to place a size-only constraint on the
+  // flops in order to prevent FSM state encoding optimizations.
+  logic [StateWidth-1:0] aes_ctrl_cs_raw;
+  assign aes_ctrl_cs = aes_ctrl_e'(aes_ctrl_cs_raw);
+  prim_flop #(
+    .Width(StateWidth),
+    .ResetValue(StateWidth'(IDLE))
+  ) u_state_regs (
+    .clk_i,
+    .rst_ni,
+    .d_i ( aes_ctrl_ns     ),
+    .q_o ( aes_ctrl_cs_raw )
+  );
 
   // We only use clean initial keys. Either software/counter has updated
   // - all initial key registers, or
@@ -634,6 +679,12 @@
       })
   `ASSERT_KNOWN(AesOpKnown, op_i)
   `ASSERT_KNOWN(AesCiphOpKnown, cipher_op_i)
-  `ASSERT_KNOWN(AesControlStateValid, aes_ctrl_cs)
+  `ASSERT(AesControlStateValid, !alert_o |-> aes_ctrl_cs inside {
+      IDLE,
+      LOAD,
+      UPDATE_PRNG,
+      FINISH,
+      CLEAR
+      })
 
 endmodule
diff --git a/hw/ip/aes/rtl/aes_core.sv b/hw/ip/aes/rtl/aes_core.sv
index 1ad52f8..6c6928b 100644
--- a/hw/ip/aes/rtl/aes_core.sv
+++ b/hw/ip/aes/rtl/aes_core.sv
@@ -35,8 +35,8 @@
   input  logic  [WidthPRDMasking-1:0] entropy_masking_i,
 
   // Alerts
-  output logic                        ctrl_err_update_o,
-  output logic                        ctrl_err_storage_o,
+  output logic                        alert_recoverable_o,
+  output logic                        alert_fatal_o,
 
   // Bus Interface
   input  aes_reg2hw_t                 reg2hw,
@@ -56,6 +56,16 @@
   logic                        manual_operation_q;
   logic                        force_zero_masks_q;
   ctrl_reg_t                   ctrl_d, ctrl_q;
+  logic                        ctrl_err_update_we;
+  logic                        ctrl_err_update;
+  logic                        ctrl_err_update_d;
+  logic                        ctrl_err_update_q;
+  logic                        ctrl_err_storage_we;
+  logic                        ctrl_err_storage;
+  logic                        ctrl_err_storage_d;
+  logic                        ctrl_err_storage_q;
+  logic                        ctrl_alert;
+
 
   logic        [3:0][3:0][7:0] state_in;
   si_sel_e                     state_in_sel;
@@ -86,6 +96,7 @@
   logic            [7:0]       ctr_we;
   logic                        ctr_incr;
   logic                        ctr_ready;
+  logic                        ctr_alert;
 
   logic            [3:0][31:0] data_in_prev_d;
   logic            [3:0][31:0] data_in_prev_q;
@@ -116,6 +127,7 @@
   logic                        cipher_key_clear_busy;
   logic                        cipher_data_out_clear;
   logic                        cipher_data_out_clear_busy;
+  logic                        cipher_alert;
 
   // Pseudo-random data for clearing purposes
   logic [WidthPRDClearing-1:0] prd_clearing;
@@ -268,6 +280,7 @@
 
     .incr_i   ( ctr_incr  ),
     .ready_o  ( ctr_ready ),
+    .alert_o  ( ctr_alert ),
 
     .ctr_i    ( iv_q      ),
     .ctr_o    ( ctr       ),
@@ -345,7 +358,7 @@
     .out_valid_o        ( cipher_out_valid           ),
     .out_ready_i        ( cipher_out_ready           ),
 
-    .cfg_valid_i        ( ~ctrl_err_storage_o        ),
+    .cfg_valid_i        ( ~ctrl_err_storage          ), // Used for gating assertions only.
     .op_i               ( cipher_op                  ),
     .key_len_i          ( key_len_q                  ),
     .crypt_i            ( cipher_crypt               ),
@@ -356,6 +369,7 @@
     .key_clear_o        ( cipher_key_clear_busy      ),
     .data_out_clear_i   ( cipher_data_out_clear      ),
     .data_out_clear_o   ( cipher_data_out_clear_busy ),
+    .alert_o            ( cipher_alert               ),
 
     .prd_clearing_i     ( prd_clearing               ),
 
@@ -453,14 +467,10 @@
     .qe          (                    ),
     .q           ( ctrl_q             ),
     .qs          (                    ),
-    .err_update  ( ctrl_err_update_o  ),
-    .err_storage ( ctrl_err_storage_o )
+    .err_update  ( ctrl_err_update_d  ),
+    .err_storage ( ctrl_err_storage_d )
   );
 
-  // Make sure the storage error is observable via status register.
-  assign hw2reg.status.ctrl_err_storage.d  = ctrl_err_storage_o;
-  assign hw2reg.status.ctrl_err_storage.de = ctrl_err_storage_o;
-
   // Get shorter references.
   assign aes_op_q           = ctrl_q.operation;
   assign aes_mode_q         = ctrl_q.mode;
@@ -468,10 +478,6 @@
   assign manual_operation_q = ctrl_q.manual_operation;
   assign force_zero_masks_q = ctrl_q.force_zero_masks;
 
-  // Unused alert signals
-  logic unused_alert_signals;
-  assign unused_alert_signals = ^reg2hw.alert_test;
-
   /////////////
   // Control //
   /////////////
@@ -485,7 +491,7 @@
 
     .ctrl_qe_i               ( ctrl_qe                          ),
     .ctrl_we_o               ( ctrl_we                          ),
-    .ctrl_err_storage_i      ( ctrl_err_storage_o               ),
+    .ctrl_err_storage_i      ( ctrl_err_storage                 ),
     .op_i                    ( aes_op_q                         ),
     .mode_i                  ( aes_mode_q                       ),
     .cipher_op_i             ( cipher_op                        ),
@@ -496,6 +502,8 @@
     .data_in_clear_i         ( reg2hw.trigger.data_in_clear.q   ),
     .data_out_clear_i        ( reg2hw.trigger.data_out_clear.q  ),
     .prng_reseed_i           ( reg2hw.trigger.prng_reseed.q     ),
+    .alert_fatal_i           ( alert_fatal_o                    ),
+    .alert_o                 ( ctrl_alert                       ),
 
     .key_init_qe_i           ( key_init_qe                      ),
     .iv_qe_i                 ( iv_qe                            ),
@@ -608,6 +616,48 @@
   assign hw2reg.ctrl_shadowed.manual_operation.d = manual_operation_q;
   assign hw2reg.ctrl_shadowed.force_zero_masks.d = force_zero_masks_q;
 
+  ////////////
+  // Alerts //
+  ////////////
+
+  // Recoverable alert conditions remain asserted until AES operation is restarted by rewriting the
+  // Control Register.
+  assign ctrl_err_update_we = ctrl_err_update_d | ctrl_we;
+  always_ff @(posedge clk_i or negedge rst_ni) begin : ctrl_err_update_reg
+    if (!rst_ni) begin
+      ctrl_err_update_q <= 1'b0;
+    end else if (ctrl_err_update_we) begin
+      ctrl_err_update_q <= ctrl_err_update_d;
+    end
+  end
+  assign ctrl_err_update = ctrl_err_update_d | ctrl_err_update_q;
+
+  // Fatal alert conditions need to remain asserted until reset.
+  assign ctrl_err_storage_we = ctrl_err_storage_d;
+  always_ff @(posedge clk_i or negedge rst_ni) begin : ctrl_err_storage_reg
+    if (!rst_ni) begin
+      ctrl_err_storage_q <= 1'b0;
+    end else if (ctrl_err_storage_we) begin
+      ctrl_err_storage_q <= 1'b1;
+    end
+  end
+  assign ctrl_err_storage = ctrl_err_storage_d | ctrl_err_storage_q;
+
+  // Collect alert signals.
+  assign alert_recoverable_o = ctrl_err_update;
+  assign alert_fatal_o       = ctrl_err_storage | ctr_alert | cipher_alert | ctrl_alert;
+
+  // Make alerts observable via status register.
+  assign hw2reg.status.alert_recoverable.d  = alert_recoverable_o;
+  assign hw2reg.status.alert_recoverable.de = ctrl_err_update_we;
+
+  assign hw2reg.status.alert_fatal.d  = alert_fatal_o;
+  assign hw2reg.status.alert_fatal.de = alert_fatal_o;
+
+  // Unused alert signals
+  logic unused_alert_signals;
+  assign unused_alert_signals = ^reg2hw.alert_test;
+
   ////////////////
   // Assertions //
   ////////////////
@@ -623,7 +673,7 @@
       IV_CLEAR
       })
   `ASSERT_KNOWN(AesDataInPrevSelKnown, data_in_prev_sel)
-  `ASSERT(AesModeValid, !ctrl_err_storage_o |-> aes_mode_q inside {
+  `ASSERT(AesModeValid, !ctrl_err_storage |-> aes_mode_q inside {
       AES_ECB,
       AES_CBC,
       AES_CFB,
diff --git a/hw/ip/aes/rtl/aes_ctr.sv b/hw/ip/aes/rtl/aes_ctr.sv
index 3554001..ee7e839 100644
--- a/hw/ip/aes/rtl/aes_ctr.sv
+++ b/hw/ip/aes/rtl/aes_ctr.sv
@@ -14,6 +14,7 @@
 
   input  logic             incr_i,
   output logic             ready_o,
+  output logic             alert_o,
 
   input  logic [7:0][15:0] ctr_i, // 8 times 2 bytes
   output logic [7:0][15:0] ctr_o, // 8 times 2 bytes
@@ -39,8 +40,26 @@
   endfunction
 
   // Types
-  typedef enum logic {
-    IDLE, INCR
+  // $ ./sparse-fsm-encode.py -d 3 -m 3 -n 5 \
+  //      -s 31468618 --language=sv
+  //
+  // Hamming distance histogram:
+  //
+  //  0: --
+  //  1: --
+  //  2: --
+  //  3: |||||||||||||||||||| (66.67%)
+  //  4: |||||||||| (33.33%)
+  //  5: --
+  //
+  // Minimum Hamming distance: 3
+  // Maximum Hamming distance: 4
+  //
+  localparam int StateWidth = 5;
+  typedef enum logic [StateWidth-1:0] {
+    IDLE  = 5'b01110,
+    INCR  = 5'b11000,
+    ERROR = 5'b00001
   } aes_ctr_e;
 
   // Signals
@@ -83,6 +102,7 @@
     // Outputs
     ready_o         = 1'b0;
     ctr_we          = 1'b0;
+    alert_o         = 1'b0;
 
     // FSM
     aes_ctr_ns      = aes_ctr_cs;
@@ -111,23 +131,45 @@
         end
       end
 
-      default: aes_ctr_ns = IDLE;
+      ERROR: begin
+        // Terminal error state
+        alert_o = 1'b1;
+      end
+
+      // We should never get here. If we do (e.g. via a malicious
+      // glitch), error out immediately.
+      default: begin
+        alert_o    = 1'b1;
+        aes_ctr_ns = ERROR;
+      end
     endcase
   end
 
   // Registers
   always_ff @(posedge clk_i or negedge rst_ni) begin
     if (!rst_ni) begin
-      aes_ctr_cs      <= IDLE;
       ctr_slice_idx_q <= '0;
       ctr_carry_q     <= '0;
     end else begin
-      aes_ctr_cs      <= aes_ctr_ns;
       ctr_slice_idx_q <= ctr_slice_idx_d;
       ctr_carry_q     <= ctr_carry_d;
     end
   end
 
+  // This primitive is used to place a size-only constraint on the
+  // flops in order to prevent FSM state encoding optimizations.
+  logic [StateWidth-1:0] aes_ctr_cs_raw;
+  assign aes_ctr_cs = aes_ctr_e'(aes_ctr_cs_raw);
+  prim_flop #(
+    .Width(StateWidth),
+    .ResetValue(StateWidth'(IDLE))
+  ) u_state_regs (
+    .clk_i,
+    .rst_ni,
+    .d_i ( aes_ctr_ns     ),
+    .q_o ( aes_ctr_cs_raw )
+  );
+
   /////////////
   // Outputs //
   /////////////
@@ -151,6 +193,9 @@
   ////////////////
   // Assertions //
   ////////////////
-  `ASSERT_KNOWN(AesCtrStateKnown, aes_ctr_cs)
+  `ASSERT(AesCtrStateValid, !alert_o |-> aes_ctr_cs inside {
+      IDLE,
+      INCR
+      })
 
 endmodule
diff --git a/hw/ip/aes/rtl/aes_reg_pkg.sv b/hw/ip/aes/rtl/aes_reg_pkg.sv
index d4ef540..35944e1 100644
--- a/hw/ip/aes/rtl/aes_reg_pkg.sv
+++ b/hw/ip/aes/rtl/aes_reg_pkg.sv
@@ -19,11 +19,11 @@
     struct packed {
       logic        q;
       logic        qe;
-    } ctrl_err_update;
+    } recoverable;
     struct packed {
       logic        q;
       logic        qe;
-    } ctrl_err_storage;
+    } fatal;
   } aes_reg2hw_alert_test_reg_t;
 
   typedef struct packed {
@@ -187,7 +187,11 @@
     struct packed {
       logic        d;
       logic        de;
-    } ctrl_err_storage;
+    } alert_recoverable;
+    struct packed {
+      logic        d;
+      logic        de;
+    } alert_fatal;
   } aes_hw2reg_status_reg_t;
 
 
@@ -209,14 +213,14 @@
   // Internal design logic to register //
   ///////////////////////////////////////
   typedef struct packed {
-    aes_hw2reg_key_share0_mreg_t [7:0] key_share0; // [933:678]
-    aes_hw2reg_key_share1_mreg_t [7:0] key_share1; // [677:422]
-    aes_hw2reg_iv_mreg_t [3:0] iv; // [421:294]
-    aes_hw2reg_data_in_mreg_t [3:0] data_in; // [293:162]
-    aes_hw2reg_data_out_mreg_t [3:0] data_out; // [161:34]
-    aes_hw2reg_ctrl_shadowed_reg_t ctrl_shadowed; // [33:22]
-    aes_hw2reg_trigger_reg_t trigger; // [21:10]
-    aes_hw2reg_status_reg_t status; // [9:0]
+    aes_hw2reg_key_share0_mreg_t [7:0] key_share0; // [935:680]
+    aes_hw2reg_key_share1_mreg_t [7:0] key_share1; // [679:424]
+    aes_hw2reg_iv_mreg_t [3:0] iv; // [423:296]
+    aes_hw2reg_data_in_mreg_t [3:0] data_in; // [295:164]
+    aes_hw2reg_data_out_mreg_t [3:0] data_out; // [163:36]
+    aes_hw2reg_ctrl_shadowed_reg_t ctrl_shadowed; // [35:24]
+    aes_hw2reg_trigger_reg_t trigger; // [23:12]
+    aes_hw2reg_status_reg_t status; // [11:0]
   } aes_hw2reg_t;
 
   // Register Address
diff --git a/hw/ip/aes/rtl/aes_reg_top.sv b/hw/ip/aes/rtl/aes_reg_top.sv
index eb41b73..7b7d6e5 100644
--- a/hw/ip/aes/rtl/aes_reg_top.sv
+++ b/hw/ip/aes/rtl/aes_reg_top.sv
@@ -71,10 +71,10 @@
   // Define SW related signals
   // Format: <reg>_<field>_{wd|we|qs}
   //        or <reg>_{wd|we|qs} if field == 1 or 0
-  logic alert_test_ctrl_err_update_wd;
-  logic alert_test_ctrl_err_update_we;
-  logic alert_test_ctrl_err_storage_wd;
-  logic alert_test_ctrl_err_storage_we;
+  logic alert_test_recoverable_wd;
+  logic alert_test_recoverable_we;
+  logic alert_test_fatal_wd;
+  logic alert_test_fatal_we;
   logic [31:0] key_share0_0_wd;
   logic key_share0_0_we;
   logic [31:0] key_share0_1_wd;
@@ -167,37 +167,38 @@
   logic status_stall_qs;
   logic status_output_valid_qs;
   logic status_input_ready_qs;
-  logic status_ctrl_err_storage_qs;
+  logic status_alert_recoverable_qs;
+  logic status_alert_fatal_qs;
 
   // Register instances
   // R[alert_test]: V(True)
 
-  //   F[ctrl_err_update]: 0:0
+  //   F[recoverable]: 0:0
   prim_subreg_ext #(
     .DW    (1)
-  ) u_alert_test_ctrl_err_update (
+  ) u_alert_test_recoverable (
     .re     (1'b0),
-    .we     (alert_test_ctrl_err_update_we),
-    .wd     (alert_test_ctrl_err_update_wd),
+    .we     (alert_test_recoverable_we),
+    .wd     (alert_test_recoverable_wd),
     .d      ('0),
     .qre    (),
-    .qe     (reg2hw.alert_test.ctrl_err_update.qe),
-    .q      (reg2hw.alert_test.ctrl_err_update.q ),
+    .qe     (reg2hw.alert_test.recoverable.qe),
+    .q      (reg2hw.alert_test.recoverable.q ),
     .qs     ()
   );
 
 
-  //   F[ctrl_err_storage]: 1:1
+  //   F[fatal]: 1:1
   prim_subreg_ext #(
     .DW    (1)
-  ) u_alert_test_ctrl_err_storage (
+  ) u_alert_test_fatal (
     .re     (1'b0),
-    .we     (alert_test_ctrl_err_storage_we),
-    .wd     (alert_test_ctrl_err_storage_wd),
+    .we     (alert_test_fatal_we),
+    .wd     (alert_test_fatal_wd),
     .d      ('0),
     .qre    (),
-    .qe     (reg2hw.alert_test.ctrl_err_storage.qe),
-    .q      (reg2hw.alert_test.ctrl_err_storage.q ),
+    .qe     (reg2hw.alert_test.fatal.qe),
+    .q      (reg2hw.alert_test.fatal.q ),
     .qs     ()
   );
 
@@ -1031,12 +1032,12 @@
   );
 
 
-  //   F[ctrl_err_storage]: 4:4
+  //   F[alert_recoverable]: 4:4
   prim_subreg #(
     .DW      (1),
     .SWACCESS("RO"),
     .RESVAL  (1'h0)
-  ) u_status_ctrl_err_storage (
+  ) u_status_alert_recoverable (
     .clk_i   (clk_i    ),
     .rst_ni  (rst_ni  ),
 
@@ -1044,15 +1045,40 @@
     .wd     ('0  ),
 
     // from internal hardware
-    .de     (hw2reg.status.ctrl_err_storage.de),
-    .d      (hw2reg.status.ctrl_err_storage.d ),
+    .de     (hw2reg.status.alert_recoverable.de),
+    .d      (hw2reg.status.alert_recoverable.d ),
 
     // to internal hardware
     .qe     (),
     .q      (),
 
     // to register interface (read)
-    .qs     (status_ctrl_err_storage_qs)
+    .qs     (status_alert_recoverable_qs)
+  );
+
+
+  //   F[alert_fatal]: 5:5
+  prim_subreg #(
+    .DW      (1),
+    .SWACCESS("RO"),
+    .RESVAL  (1'h0)
+  ) u_status_alert_fatal (
+    .clk_i   (clk_i    ),
+    .rst_ni  (rst_ni  ),
+
+    .we     (1'b0),
+    .wd     ('0  ),
+
+    // from internal hardware
+    .de     (hw2reg.status.alert_fatal.de),
+    .d      (hw2reg.status.alert_fatal.d ),
+
+    // to internal hardware
+    .qe     (),
+    .q      (),
+
+    // to register interface (read)
+    .qs     (status_alert_fatal_qs)
   );
 
 
@@ -1134,11 +1160,11 @@
     if (addr_hit[31] && reg_we && (AES_PERMIT[31] != (AES_PERMIT[31] & reg_be))) wr_err = 1'b1 ;
   end
 
-  assign alert_test_ctrl_err_update_we = addr_hit[0] & reg_we & ~wr_err;
-  assign alert_test_ctrl_err_update_wd = reg_wdata[0];
+  assign alert_test_recoverable_we = addr_hit[0] & reg_we & ~wr_err;
+  assign alert_test_recoverable_wd = reg_wdata[0];
 
-  assign alert_test_ctrl_err_storage_we = addr_hit[0] & reg_we & ~wr_err;
-  assign alert_test_ctrl_err_storage_wd = reg_wdata[1];
+  assign alert_test_fatal_we = addr_hit[0] & reg_we & ~wr_err;
+  assign alert_test_fatal_wd = reg_wdata[1];
 
   assign key_share0_0_we = addr_hit[1] & reg_we & ~wr_err;
   assign key_share0_0_wd = reg_wdata[31:0];
@@ -1263,6 +1289,7 @@
 
 
 
+
   // Read data return
   always_comb begin
     reg_rdata_next = '0;
@@ -1406,7 +1433,8 @@
         reg_rdata_next[1] = status_stall_qs;
         reg_rdata_next[2] = status_output_valid_qs;
         reg_rdata_next[3] = status_input_ready_qs;
-        reg_rdata_next[4] = status_ctrl_err_storage_qs;
+        reg_rdata_next[4] = status_alert_recoverable_qs;
+        reg_rdata_next[5] = status_alert_fatal_qs;
       end
 
       default: begin
diff --git a/hw/ip/csrng/rtl/csrng_block_encrypt.sv b/hw/ip/csrng/rtl/csrng_block_encrypt.sv
index 54e6185..72c2b3f 100644
--- a/hw/ip/csrng/rtl/csrng_block_encrypt.sv
+++ b/hw/ip/csrng/rtl/csrng_block_encrypt.sv
@@ -105,6 +105,7 @@
     .key_clear_o        (                            ),
     .data_out_clear_i   ( 1'b0                       ), // Disable
     .data_out_clear_o   (                            ),
+    .alert_o            (                            ), // Currently unused.
     .prd_clearing_i     ( '0                         ),
     .force_zero_masks_i ( 1'b0                       ),
     .data_in_mask_o     (                            ),
diff --git a/hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson b/hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson
index 8379f5f..d08c7e8 100644
--- a/hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson
+++ b/hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson
@@ -3822,7 +3822,7 @@
       alert_list:
       [
         {
-          name: ctrl_err_update
+          name: recoverable
           width: 1
           bits: "0"
           bitinfo:
@@ -3835,7 +3835,7 @@
           async: 1
         }
         {
-          name: ctrl_err_storage
+          name: fatal
           width: 1
           bits: "1"
           bitinfo:
@@ -8086,7 +8086,7 @@
   alert:
   [
     {
-      name: aes_ctrl_err_update
+      name: aes_recoverable
       width: 1
       bits: "0"
       bitinfo:
@@ -8100,7 +8100,7 @@
       module_name: aes
     }
     {
-      name: aes_ctrl_err_storage
+      name: aes_fatal
       width: 1
       bits: "1"
       bitinfo:
diff --git a/hw/top_earlgrey/dv/env/autogen/alert_handler_env_pkg__params.sv b/hw/top_earlgrey/dv/env/autogen/alert_handler_env_pkg__params.sv
index 878af25..96dae1c 100644
--- a/hw/top_earlgrey/dv/env/autogen/alert_handler_env_pkg__params.sv
+++ b/hw/top_earlgrey/dv/env/autogen/alert_handler_env_pkg__params.sv
@@ -5,8 +5,8 @@
 // alert_handler_env_pkg__params.sv is auto-generated by `topgen.py` tool
 
 parameter string LIST_OF_ALERTS[] = {
-  "aes_ctrl_err_update",
-  "aes_ctrl_err_storage",
+  "aes_recoverable",
+  "aes_fatal",
   "otbn_fatal",
   "otbn_recoverable",
   "sensor_ctrl_as",
diff --git a/hw/top_earlgrey/rtl/autogen/top_earlgrey.sv b/hw/top_earlgrey/rtl/autogen/top_earlgrey.sv
index 595e5e7..d06ce2d 100644
--- a/hw/top_earlgrey/rtl/autogen/top_earlgrey.sv
+++ b/hw/top_earlgrey/rtl/autogen/top_earlgrey.sv
@@ -1260,8 +1260,8 @@
     .RndCnstMskgChunkLfsrPerm(aes_pkg::RndCnstMskgChunkLfsrPermDefault)
   ) u_aes (
 
-      // [12]: ctrl_err_update
-      // [13]: ctrl_err_storage
+      // [12]: recoverable
+      // [13]: fatal
       .alert_tx_o  ( alert_tx[13:12] ),
       .alert_rx_i  ( alert_rx[13:12] ),
 
diff --git a/hw/top_earlgrey/sw/autogen/top_earlgrey.c b/hw/top_earlgrey/sw/autogen/top_earlgrey.c
index 25930e9..bc7a248 100644
--- a/hw/top_earlgrey/sw/autogen/top_earlgrey.c
+++ b/hw/top_earlgrey/sw/autogen/top_earlgrey.c
@@ -122,8 +122,8 @@
  */
 const top_earlgrey_alert_peripheral_t
     top_earlgrey_alert_for_peripheral[20] = {
-  [kTopEarlgreyAlertIdAesCtrlErrUpdate] = kTopEarlgreyAlertPeripheralAes,
-  [kTopEarlgreyAlertIdAesCtrlErrStorage] = kTopEarlgreyAlertPeripheralAes,
+  [kTopEarlgreyAlertIdAesRecoverable] = kTopEarlgreyAlertPeripheralAes,
+  [kTopEarlgreyAlertIdAesFatal] = kTopEarlgreyAlertPeripheralAes,
   [kTopEarlgreyAlertIdOtbnFatal] = kTopEarlgreyAlertPeripheralOtbn,
   [kTopEarlgreyAlertIdOtbnRecoverable] = kTopEarlgreyAlertPeripheralOtbn,
   [kTopEarlgreyAlertIdSensorCtrlAs] = kTopEarlgreyAlertPeripheralSensorCtrl,
diff --git a/hw/top_earlgrey/sw/autogen/top_earlgrey.h b/hw/top_earlgrey/sw/autogen/top_earlgrey.h
index b013810..52d004e 100644
--- a/hw/top_earlgrey/sw/autogen/top_earlgrey.h
+++ b/hw/top_earlgrey/sw/autogen/top_earlgrey.h
@@ -754,8 +754,8 @@
  * the same peripheral are guaranteed to be consecutive.
  */
 typedef enum top_earlgrey_alert_id {
-  kTopEarlgreyAlertIdAesCtrlErrUpdate = 0, /**< aes_ctrl_err_update */
-  kTopEarlgreyAlertIdAesCtrlErrStorage = 1, /**< aes_ctrl_err_storage */
+  kTopEarlgreyAlertIdAesRecoverable = 0, /**< aes_recoverable */
+  kTopEarlgreyAlertIdAesFatal = 1, /**< aes_fatal */
   kTopEarlgreyAlertIdOtbnFatal = 2, /**< otbn_fatal */
   kTopEarlgreyAlertIdOtbnRecoverable = 3, /**< otbn_recoverable */
   kTopEarlgreyAlertIdSensorCtrlAs = 4, /**< sensor_ctrl_as */