Security: Information Flow

Write "the secret never reaches this pin" as two copies of the design with equal public inputs, find a key bit on a debug port, and keep the check from passing for nothing.

Lesson
F5.8
Time
15 min
Runs on
SymbiYosys
Check
4 questions

A crypto block's debug mux carried state and counters onto eight pins. A late change widened one internal bus and shifted a bit, so one select now carried the low bit of the key. Functional tests passed, because the debug port was not functional, and the only answer to "can the key reach the pins?" was a diagram.

Two copies in lockstep until the debug select reaches 2
01234clkfreerstfreekey_ldfreedbg_sel020freek_a01freek_b00dbg_a000100dbg_b00both copies load their keydbg_sel = 2dbg_a != dbg_bSBY reports FAIL

Measured with smtbmc yices at depth 20. The solver chose keys 01 and 00: they differ in bit 0, the bit that leaks. Sampled at step 3, reported at 4.

Influence Is a Relation Between Runs

Self-Composition: Two Copies, One DifferenceEvery public input goes to both copies; only the secret differs. If a sink ever differs, the secret reached it. Arrows mean "drives".
data: where the transaction goes
Self-composition: two copies, one differenceEvery public input goes to both copies; only the secret differs. If a sink ever differs, the secret reached it. Arrows mean "drives". component Public inputs din, start, key_ld,dbg_sel: one wire toboth copies component k_a (anyconst) secret for copy A component k_b (anyconst) secret for copy B component sec_dbg copy A the design, untouched component sec_dbg copy B the same design again component assert(dbg_a ==dbg_b) the sink that mustnot see the key component dout_a, dout_b the ciphertext:allowed to differ, sonot asserted
Read the diagram as text
  • Public inputs: din, start, key_ld, dbg_sel: one wire to both copies
  • k_a (anyconst): secret for copy A
  • k_b (anyconst): secret for copy B
  • sec_dbg copy A: the design, untouched
  • sec_dbg copy B: the same design again
  • assert(dbg_a == dbg_b): the sink that must not see the key
  • dout_a, dout_b: the ciphertext: allowed to differ, so not asserted
  • Public inputs → sec_dbg copy A
  • Public inputs → sec_dbg copy B
  • k_a (anyconst) → sec_dbg copy A
  • k_b (anyconst) → sec_dbg copy B
  • sec_dbg copy A → assert(dbg_a == dbg_b)
  • sec_dbg copy B → assert(dbg_a == dbg_b)
  • sec_dbg copy B → dout_a, dout_b

The property never mentions the key's value. It asks whether two runs that differ only in the key can be told apart at the sink.

The claim that the key never influences the debug port is not true or false of one run: no single trace shows influence. It is a statement about two runs. Take two copies of the design, give them every public input in common and different secrets, and assert that the debug outputs stay equal.

Here the copies share their input wires, which is the same as assuming the inputs equal every step. The two keys are anyconst, chosen once by the solver (F1.3). With the correct mux the equality gives BMC PASS to depth 20.

The Leak, Two Copies Stacked

SinkMay depend on the key?Property
dbg, the debug portnoassert(dbg_a == dbg_b)
dout, the ciphertextyes, by designnone; cover(dout_a != dout_b): Cover reached, reported step 3
a checksum the spec lets outonly through that valueassume(sum_a == sum_b): an assumption on a derived value, reviewed as a declassification

The cover on the allowed sink is the vacuity check: if the keys could not make the copies differ anywhere, every equality would pass for nothing.

With the shifted bus the run gives FAIL at reported step 4. The solver chose two keys that differ in bit 0, loaded them, and set the select to 2. Nothing else differs between the copies until that step.

This is a safety property: it has a failing step. It is also complete for non-interference at that sink: if no pair of runs differing only in the secret ever differs there, the sink carries nothing about the secret. A timing leak shows up the same way if the sink's valid signal is in the equality.

Proving It, and the Shortcut Tools Take

Correct muxMeasured resultTime
bmc, depth 20BMC PASS to depth 20555 ms
prove, k-inductionUNKNOWN at k = 4 (base case passed, induction step failed)538 ms
prove, abc pdrProve PASS (abc pdr)581 ms

Engine smtbmc yices unless named; reset assumed in step 0.

k-induction fails because an unreachable start state may have the two copies already out of step in their counters. PDR closes it without help. The invariant it needs is that every key-free register is equal in both copies. Writing that as a helper is the manual route, but this engine refuses the hierarchical names (a.cnt) it would take.

Commercial security apps usually track a taint mark from the secret through the logic. That is a one-copy approximation of the two-copy definition, and it can flag paths where the value cannot actually leak. Two copies are the definition.

Asked in Interviews

QuestionAnswer
What is information-flow verification?Proving a secret cannot influence a sink: two copies, equal public inputs, different secrets, equal sinks. This is self-composition.
How is it a safety property?The equality has a failing step and a trace.
What is taint tracking?A one-copy approximation that follows a mark from the secret; the two-copy form is the definition.
What is declassification?An allowed flow, written as an assumption on a derived value and reviewed as such.

Try It

The rule in SVA

assert property (@(posedge clk) disable iff (rst) dbg_a == dbg_b);
cover property (@(posedge clk) !rst && dout_a != dout_b);

One equality per forbidden sink, one cover on an allowed one.

The composition as this engine runs it

module sec_top(input clk, rst, start, key_ld, input [7:0] din, input [1:0] dbg_sel);
  (* anyconst *) reg [7:0] k_a, k_b;   // two secrets, chosen once by the solver
  wire [7:0] dout_a, dout_b, dbg_a, dbg_b;
  sec_dbg a(.clk(clk), .rst(rst), .start(start), .key_ld(key_ld), .din(din), .key_in(k_a), .dbg_sel(dbg_sel), .dout(dout_a), .dbg(dbg_a));
  sec_dbg b(.clk(clk), .rst(rst), .start(start), .key_ld(key_ld), .din(din), .key_in(k_b), .dbg_sel(dbg_sel), .dout(dout_b), .dbg(dbg_b));
`ifdef FORMAL
  always @(posedge clk) begin
    if ($initstate) begin assume(rst); end else begin assume(!rst); end
  end
  always @(posedge clk) if (!rst) begin
    assert(dbg_a == dbg_b);   // the sink must not see the secret
  end
`endif
endmodule

Two instances, shared inputs, two anyconst keys.

The whole task. prep -top sec_top flattens both copies into one model.
[options]
mode bmc
depth 20
[engines]
smtbmc yices
[script]
read_verilog -formal -sv design.sv
prep -top sec_top
[files]
design.sv
  1. Check the bench is alive

    Run the cover on dout first: Cover reached, reported step 3. The keys can make the copies differ.

  2. Run the leak

    Run as it is: FAIL at reported step 4. Find the step where the copies split and the keys' differing bit.

  3. Mask the spare

    Drive the spare select with zero: BMC PASS to depth 20.

  4. Prove it

    Switch to mode prove: k = 4 gives UNKNOWN at k = 4 (base case passed, induction step failed); abc pdr gives Prove PASS (abc pdr).

  5. Break it on purpose

    Add always @(*) assume(k_a == k_b); and rerun the cover. Why does every equality now pass (F3.2)?

design.sv

props.sv

Use assert property (@(posedge clk) ...);. Leave blank for immediate assertions in the design.

Inputs are free unless constrained. Run to find a counterexample or check a proof.

Model: one positive-edge clock, binary data. Sampled history requires declaration-initialized variables; primary-input history is unsupported.

SBY summary and log

Check Yourself

Predict the outputsec_top with the key bit on debug select 2, mode bmc, default depth 20, smtbmc yices, reset assumed in step 0, keys anyconst, public inputs shared, assert(dbg_a == dbg_b). At which reported step does it fail?
module sec_dbg(input clk, rst, start, key_ld, input [7:0] din, key_in, input [1:0] dbg_sel,
  output [7:0] dout, output reg [7:0] dbg=0);
  reg [7:0] key=0, acc=0;
  reg [3:0] cnt=0;
  reg busy=0;
  wire [7:0] mix = acc ^ key;
  assign dout = (mix << cnt[2:0]) | (mix >> (4'd8 - cnt[2:0]));   // the ciphertext: may depend on key
  always @(posedge clk)
    if (rst) begin key <= 0; acc <= 0; cnt <= 0; busy <= 0; dbg <= 0; end
    else begin
      if (key_ld) key <= key_in;
      if (start) begin busy <= 1; cnt <= 0; acc <= din; end
      else if (busy) begin cnt <= cnt + 4'd1; acc <= acc + din; if (cnt == 4'd7) busy <= 0; end
      case (dbg_sel)                                   // the debug port: must not depend on key
        2'd0: dbg <= {4'd0, cnt};
        2'd1: dbg <= {busy, 3'd0, cnt};
        2'd2: dbg <= {7'd0, key[0]};   // bug: a shifted bus lands key[0] here
        2'd3: dbg <= acc;
      endcase
    end
endmodule

// Self-composition: two copies, every public input shared, only the secret differs.
module sec_top(input clk, rst, start, key_ld, input [7:0] din, input [1:0] dbg_sel);
  (* anyconst *) reg [7:0] k_a, k_b;   // two secrets, chosen once by the solver
  wire [7:0] dout_a, dout_b, dbg_a, dbg_b;
  sec_dbg a(.clk(clk), .rst(rst), .start(start), .key_ld(key_ld), .din(din), .key_in(k_a), .dbg_sel(dbg_sel), .dout(dout_a), .dbg(dbg_a));
  sec_dbg b(.clk(clk), .rst(rst), .start(start), .key_ld(key_ld), .din(din), .key_in(k_b), .dbg_sel(dbg_sel), .dout(dout_b), .dbg(dbg_b));
`ifdef FORMAL
  always @(posedge clk) begin
    if ($initstate) begin assume(rst); end else begin assume(!rst); end
  end
  always @(posedge clk) if (!rst) begin
    assert(dbg_a == dbg_b);   // the sink must not see the secret
  end
`endif
endmodule
ConceptWhich bench line makes the security check pass for nothing?
ConceptWhy does information flow need two copies of the design?
Read the waveformIn the stacked trace, at which step do the copies first differ, and on which signal?
Two copies in lockstep until the debug select reaches 2
01234clkfreerstfreekey_ldfreedbg_sel020freek_a01freek_b00dbg_a000100dbg_b00both copies load their keydbg_sel = 2dbg_a != dbg_bSBY reports FAIL

Measured with smtbmc yices at depth 20. The solver chose keys 01 and 00: they differ in bit 0, the bit that leaks. Sampled at step 3, reported at 4.

Remember

  • Information flow is a property of two runs: two copies, shared public inputs, different secrets, equal forbidden sinks.
  • The leak has a failing step, so it is a safety check; a proof means the sink carries nothing about the secret.
  • Cover an allowed sink to show the secrets can make a difference, or the equalities may pass for nothing.
  • Taint tracking approximates the two-copy definition; declassification is an assumption to review.