Ethernet · Module 25
"CRC Provides Security"
CRC-32 is linear over GF(2), so a chosen change has a computable compensating change to the check sequence — a residual of 1 where noise sees 2⁻³², and 6.92x the gates to fix it.
Chapter 6.1 specified what the frame check sequence guarantees, with bounds attached to every clause. This chapter is about a guarantee it never made and is constantly credited with.
The myth, stated as the people who hold it would state it. Every frame carries a 32-bit check value computed over its contents. A frame whose contents were altered fails the check. So a frame that passes the check has not been altered, and the FCS is therefore an integrity mechanism.
The first two sentences are true. The third does not follow, and the reason is one algebraic property.
| What is true | Section | |
|---|---|---|
| CRC-32 is a linear function over GF(2) | CRC(a ⊕ b) = CRC(a) ⊕ CRC(b) for the pure division | 2 |
| so a chosen change has a computable compensation | change the payload, compute the delta, apply it to the FCS | 4 |
| the residual for a RANDOM error is 2⁻³² | one in 4 294 967 296 | 6 |
| the residual for a CHOSEN change is 1 | exactly one, not a smaller number | 8 |
Against noise the check fails once in four billion times. Against an adversary it fails every time, and the difference is not a matter of degree — it is the difference between a probability and a certainty.
And the substitute is not free. Section 10 derives what an actual integrity mechanism costs: 404 640 gate equivalents of AES-GCM at 400 Gb/s against a 49 142-GE CRC engine — 8.2× — plus 24 to 32 octets of frame overhead, which at minimum frame size is 28.57% to 38.10% more wire.
1. Scope — What a Check Detects and What It Resists
This chapter owns four derivations and one substitute, priced.
| What is derived | |
|---|---|
| Sections 2 to 5 | linearity from the polynomial, and the compensation construction it implies |
| Sections 6 to 9 | what CRC guarantees with bounds, and why the adversarial case is a certainty rather than a small probability |
| Sections 10 to 13 | MACsec's GCM-AES: gates, frame overhead, key state, and what it does to Chapter 25.1's parse window |
| Sections 14 to 17 | what an integrity claim must never do, and the two mechanisms side by side |
What this chapter does not own. Chapter 6.2 owns the polynomial division and the four conventions wrapped around it; Chapter 6.3 owns the residue; Chapter 6.4 owns the parallel engine; Chapter 19.4 owns its integration and its area. This chapter uses all four and re-derives none of them.
And it is a defensive chapter. The mathematics below is the reason a design must not use a check sequence as an integrity mechanism, and the remedy is the mechanism that does the job. Nothing here is tooling for altering frames on a network; it is the arithmetic that says why the check cannot be relied on and what replaces it.
2. The Linearity, Derived from the Polynomial
Chapter 6.2 §3 established the arithmetic in one sentence: treat the message as a polynomial over the field with two elements, divide it by the generator, and keep the remainder. Linearity follows from that sentence and nothing else.
Write the pure division — no initial value, no reflection, no complement — as a function.
R(m) = m(x) · x³² mod G(x) over GF(2)
G(x) = x³² + x²⁶ + x²³ + x²² + x¹⁶ + x¹² + x¹¹ + x¹⁰ + x⁸ + x⁷ + x⁵ + x⁴ + x² + x + 1That is 0x04C11DB7 with the x³² term implicit — Chapter 6.2 §4's taps, read off directly.
Now the property, in three lines.
addition in GF(2) is XOR, and polynomial multiplication distributes over it:
(a ⊕ b) · x³² = (a · x³²) ⊕ (b · x³²)
the remainder of a sum is the sum of the remainders:
((a ⊕ b) · x³²) mod G = ((a · x³²) mod G) ⊕ ((b · x³²) mod G)
therefore:
R(a ⊕ b) = R(a) ⊕ R(b)The check value of a sum is the sum of the check values, which is the definition of a linear function — and it is a consequence of the mechanism being polynomial division rather than an accident of this polynomial.
Three things follow immediately and each matters for the rest of the chapter.
One — the effect of a change depends only on the change. If a frame m becomes m ⊕ d, then R(m ⊕ d) = R(m) ⊕ R(d). The new check value is the old one XORed with the check value of the difference pattern, and the difference pattern is what the adversary chose. The original frame's contents do not enter.
Two — the conventions do not remove the property, they make it affine. Chapter 6.2 §5's four wrappers — a nonzero initial value, input and output reflection, and a final complement — change the value and not the structure. With them, the Ethernet FCS satisfies:
FCS(m ⊕ d) = FCS(m) ⊕ R₀(d)where R₀ is the pure division of d with a zero initial value and no complement. The initial value contributes a constant that cancels in the difference; the complement contributes a constant that cancels in the difference; and reflection is a relabelling of bit order that commutes with XOR. So the compensating delta is computable from the difference pattern alone.
Three — this is not a weakness in CRC-32 and a stronger polynomial does not fix it. The property holds for every cyclic redundancy check, because every one of them is polynomial division. A 64-bit CRC has the same property, and so would a 256-bit one.
3. RTL 1 — The Security Package and the CRC Difference Model
// ---------------------------------------------------------------------
// crcsec_pkg -- the constants an integrity argument needs, with the
// polynomial taken from Chapter 6.2 Section 4 and every derived figure
// computed here rather than written as a literal.
//
// Units: Chapter 23.3 Section 2's bitcell equivalent for STATE, and
// gate equivalents for LOGIC. Section 19 explains why this chapter
// needs both and the previous eight needed one.
// 1 BCE = 0.35 GE = one bit of usable on-die SRAM
// 1 flip-flop = 7 GE = 20 BCE
// Chapter 19.7 Section 19's MAC receive datapath = 283 320 BCE
// ---------------------------------------------------------------------
package crcsec_pkg;
localparam int unsigned DATAPATH_BCE = 283_320;
localparam int unsigned BCE_PER_FLOP = 20;
localparam int unsigned GE_PER_FLOP = 7;
localparam int unsigned GE_PER_XOR = 3; // 2-input XOR, rounded up
// ---- the generator, Chapter 6.2 Section 4 ---------------------------
// x^32 implicit; the stored value is the low 32 terms.
localparam logic [31:0] CRC32_POLY = 32'h04C1_1DB7;
localparam int unsigned CRC_WIDTH = 32;
// ---- Chapter 6.1's guarantees, with their bounds --------------------
localparam int unsigned BURST_BOUND_BITS = 32;
localparam int unsigned WEIGHT3_ANY_LENGTH = 3;
localparam int unsigned WEIGHT4_MAX_OCTETS = 375;
// ---- MACsec, 802.1AE -------------------------------------------------
localparam int unsigned SECTAG_NO_SCI = 8;
localparam int unsigned SECTAG_WITH_SCI = 16;
localparam int unsigned ICV_OCTETS = 16;
localparam int unsigned AES_BLOCK_BITS = 128;
localparam int unsigned AES_ROUNDS = 10; // AES-128
// ---- a gate model for the two mechanisms ----------------------------
// Derived, not quoted: an S-box optimised for a pipelined datapath,
// MixColumns as four column circuits, AddRoundKey as 128 XORs.
localparam int unsigned SBOX_GE = 400;
localparam int unsigned MIXCOL_GE = 800; // 4 columns x 200
localparam int unsigned GHASH_GE = 17_000; // Karatsuba + reduction
// Chapter 19.4 Section 6's 512-bit engine, re-read: 19 461 XOR terms
// across both cores and about 70 flops of state.
localparam int unsigned CRC_XOR_TERMS = 19_461;
localparam int unsigned CRC_FLOPS = 70;
typedef enum logic [1:0] {
THREAT_NOISE = 2'd0, // Chapter 6.1's channel
THREAT_FAULT = 2'd1, // a broken device, no intent
THREAT_CHOSEN = 2'd2 // a difference pattern somebody selected
} threat_e;
// ---- the polynomial division, one bit, no conventions ---------------
function automatic logic [31:0] crc_step(logic [31:0] state, logic bit_in);
logic fb;
fb = state[31] ^ bit_in;
return fb ? ((state << 1) ^ CRC32_POLY) : (state << 1);
endfunction
// R0(d): the pure remainder of a difference pattern, zero init, no
// complement. Section 4 shows this IS the compensating delta.
function automatic logic [31:0] r0(logic [1023:0] d, int unsigned nbits);
logic [31:0] s;
s = '0;
for (int i = 0; i < nbits; i++) s = crc_step(s, d[i]);
return s;
endfunction
// ---- derived gate counts ---------------------------------------------
function automatic int unsigned aes_round_ge();
return (16 * SBOX_GE) + MIXCOL_GE + (AES_BLOCK_BITS * GE_PER_XOR);
endfunction
function automatic int unsigned aes_pipeline_ge();
return (AES_ROUNDS * aes_round_ge())
+ (AES_ROUNDS * AES_BLOCK_BITS * GE_PER_FLOP);
endfunction
function automatic int unsigned gcm_pipeline_ge();
return aes_pipeline_ge() + GHASH_GE;
endfunction
function automatic int unsigned crc_engine_ge();
return (CRC_XOR_TERMS * GE_PER_XOR) + (CRC_FLOPS * GE_PER_FLOP);
endfunction
function automatic int unsigned datapaths_milli(int unsigned bce);
return (bce * 1000) / DATAPATH_BCE;
endfunction
endpackage// ---------------------------------------------------------------------
// crc_difference_model -- Section 2's linearity, measured.
//
// It computes the check value of a frame, of a difference pattern, and
// of their exclusive-or, and asserts that the third equals the first
// two combined. The output that matters is linearity_holds, which is
// always high and is the chapter's entire subject.
// ---------------------------------------------------------------------
module crc_difference_model
import crcsec_pkg::*;
(
input logic clk,
input logic rst_n,
input logic eval,
input logic [1023:0] frame_bits,
input logic [1023:0] diff_bits,
input logic [15:0] nbits,
output logic [31:0] r_frame,
output logic [31:0] r_diff,
output logic [31:0] r_combined,
output logic [31:0] r_expected,
output logic linearity_holds,
output logic diff_is_nonzero,
output logic [15:0] diff_weight,
output logic [15:0] diff_span_bits,
output logic within_burst_bound,
output logic [31:0] c_evaluations
);
logic [1023:0] combined;
int unsigned lo, hi;
always_comb begin
combined = frame_bits ^ diff_bits;
r_frame = r0(frame_bits, int'(nbits));
r_diff = r0(diff_bits, int'(nbits));
r_combined = r0(combined, int'(nbits));
// Section 2's identity, evaluated rather than asserted in prose.
r_expected = r_frame ^ r_diff;
linearity_holds = (r_combined == r_expected);
diff_is_nonzero = (diff_bits != '0);
diff_weight = '0;
for (int i = 0; i < 1024; i++)
if (diff_bits[i]) diff_weight = diff_weight + 16'd1;
// The span between the first and last set bit -- Chapter 6.1's
// burst length, which is what the 32-bit bound is about.
lo = 1024; hi = 0;
for (int i = 0; i < 1024; i++) begin
if (diff_bits[i]) begin
if (i < lo) lo = i;
if (i > hi) hi = i;
end
end
diff_span_bits = diff_is_nonzero ? 16'(hi - lo + 1) : 16'd0;
within_burst_bound = diff_is_nonzero
&& (diff_span_bits <= 16'(BURST_BOUND_BITS));
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) c_evaluations <= '0;
else if (eval) c_evaluations <= c_evaluations + 32'd1;
end
endmoduleClassification: a measurement of an algebraic identity, whose output is a constant 1 that the chapter exists to explain.
What it teaches: that linearity_holds is high for every frame and every difference pattern, so Section 2's identity is not a special case or an approximation. The check value of a modified frame depends on the original's check value and on the difference — and on nothing else about the original. That is the sentence the rest of the chapter is built on.
And it teaches that diff_span_bits is the quantity Chapter 6.1 §5's burst bound is about. A difference confined to 32 bits or fewer is always detected; the model reports within_burst_bound so a scenario can be classified before its outcome is examined, and Section 9's adversary model exists precisely to produce differences that are not.
Deliberately simplified: r0 is a bit-serial loop, unsynthesisable at any useful width and written that way because Chapter 6.4 owns the parallel form and this module is about the algebra. The frame is capped at 1 024 bits where a real frame is up to 12 144; the identity does not depend on the length and the model does not need to demonstrate it at one. And the conventions are absent — no initial value, no reflection, no complement — which is deliberate: Section 4 shows they cancel in the difference, and showing that requires starting without them.
Production implication: the weight and span outputs are the two a verification environment should keep, and the reason is that they classify a failure before anybody looks at the data. A received frame that fails its FCS carries a difference pattern nobody can see — the original is not available — but a frame that fails in a testbench does, and recording the injected difference's weight and span turns a pass-fail result into evidence about which of Chapter 6.1's guarantees was exercised. Chapter 20.5 §20's class 92 is the warning that comes with it: the injector's model of an error and the design's classification of it must not be the same rules written twice, and the defence is that these two outputs describe the pattern rather than the verdict.
4. The Compensation Construction
Section 2 gave the identity. This section states what it means operationally, which is the part the myth cannot survive.
The construction, in four lines.
a frame m has check value FCS(m), and the receiver verifies it
somebody changes the payload: m becomes m ⊕ d
by Section 2's identity: FCS(m ⊕ d) = FCS(m) ⊕ R₀(d)
so the altered frame's check value is computable from d aloneThe fourth line is the whole finding. The compensating value does not require knowing the original frame's contents, only the difference pattern and the original check value — and the original check value is in the frame, in plain sight, four octets from the end.
Work through what each party has.
| The receiver has | Anybody on the path has | |
|---|---|---|
| the polynomial | yes — it is in the standard | yes — same standard |
| the frame | yes | yes |
| the original FCS | yes | yes — it is the last four octets |
| a secret | NO | — |
Row four is empty on both sides and that is the mechanism's situation, not a flaw in its arithmetic. Every input the check uses is public, so the check distinguishes a party who followed the rules from a party who did not follow them — and an adversary follows them.
The conventions do not help. Chapter 6.2 §5's initial value, reflections and final complement change the check's value and not its structure: the initial value's contribution is identical for
mandm ⊕ dand cancels in the difference; the complement is a constant XOR and cancels; reflection is a relabelling of bit positions that commutes with XOR. All four survive the subtraction.
And the padding and length rules do not help either. Chapter 5.6's minimum-length padding is part of the frame, so it is part of m and part of what the difference may touch.
Three defensive consequences follow, and they are the reason this section is in an engineering track rather than a security one.
One — a design must not treat a passing FCS as evidence about the sender. Chapter 5.8 §7 already said what a passing check IS evidence of: that the covered octets arrived as they left the last append point, which is a statement about a hop and about noise. It is not evidence about origin, intent, or authorship.
Two — end-to-end integrity cannot be built from hop-by-hop checks even in principle. Chapter 5.8 §8 established that the FCS is recomputed at every store-and-forward hop; so the check a receiver verifies was computed by the last switch, on whatever that switch had, and it certifies the last hop and nothing before it.
Three — the remedy is not a bigger check. A 64-bit CRC has the same linearity and therefore the same compensation; the remedy is a mechanism whose guarantee rests on a secret, which is Section 10.
5. RTL 2 — The Compensating-Delta Computer
// ---------------------------------------------------------------------
// compensating_delta -- Section 4's construction as a model, so a
// verification environment can demonstrate that a chosen difference
// leaves the check satisfied.
//
// It is an ELABORATION and TESTBENCH model. Its purpose is to make the
// design's integrity claim falsifiable: a design asserting that a
// passing FCS implies an unaltered frame can be shown wrong here, at
// no cost and on no real network.
//
// The output that matters is check_still_passes, which is hard-wired
// high for any nonzero difference. That constant IS the myth's refutation.
// ---------------------------------------------------------------------
module compensating_delta
import crcsec_pkg::*;
(
input logic clk,
input logic rst_n,
input logic eval,
input logic [31:0] fcs_original,
input logic [1023:0] diff_bits,
input logic [15:0] nbits,
input logic [1:0] threat, // threat_e
output logic [31:0] delta,
output logic [31:0] fcs_compensated,
output logic check_still_passes,
output logic requires_a_secret,
output logic inputs_are_all_public,
output logic [15:0] bits_of_secret_available,
output logic [31:0] detection_prob_recip, // 1 in N, for noise
output logic [31:0] c_compensations
);
always_comb begin
// Section 4 line 3: the compensating value is R0 of the difference.
delta = r0(diff_bits, int'(nbits));
fcs_compensated = fcs_original ^ delta;
// For a CHOSEN difference, the compensated frame verifies. For a
// difference the channel produced, nobody computed the delta and
// the check does its job.
check_still_passes = (threat == 2'(THREAT_CHOSEN)) && (diff_bits != '0);
// THE structural facts.
requires_a_secret = 1'b0; // the CRC needs none
inputs_are_all_public = 1'b1; // polynomial, frame, FCS
bits_of_secret_available = 16'd0;
// Chapter 6.1 Section 6: the residual for a pattern outside the
// deterministic classes. Meaningful for noise, meaningless for a
// chosen difference, and reported as such.
detection_prob_recip = (threat == 2'(THREAT_CHOSEN))
? 32'd1 // 1 in 1 -- it passes
: 32'hFFFF_FFFF; // 1 in 4 294 967 295+
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) c_compensations <= '0;
else if (eval) c_compensations <= c_compensations + 32'd1;
end
endmoduleClassification: one XOR and three constants, of which the constants are the content.
What it teaches: that bits_of_secret_available is zero and that this is the entire difference between a check and a protection. The module's arithmetic is a single XOR — fcs_original ^ r0(diff) — and the reason it works is not that the arithmetic is clever but that no input to it is withheld from anybody. Section 10's substitute changes exactly this field and nothing else about the shape of the problem.
And it teaches that detection_prob_recip reports two incomparable things under one name. For a noise difference it is about 4.29 × 10⁹ — Chapter 6.1 §6's 2⁻³² residual. For a chosen difference it is 1, meaning the frame passes. Those are not two points on a scale; the first is a probability over a distribution the design does not control and the second is a certainty, and Section 8 is about why writing them in the same column is the myth's root.
Deliberately simplified: threat is an input a testbench supplies, because no hardware can determine whether a difference was chosen — that is the property that makes the problem what it is, and the module makes it an explicit input rather than pretending otherwise. The delta is computed with the pure division and Section 4's argument about the conventions cancelling is taken as established rather than re-demonstrated. And the module does not model a length change, which is the harder case: altering the frame's length moves the FCS's position and changes what is covered, and a design that relies on length consistency is relying on Chapter 5.5's length field, which is inside the covered range and therefore inside d.
Production implication: the length case is the one that catches designs which have half-understood the problem. A design that notices the FCS can be compensated sometimes adds a second check — a length field consistency test, a payload checksum, a sequence number — and every one of them is inside the frame and therefore inside the difference the adversary chooses. Adding public checks does not accumulate protection; n public checks are compensated in n steps by a party who read the same specification. The only structure that changes the answer is a key, and Section 10 prices it. A design review's question is therefore not how many checks does the frame carry but does any of them depend on something the sender and receiver know and nobody else does.
6. What CRC Actually Guarantees, Stated Exactly
A chapter arguing that a mechanism does not do one thing owes an exact statement of what it does. Chapter 6.1 §5 gave it; this section restates it because the gap in Section 8 is measured against it.
| Guarantee | Bound | Where it comes from |
|---|---|---|
| every burst error | up to 32 bits | the generator's degree — a nonzero polynomial below degree 32 cannot be divisible by it |
| every pattern of weight ≤ 3 | at any standard frame length | the code's minimum distance |
| every pattern of weight 4 | frames up to about 375 octets | minimum distance falls as protected length rises |
| everything else | detected with probability ≈ 1 − 2⁻³² | the remainder of an arbitrary difference is effectively arbitrary |
Row four is a residual of one in 4 294 967 296, and it is worth putting that number in context rather than leaving it as an adjective.
At 100 Gb/s with 1 518-octet frames, Chapter 8.3's arithmetic gives 8.13 million frames per second. If every frame carried an error outside the deterministic classes — which no real link does — an undetected one would arrive every 528 seconds. At a realistic post-FEC frame error rate of 10⁻¹² the interval is 5.28 × 10¹⁴ seconds, which is about 16.7 million years.
Against noise the mechanism is not merely adequate; it is enormous overkill by four orders of magnitude, and that is why nobody thinks about it.
And the fourth row of Chapter 6.1's list is the one this chapter must not add to. That chapter already refused the folklore guarantee — "it detects all odd numbers of bit errors" — which does not hold for this polynomial, because a polynomial is divisible by (x + 1) exactly when it has an even number of terms and 0x04C11DB7 with its implicit x³² has fifteen. This chapter does not revisit that; it adds a different missing guarantee, which is resistance.
Two properties of the guarantee list decide everything in Section 8.
One — every clause is a statement about the difference pattern's shape. Length, weight, position. None is a statement about the difference's provenance.
Two — the mechanism cannot observe provenance. A 40-bit difference is a 40-bit difference; nothing in the frame records whether a laser diode or a person produced it.
7. RTL 3 — The Guarantee Model
// ---------------------------------------------------------------------
// guarantee_model -- Chapter 6.1's four clauses as a classifier, so a
// difference pattern can be placed against them before its outcome is
// examined.
//
// The output that matters is guarantee_applies. When it is low the
// frame is in the residual class, and the residual is meaningful only
// if the difference was not chosen -- which is the input this module
// cannot compute and Section 9's model supplies.
// ---------------------------------------------------------------------
module guarantee_model
import crcsec_pkg::*;
(
input logic clk,
input logic rst_n,
input logic eval,
input logic [15:0] diff_span_bits,
input logic [15:0] diff_weight,
input logic [15:0] frame_octets,
input logic [1:0] threat,
output logic in_burst_class,
output logic in_weight3_class,
output logic in_weight4_class,
output logic guarantee_applies,
output logic residual_class,
output logic [31:0] residual_recip,
output logic residual_is_meaningful,
output logic provenance_observable,
output logic [31:0] c_guaranteed,
output logic [31:0] c_residual
);
always_comb begin
// Chapter 6.1 Section 5, clause by clause.
in_burst_class = (diff_span_bits != 16'd0)
&& (diff_span_bits <= 16'(BURST_BOUND_BITS));
in_weight3_class = (diff_weight != 16'd0)
&& (diff_weight <= 16'(WEIGHT3_ANY_LENGTH));
in_weight4_class = (diff_weight == 16'd4)
&& (frame_octets <= 16'(WEIGHT4_MAX_OCTETS));
guarantee_applies = in_burst_class || in_weight3_class || in_weight4_class;
residual_class = (diff_span_bits != 16'd0) && !guarantee_applies;
// 2^-32, expressed as one in N.
residual_recip = 32'hFFFF_FFFF;
// THE qualification. The residual describes an ARBITRARY remainder.
// A chosen difference does not have one.
residual_is_meaningful = residual_class && (threat != 2'(THREAT_CHOSEN));
// And the fact that makes the qualification unenforceable in
// hardware: nothing in the frame says where the difference came from.
provenance_observable = 1'b0;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_guaranteed <= '0; c_residual <= '0;
end else if (eval) begin
if (guarantee_applies) c_guaranteed <= c_guaranteed + 32'd1;
if (residual_class) c_residual <= c_residual + 32'd1;
end
end
endmoduleClassification: a three-way classifier over a difference pattern, with a fourth output that says when the classification means anything.
What it teaches: that provenance_observable is hard-wired zero and it is the module's reason for existing. The three guarantee classes are computable from the difference alone; whether the residual applies is not, because it depends on how the difference arose. A design can classify the shape and can never classify the source, which is why threat is an input from a testbench and can never be one in silicon.
And it teaches that residual_is_meaningful is the field a security argument needs and no datasheet carries. A part reporting "undetected error probability 2⁻³²" has reported a number whose applicability it cannot determine, and the number is correct for the channel model Chapter 6.1 assumed and vacuous for any other.
Deliberately simplified: the weight-4 bound is a single constant at 375 octets where Chapter 6.1 §7's minimum-distance profile is a staircase with several steps. The weight count and span are supplied from Section 3's model rather than computed here, so this module trusts a structure it is classifying against. And the three classes are treated as disjoint in the counters where they overlap — a weight-2 difference spanning 10 bits is in two classes at once — which is harmless for classification and wrong for accounting.
Production implication: the overlap is worth fixing in a real coverage model, because it hides the case that matters. Chapter 20.4's coverage of error injection wants to know how many patterns fell in each class exclusively, and a pattern counted twice makes the residual class look smaller than it is. The residual class is where a design's error-handling behaviour is least tested, precisely because the guarantees make it rare — so the class that matters most for a directed test is the one the accounting most easily undercounts. One priority encoder, and the counters partition.
8. The Gap Is Not a Matter of Degree
Sections 6 and 7 established what the check promises. This section states the gap to an integrity mechanism, and the point is that it is not a gap on a scale.
Put the two numbers beside each other.
| Against noise | Against a chosen difference | |
|---|---|---|
| CRC-32 | residual 2⁻³² = 2.33 × 10⁻¹⁰ | residual 1 |
| GCM-AES with a 128-bit ICV | detected | forgery probability ≈ 2⁻¹²⁸ = 2.94 × 10⁻³⁹ |
The bottom-right cell against the top-right cell is the gap, and it is not 2⁻¹²⁸ against 2⁻³². It is 2⁻¹²⁸ against 1 — a certainty against a vanishing probability — and no amount of widening the check moves the top row, because widening it does not create a secret.
The ratio that IS a ratio is between the two mechanisms' guarantees under their own models.
2⁻³² ÷ 2⁻¹²⁸ = 2⁹⁶ = 7.92 × 10²⁸Ninety-six bits, and that number describes the gap between a detector's residual against noise and an integrity check's forgery bound against an adversary — which are the two mechanisms doing their own jobs, not the same job at different strengths.
Three consequences follow and each is a design rule.
One — a design must not offer a wider CRC as a security improvement. A 64-bit CRC's residual against noise is 2⁻⁶⁴, which is an excellent detector. Against a chosen difference it is still 1, because Section 2's linearity is a property of polynomial division at every width.
Two — a design must not stack public checks. Section 5's production note: n public checks are compensated in n steps. A length field, a payload checksum and a sequence number are all inside the covered range and all computable by the same party.
Three — the presence of a check must not be reported as the presence of integrity. Section 15's telemetry separates the two into different fields for exactly this reason.
9. RTL 4 — The Adversary Model
// ---------------------------------------------------------------------
// adversary_model -- what a mechanism's guarantee evaluates to when the
// difference pattern is chosen rather than sampled.
//
// It is a MODEL used to reason about a design's claims. It computes no
// frames and drives no interface; its outputs are a classification of
// what each mechanism promises under each threat.
//
// The output that matters is guarantee_survives, which is zero for
// every public mechanism and one for a keyed one -- and the field that
// decides it is key_bits.
// ---------------------------------------------------------------------
module adversary_model
import crcsec_pkg::*;
(
input logic clk,
input logic rst_n,
input logic eval,
input logic [1:0] threat,
input logic [15:0] check_width_bits,
input logic [15:0] key_bits,
input logic check_inputs_public,
input logic [15:0] stacked_public_checks,
output logic guarantee_survives,
output logic [15:0] effective_security_bits,
output logic [15:0] compensation_steps,
output logic widening_helps,
output logic stacking_helps,
output logic only_a_key_helps,
output logic [31:0] c_chosen,
output logic [31:0] c_noise
);
always_comb begin
// A mechanism resists a chosen difference only if something about
// it is withheld. Width is not withheld; a key is.
guarantee_survives = (threat != 2'(THREAT_CHOSEN)) || (key_bits != 16'd0);
effective_security_bits = (threat == 2'(THREAT_CHOSEN))
? key_bits // zero for a CRC
: check_width_bits; // 32, against noise
// Section 5's production note, as arithmetic: n public checks cost
// n compensations, which is linear and therefore not a defence.
compensation_steps = check_inputs_public ? stacked_public_checks : 16'hFFFF;
// The three answers a design review needs.
widening_helps = (threat != 2'(THREAT_CHOSEN));
stacking_helps = 1'b0;
only_a_key_helps = (threat == 2'(THREAT_CHOSEN));
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_chosen <= '0; c_noise <= '0;
end else if (eval) begin
if (threat == 2'(THREAT_CHOSEN)) c_chosen <= c_chosen + 32'd1;
else c_noise <= c_noise + 32'd1;
end
end
endmoduleClassification: a threat-conditioned reading of a mechanism's specification, in which one input decides every output.
What it teaches: that effective_security_bits reports 32 under noise and key_bits under a chosen difference, and that for a CRC key_bits is zero. Zero bits of security is not a small number of bits; it is the statement that the adversary's work is a single XOR, which Section 5's module performs.
And it teaches that stacking_helps is a hard-wired zero. compensation_steps equals the number of public checks, so a frame carrying five public checks costs five compensations — linear work for the adversary against a design that believed it had multiplied its protection. Only key_bits changes the shape of the problem, which is why only_a_key_helps exists as a separate output rather than being inferable from the others.
Deliberately simplified: threat is an input, as in Section 5, and for the same unavoidable reason. key_bits is treated as the security level directly, where a real analysis distinguishes the key's length from the tag's length and from the number of forgery attempts permitted — GCM's forgery bound degrades with the number of attempts and with the message length, and neither is modelled. And the model has no notion of key management, which is where deployed systems actually fail: a key that is shared too widely, rotated too rarely or stored where the adversary can read it reduces key_bits to zero as effectively as having none.
Production implication: the key-management omission is the honest one to flag, because it is where the substitute's real cost lies. Section 10 prices the gates and they are affordable; what is not automatically affordable is the machinery around the key — distribution, rotation, storage in a form the rest of the chip cannot read, and a defined behaviour when it is unavailable. A design that implements GCM-AES perfectly and stores its key in a software-readable register has key_bits equal to zero in every respect that matters, and the arithmetic in this chapter applies to it unchanged. The gate count is the easy half and the chapter says so rather than implying the problem ends at the datapath.
10. What the Real Substitute Costs
Sections 2 to 9 said the check cannot do the job. This section prices the mechanism that can, in gates, in frame overhead and in state — and the three do not compare in one unit.
MACsec — IEEE 802.1AE — authenticates and optionally encrypts a frame with GCM-AES. Derive its datapath.
| Structure | Derivation | Gate equivalents |
|---|---|---|
| one S-box, pipelined | speed-optimised rather than compact | 400 |
| SubBytes, one round | 16 S-boxes | 6 400 |
| MixColumns, one round | 4 column circuits at ≈ 200 GE | 800 |
| AddRoundKey, one round | 128 XOR gates at 3 GE | 384 |
| one AES-128 round | sum of the three | 7 584 |
| ten rounds, unrolled | × 10 | 75 840 |
| pipeline registers | 10 stages × 128 bits × 7 GE | 8 960 |
| one AES-128 pipeline | — | 84 800 |
| GHASH: a 128-bit Karatsuba multiplier plus reduction | — | 17 000 |
| one AES-GCM pipeline | — | 101 800 |
And the throughput requirement sets how many of them.
| Line rate | 128-bit blocks/s | Pipelines at 1 GHz | Total |
|---|---|---|---|
| 100 Gb/s | 0.781 G | 1 | 101 800 GE |
| 400 Gb/s | 3.125 G | 4 | 407 200 GE |
Against Chapter 19.4 §6's CRC engine, re-read rather than recalled: 19 461 XOR terms across both cores and about 70 flops of state.
19 461 XOR × 3 GE + 70 flops × 7 GE = 58 873 GE| GE | Ratio | |
|---|---|---|
| CRC-32 engine, 512-bit, dual-core | 58 873 | 1.0 |
| AES-GCM at 100 Gb/s | 101 800 | 1.73× |
| AES-GCM at 400 Gb/s | 407 200 | 6.92× |
Seven times a CRC engine at 400 Gb/s, which is a real cost and is not the reason nobody deploys MACsec. The frame overhead is.
The frame overhead, derived. A protected frame carries a SecTAG — 8 octets without the secure channel identifier, 16 with it — and a 16-octet integrity check value.
| 64-octet frame | 1 518-octet frame | |
|---|---|---|
| wire slot, unprotected | 84 | 1 538 |
| + SecTAG 8 and ICV 16 | 108 — +28.57% wire | 1 562 — +1.56% |
| + SecTAG 16 and ICV 16 | 116 — +38.10% wire | 1 570 — +2.08% |
| payload efficiency, unprotected | 54.76% | 97.53% |
| payload efficiency, +24 octets | 42.59% | 96.03% |
| payload efficiency, +32 octets | 39.66% | 95.54% |
Chapter 8.3 §2's hyperbola again, with a larger constant — and the same conclusion it always produces: the overhead is severe at the small end and negligible at the large one.
And the state, which is the one quantity BCE prices cleanly.
| Structure | Bits | BCE | × the datapath |
|---|---|---|---|
| per secure association: key 128, SCI 64, packet number 32, replay window 64 | 288 | 288 | 0.001 |
| 4 associations × 64 ports | 73 728 | 73 728 | 0.260 |
A quarter of a MAC datapath of state, and seven MAC datapaths' worth of logic if the gates are converted — and Section 19 argues that converting them is the wrong move.
11. RTL 5 — The MACsec Overhead Model
// ---------------------------------------------------------------------
// macsec_overhead_model -- what protection costs on the wire and in
// state, derived from the SecTAG and ICV sizes rather than quoted.
//
// The output that matters is wire_cost_ppm, because it is the number
// that decides whether a deployment can afford protection on its
// traffic mix -- and it is Chapter 8.3's hyperbola with a bigger
// constant, so the answer depends entirely on the frame size.
// ---------------------------------------------------------------------
module macsec_overhead_model
import crcsec_pkg::*;
(
input logic clk,
input logic rst_n,
input logic [15:0] payload_octets,
input logic include_sci,
input logic [15:0] associations,
input logic [15:0] ports,
input logic [15:0] rate_gbps,
output logic [15:0] sectag_octets,
output logic [15:0] overhead_octets,
output logic [31:0] wire_unprotected,
output logic [31:0] wire_protected,
output logic [31:0] wire_cost_ppm,
output logic [31:0] eff_unprotected_ppm,
output logic [31:0] eff_protected_ppm,
output logic [31:0] sa_state_bce,
output logic [31:0] sa_state_dp_milli,
output logic [15:0] pipelines_needed,
output logic [31:0] gcm_ge,
output logic [31:0] crc_ge,
output logic [15:0] gate_ratio_x100
);
logic [31:0] blocks_per_s_m; // millions of 128-bit blocks per second
always_comb begin
sectag_octets = include_sci ? 16'(SECTAG_WITH_SCI)
: 16'(SECTAG_NO_SCI);
overhead_octets = sectag_octets + 16'(ICV_OCTETS);
// Chapter 24.3 Section 1's 38 fixed octets, plus the protection.
wire_unprotected = 32'(payload_octets) + 32'd38;
wire_protected = wire_unprotected + 32'(overhead_octets);
wire_cost_ppm = (wire_unprotected == 0) ? 32'd0
: ((32'(overhead_octets) * 1_000_000) / wire_unprotected);
eff_unprotected_ppm = (wire_unprotected == 0) ? 32'd0
: ((32'(payload_octets) * 1_000_000) / wire_unprotected);
eff_protected_ppm = (wire_protected == 0) ? 32'd0
: ((32'(payload_octets) * 1_000_000) / wire_protected);
// State: the one quantity BCE prices without argument.
sa_state_bce = 32'd288 * 32'(associations) * 32'(ports);
sa_state_dp_milli = 32'(datapaths_milli(sa_state_bce));
// Throughput: 128-bit blocks per second against a 1 GHz core.
blocks_per_s_m = (32'(rate_gbps) * 1000) / 32'(AES_BLOCK_BITS);
pipelines_needed = 16'((blocks_per_s_m + 999) / 1000);
gcm_ge = 32'(pipelines_needed) * 32'(gcm_pipeline_ge());
crc_ge = 32'(crc_engine_ge());
gate_ratio_x100 = (crc_ge == 0) ? 16'd0
: 16'((gcm_ge * 32'd100) / crc_ge);
end
endmoduleClassification: two divisions and a gate model, reported in three units because the three costs are three kinds of thing.
What it teaches: that wire_cost_ppm is 285 714 at a 64-octet frame and 15 604 at 1 518 — 28.57% against 1.56% — and that this is the number a deployment decides on. A fabric carrying Chapter 23.2's collective traffic pays 1.56% or less; a fabric carrying minimum-size control messages pays nearly thirty per cent, and the same mechanism is cheap in one and expensive in the other.
And it teaches that gate_ratio_x100 reports 692 at 400 Gb/s — 6.92× a CRC engine — which is large in a ratio and modest in absolute terms: 407 200 GE is a few hundredths of a square millimetre at a modern node, and Chapter 23.3's switch spends 91% of its die on a buffer.
Deliberately simplified: the S-box is priced at a single figure where the choice between a compact and a speed-optimised implementation moves it by nearly a factor of two, and that choice depends on the achievable frequency — Chapter 23.6's argument that frequency is an output applies here too. pipelines_needed assumes a 1 GHz core and full pipeline utilisation, ignoring that a frame's blocks are dependent in GHASH's accumulation chain. And confidentiality is not priced separately from integrity, where a deployment that authenticates without encrypting runs the same AES and skips nothing.
Production implication: the last simplification is the one with money attached and it is worth stating. 802.1AE permits integrity without confidentiality — the ICV is computed and the payload is sent in the clear — and the gate cost is identical, because GCM's authentication path runs AES to generate the keystream whether or not the keystream is applied. So a deployment choosing integrity-only saves nothing in silicon and gains the ability for every switch, tap and analyser on the path to keep working — which is Chapter 21.9's entire apparatus continuing to function. That is usually the right trade and it is rarely made deliberately, because the two modes are presented as a security-level choice rather than as an observability one.
12. What Integrity Does to the Parse Window
Chapter 25.1 §3 established that a MAC's parse window ends at octet 13, or 21 with a VLAN tag, and that crossing the boundary costs a window. MACsec inserts a header at exactly that boundary and the consequence is a third crossing nobody counts.
Where the SecTAG goes.
octet 0 6 12 14 end−16 end−4
+------------+------------+--------+----------------------+--------+----+
| destination| source | 0x88E5 | SecTAG + protected | ICV |FCS |
+------------+------------+--------+----------------------+--------+----+
^ MACsec EtherType ^ 16 octetsSo a protected frame's real EtherType is not at octet 12. Octet 12 holds 0x88E5 — Chapter 25.1 §4's MACsec EtherType, one of the eleven that carries no IP octet — and the payload's own type is inside the protected region, after the SecTAG.
| Unprotected, untagged | Unprotected, VLAN tagged | MACsec, SecTAG 8 | MACsec, SecTAG 16 | |
|---|---|---|---|---|
| MAC's last parsed octet | 13 | 21 | 21 | 29 |
| payload EtherType at | 12 | 16 | 22 | 30 |
| can a switch classify on it? | yes | yes | only after decrypting | only after decrypting |
Row three is the operational consequence and it is larger than the gate count. A switch that classifies on an inner field — a five-tuple hash for Chapter 15.2, a priority derived from a DSCP, an access list — sees 0x88E5 and nothing else on a protected link.
MACsec does not merely add octets. It makes every field above the SecTAG invisible to every device that does not hold the key, which is every device that is not an endpoint of that link.
And that is a feature, not a defect — it is what protection means — but it interacts with Chapter 25.1 §12's five failures in a specific way. A hash with no fallback, on a MACsec link, has no tuple to find and puts every frame on one aggregation member. Chapter 25.1 §12 derived that as a 70% imbalance at 8% non-IP traffic; on a fully protected link the non-IP share is 100% and the imbalance is total.
| Non-IP share | Member 0's share of 8 | Imbalance | |
|---|---|---|---|
| ordinary traffic | 8% | 19.5% | 70% |
| fully MACsec-protected | 100% | 100% | 700% |
The fix is the one Chapter 25.1 §12 already named — hash over whatever fields are present, falling back to the MAC addresses and the EtherType — and on a protected link those three fields are exactly what remains visible.
13. RTL 6 — The Protected Parser
// ---------------------------------------------------------------------
// protected_parser -- Chapter 25.1's parse window meeting a MACsec
// boundary, and the two different reasons a parse stops.
//
// The output that matters is the pair stopped_by_budget and
// stopped_by_protection. A design that reports them through one bit has
// conflated a limit it could raise with a guarantee it bought.
// ---------------------------------------------------------------------
module protected_parser
import crcsec_pkg::*;
#(
parameter int unsigned WINDOW_OCTETS = 64,
parameter int unsigned MAX_TAGS = 2
)(
input logic clk,
input logic rst_n,
input logic frame_start,
input logic octet_valid,
input logic [15:0] octet_index,
input logic [15:0] type_at_cursor,
input logic type_valid,
input logic have_key,
output logic [15:0] cursor,
output logic [15:0] tags_seen,
output logic is_vlan,
output logic is_macsec,
output logic reached_payload_type,
output logic stopped_by_budget,
output logic stopped_by_protection,
output logic classification_available,
output logic [15:0] visible_fields_mask,
output logic [31:0] c_protected_frames,
output logic [31:0] c_budget_stops
);
localparam logic [15:0] ET_VLAN = 16'h8100;
localparam logic [15:0] ET_SVLAN = 16'h88A8;
localparam logic [15:0] ET_MACSEC = 16'h88E5;
always_comb begin
is_vlan = type_valid && ((type_at_cursor == ET_VLAN) ||
(type_at_cursor == ET_SVLAN));
is_macsec = type_valid && (type_at_cursor == ET_MACSEC);
// Two stops, two meanings.
stopped_by_budget = (cursor >= 16'(WINDOW_OCTETS))
|| (tags_seen > 16'(MAX_TAGS));
stopped_by_protection = is_macsec && !have_key;
reached_payload_type = type_valid && !is_vlan && !is_macsec
&& !stopped_by_budget;
// What a device without the key can still classify on: the two
// addresses and the outer EtherType. Chapter 25.1 Section 12's
// fallback fields, and on a protected link they are all there is.
visible_fields_mask = stopped_by_protection ? 16'h0007 // DA, SA, type
: 16'h00FF;
classification_available = reached_payload_type;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cursor <= 16'd12; tags_seen <= '0;
c_protected_frames <= '0; c_budget_stops <= '0;
end else begin
if (frame_start) begin
cursor <= 16'd12; tags_seen <= '0;
end else if (octet_valid && type_valid) begin
if (is_vlan) begin
cursor <= cursor + 16'd4;
tags_seen <= tags_seen + 16'd1;
end else if (is_macsec) begin
cursor <= cursor + 16'd8;
c_protected_frames <= c_protected_frames + 32'd1;
end
end
if (stopped_by_budget) c_budget_stops <= c_budget_stops + 32'd1;
end
end
endmoduleClassification: a chaining parser with two distinct termination conditions, and the distinction is the module's whole contribution.
What it teaches: that stopped_by_protection and stopped_by_budget are different events with different owners. A budget stop is a design limit, raised by widening the window or the tag depth; a protection stop is a property of the deployment, and no design change removes it. A part that reports one bit for both invites an integrator to widen a window that was never the constraint.
And it teaches that visible_fields_mask is the honest answer to what can this switch classify on. On a protected link the answer is the two addresses and the outer EtherType — three fields — which is exactly Chapter 25.1 §12's fallback set. A design whose hash, filter or queue selection needs more has a configuration that is silently inoperative on protected links.
Deliberately simplified: the cursor advances by a fixed 8 for MACsec where the SecTAG is 8 or 16 depending on whether the secure channel identifier is present, and the bit that says which is inside the SecTAG itself — Chapter 19.2 §6's content-dependent offset, one level deeper. The module has no notion of decrypting, so have_key is an input standing in for a whole engine. And visible_fields_mask is a constant per case where a real design may also read the SecTAG's own fields, which are in the clear.
Production implication: the SecTAG's clear fields are the ones a deployment should be using and usually is not. The packet number is visible, in the clear, on every protected frame — and it is a per-association sequence number, which is precisely the field Chapter 21.6 spent a chapter wanting: a gap in it is an unambiguous, attributable loss, visible to any device on the link without the key. A switch that counted packet-number gaps per secure association would have the reason field Chapter 21.6 §13 established does not exist in an ordinary frame. It costs a 32-bit register and a comparison per association — 4 × 64 × 32 = 8 192 bits, 8 192 BCE, 0.029 datapaths — and it turns a protected link into the most observable link in the fabric, which is the opposite of what everyone assumes MACsec does.
14. What an Integrity Claim Must Never Do
Five prohibitions, each with a failure from this chapter behind it.
| # | Never | Because |
|---|---|---|
| 1 | report a passing FCS as evidence the frame is unaltered | Section 4: the compensating delta is one XOR of public values |
| 2 | offer a wider check as a security improvement | Section 8: linearity holds at every width; a 64-bit CRC's adversarial residual is still 1 |
| 3 | stack public checks and call it defence in depth | Section 9: n public checks cost n compensations — linear work |
| 4 | count a wire error and an integrity violation in the same counter | Section 8's callout: a noisy link then produces an unactionable integrity alarm |
| 5 | report a protection stop and a budget stop through one bit | Section 13: one is a limit somebody could raise, the other is a guarantee somebody bought |
Row four is the one that gets a working MACsec deployment disabled.
A protected link carries both kinds of failure. A frame corrupted by the channel fails its FCS and is discarded — thousands of times more often than any attack — and a frame altered without the key fails its ICV. If both increment c_integrity_violations, the counter tracks the link's error rate and an operator watching it sees an attack whenever a transceiver ages. Within a month somebody turns the alarm off, and the counter that would have reported a real event is the one nobody is watching.
The separation is two counters and a wiring decision, and Section 8's callout derived why the order makes it possible: the FCS runs first and discards the channel's corruption before the cryptographic engine sees it.
15. RTL 7 — Integrity Telemetry
// ---------------------------------------------------------------------
// integrity_telemetry -- the counters that keep a protection claim
// honest, with the two failure kinds kept apart.
//
// Design rule: a check and a protection are different mechanisms, so
// every counter here names which one it is about. A design that merges
// them produces a number nobody can act on.
// ---------------------------------------------------------------------
module integrity_telemetry
import crcsec_pkg::*;
(
input logic clk,
input logic rst_n,
input logic clear,
input logic frame_valid,
input logic fcs_failed,
input logic icv_failed,
input logic protection_enabled,
input logic key_present,
input logic replay_detected,
input logic pn_gap_detected,
input logic [15:0] pn_gap_size,
input logic stopped_by_protection_i,
input logic stopped_by_budget_i,
output logic [47:0] c_frames,
output logic [47:0] c_fcs_failures,
output logic [47:0] c_icv_failures,
output logic [47:0] c_replays,
output logic [47:0] c_pn_gaps,
output logic [47:0] c_pn_frames_lost,
output logic [47:0] c_protection_stops,
output logic [47:0] c_budget_stops_o,
output logic [31:0] fcs_fail_ppm,
output logic [31:0] icv_fail_ppm,
output logic counters_are_separated,
output logic claim_is_supported,
output logic integrity_alarm
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || clear) begin
c_frames <= '0; c_fcs_failures <= '0; c_icv_failures <= '0;
c_replays <= '0; c_pn_gaps <= '0; c_pn_frames_lost <= '0;
c_protection_stops <= '0; c_budget_stops_o <= '0;
end else if (frame_valid) begin
c_frames <= c_frames + 48'd1;
// Section 8's triage, honoured: a frame the wire corrupted is
// counted as a wire error and never reaches the ICV counter.
if (fcs_failed) c_fcs_failures <= c_fcs_failures + 48'd1;
else if (icv_failed) c_icv_failures <= c_icv_failures + 48'd1;
if (replay_detected) c_replays <= c_replays + 48'd1;
if (pn_gap_detected) begin
c_pn_gaps <= c_pn_gaps + 48'd1;
// Section 13's production note: the packet number is in the
// clear, so a gap is an attributable loss count.
c_pn_frames_lost <= c_pn_frames_lost + 48'(pn_gap_size);
end
if (stopped_by_protection_i)
c_protection_stops <= c_protection_stops + 48'd1;
if (stopped_by_budget_i)
c_budget_stops_o <= c_budget_stops_o + 48'd1;
end
end
always_comb begin
fcs_fail_ppm = (c_frames == 0) ? 32'd0
: 32'((c_fcs_failures * 48'd1_000_000) / c_frames);
icv_fail_ppm = (c_frames == 0) ? 32'd0
: 32'((c_icv_failures * 48'd1_000_000) / c_frames);
// The structural claims this module makes about itself.
counters_are_separated = 1'b1;
// A protection claim is supported only if protection is on AND a
// key is present. Either alone is a claim about a configuration.
claim_is_supported = protection_enabled && key_present;
// And the alarm fires on the counter that means something.
integrity_alarm = claim_is_supported
&& ((c_icv_failures != 48'd0) || (c_replays != 48'd0));
end
endmoduleClassification: a counter bank whose defining feature is an else — the FCS failure and the ICV failure are mutually exclusive by construction.
What it teaches: that the else if in the counting logic is the whole of Section 8's triage rendered in hardware. A frame that failed its FCS never reaches the ICV counter, so icv_fail_ppm measures alteration rather than noise — and on a link with a 10⁻⁶ frame error rate and no attacker, it reads exactly zero while fcs_fail_ppm reads 1. Two numbers, two mechanisms, and only one of them should ever be nonzero.
And it teaches that claim_is_supported is a check on a configuration rather than on traffic. A port with protection enabled and no key is not protected, and a design reporting only protection_enabled has reported an intention. Both bits, ANDed, and the claim matches the state.
Deliberately simplified: pn_gap_size is supplied rather than computed, so the module trusts a comparator it does not contain. The replay counter does not distinguish a replay from a reordered frame, which on a fabric with Chapter 15.2's multi-path forwarding is a real and benign event — the replay window exists to tolerate it and a window too small counts legitimate reordering as attack. And all counters are per port where a real deployment wants them per secure association.
Production implication: the replay-window sizing is the parameter that produces false alarms and it has the same shape as three earlier parameters in this track. A window smaller than the fabric's reordering depth counts benign reordering as replay; a window larger than necessary lets a genuine replay through. The derivation is the fabric's path-skew bound — Chapter 23.2 §14's reassembly argument — divided by the frame interval, and at 100 Gb/s with 1 518-octet frames and a 1 µs skew that is 8.13 frames, so a 32-entry window is comfortable and a 4-entry one is not. It is the same move Chapter 14.2 §9 made for a pause watermark and Chapter 25.2 §8 made for a table's aging interval: derive the threshold from the delay it protects against rather than from the resource it sits in.
16. RTL 8 — The Integrity Conformance Monitor
// ---------------------------------------------------------------------
// integrity_conformance -- the checks that hold an integrity claim to
// what the mechanism actually provides.
//
// Note what is absent: there is no check that frames are unaltered.
// Section 7 established that provenance is unobservable, so no monitor
// can write it, and Section 20's rejected property is the attempt.
// ---------------------------------------------------------------------
module integrity_conformance
import crcsec_pkg::*;
(
input logic clk,
input logic rst_n,
input logic claims_integrity,
input logic [15:0] key_bits_i,
input logic fcs_checked_first,
input logic icv_checked,
input logic counters_merged,
input logic [15:0] stacked_public_checks_i,
input logic offered_as_security,
input logic protection_stop_distinct,
input logic [15:0] replay_window,
input logic [15:0] fabric_skew_frames,
output logic v_claim_without_key,
output logic v_icv_before_fcs,
output logic v_counters_merged,
output logic v_public_check_as_security,
output logic v_stop_bits_merged,
output logic v_replay_window_short,
output logic [5:0] violations,
output logic conformant
);
always_comb begin
// 1. Section 9: only a key changes the answer.
v_claim_without_key = claims_integrity && (key_bits_i == 16'd0);
// 2. Section 8's callout: the cheap check filters for the
// expensive one, so its order is load-bearing.
v_icv_before_fcs = icv_checked && !fcs_checked_first;
// 3. Prohibition 4 -- an unactionable alarm.
v_counters_merged = counters_merged;
// 4. Prohibitions 1 to 3 -- a public check offered as protection,
// however many of them there are.
v_public_check_as_security = offered_as_security && (key_bits_i == 16'd0)
&& (stacked_public_checks_i >= 16'd1);
// 5. Section 13 -- two stops, two meanings.
v_stop_bits_merged = !protection_stop_distinct;
// 6. A window below the fabric's reordering depth counts benign
// reordering as an attack.
v_replay_window_short = (replay_window < fabric_skew_frames);
violations = { v_replay_window_short, v_stop_bits_merged,
v_public_check_as_security, v_counters_merged,
v_icv_before_fcs, v_claim_without_key };
conformant = (violations == 6'b000000);
end
endmoduleClassification: six checks, none of which checks that a frame is unaltered — and the absence is the module's argument.
What it teaches: that a design cannot verify integrity and can verify that it has the means to. v_claim_without_key is the load-bearing check: a part asserting integrity with zero key bits is asserting something Section 9's model says is unavailable, and one comparison catches it.
And it teaches that v_icv_before_fcs is a check on an ordering rather than on a value. Chapter 5.8 §7 established what a passing FCS is evidence of; Section 8's callout established that running it first is what keeps the integrity counter meaningful. A design that verifies the ICV first is correct and produces a counter that tracks the wire's noise, which is the failure prohibition 4 describes arriving by a different route.
Deliberately simplified: fabric_skew_frames must be supplied and is a property of a topology the port cannot see — the same gap Chapter 25.2 §16 left open for a legitimate move rate. key_bits_i is a configuration input the monitor trusts, so a design that reports 128 while holding a key in a software-readable register passes — Section 9's production note, unenforceable here as it is unenforceable anywhere in RTL. And there is no check that the key is actually used, only that it exists.
Production implication: the unused-key case is real and has a specific signature worth recognising. A port configured for MACsec with a valid key, whose peer has not completed key agreement, falls back to unprotected transmission in many implementations — and the link works, traffic flows, and every counter reads healthy. The only evidence is c_protected_frames from Section 13 reading zero on a port that claims protection, which is a comparison between two counters that live in different blocks and are rarely put side by side. A single derived bit — claims_integrity && (c_protected_frames == 0) && (c_frames != 0) — is the check, and its absence is why "we enabled MACsec" and "MACsec is running" are different statements that no dashboard distinguishes.
17. The Two Mechanisms, Priced Side by Side
Everything this chapter derived, in one table.
| CRC-32 (the FCS) | GCM-AES (MACsec) | |
|---|---|---|
| what it is | polynomial division, linear over GF(2) | a block cipher and a field multiplier, keyed |
| inputs | all public | one is secret |
| against noise | residual 2⁻³² = 2.33 × 10⁻¹⁰ | detected |
| against a chosen difference | residual 1 | forgery ≈ 2⁻¹²⁸ = 2.94 × 10⁻³⁹ |
| adversary's work to defeat it | one XOR | 2¹²⁸ expected operations |
| logic, 400 Gb/s | 58 873 GE | 407 200 GE — 6.92× |
| state | 70 flops = 1 400 BCE | 73 728 BCE at 4 SAs × 64 ports |
| frame overhead | 4 octets | 24 or 32 octets |
| wire cost at 64 octets | 4.76% | +28.57% or +38.10% on top |
| wire cost at 1 518 octets | 0.26% | +1.56% or +2.08% |
| scope | one hop — Chapter 5.8 §8 | one hop, for the same reason |
| what it makes invisible | nothing | every field above the SecTAG |
Rows three and four together are the chapter, and the gap between the two right-hand cells is 2⁹⁶ = 7.92 × 10²⁸ — which is what separates a detector's residual against noise from an integrity check's bound against an adversary, two mechanisms each doing its own job.
The FCS is not a weak integrity mechanism. It is an excellent detector with no secret, and an integrity mechanism without a secret does not exist at any strength.
And the deployment decision, stated as the three questions Section 10's callout implies.
| Question | If the answer is | Then |
|---|---|---|
| what is the mean frame size? | large — collectives, storage, bulk | the wire cost is 1.56% and affordable |
| small — control, telemetry, synchronisation | 28.57% and it must be argued for | |
| who needs to see inside? | nobody between the endpoints | encrypt |
| taps, analysers, classifiers on the path | authenticate without encrypting — same gates, Chapter 21.9's apparatus keeps working | |
| where does the key live? | hardware the software cannot read | the arithmetic in this chapter applies |
| a software-readable register | key_bits is zero and so is everything else |
18. What the Correction Assumes
Eight assumptions, each with its direction of failure.
| # | Assumption | If it is false |
|---|---|---|
| 1 | the generator is 0x04C11DB7 with x³² implicit | Chapter 6.2 §4's taps; linearity holds for any generator, so the argument does not move |
| 2 | the four conventions cancel in the difference | Section 4: initial value and complement are constants, reflection commutes with XOR |
| 3 | Chapter 6.1's guarantee bounds | that chapter's derivation; a different polynomial moves the weight-4 length and nothing else here |
| 4 | an S-box at 400 GE | a compact implementation is nearer 230 and halves the AES figure; the ratio falls to about 4× |
| 5 | a 1 GHz cryptographic core | pipelines scale inversely; 2 GHz halves the count and doubles the timing difficulty |
| 6 | Chapter 19.4's 19 461 XOR terms | that chapter's measured figure at 512 bits; a 64-bit engine is about a sixth of it and the ratio rises |
| 7 | SecTAG 8 or 16 octets, ICV 16 | 802.1AE's sizes; a shorter ICV trades forgery probability for wire, at 2⁻ᴺ per N bits |
| 8 | BCE prices the state and not the logic | Section 19 examines it and the boundary is the chapter's cleanest |
Assumption 4 is the one that moves the headline and it deserves the caveat. A compact S-box is about 230 GE and a speed-optimised one about 400, and the choice is made by the achievable frequency — Chapter 23.6's argument that frequency is an output rather than an input applies to a cryptographic core exactly as it applies to a MAC. At 230 GE the AES pipeline is 57 600 GE and the 400 Gb/s total is 298 400 GE — 5.07× a CRC engine rather than 6.92×. The conclusion is unchanged and the number is a range.
19. The Cost, Accounted — in BCE and in Gates
This chapter's blocks.
| Block | Flops | BCE | × the datapath |
|---|---|---|---|
crc_difference_model | 32 | 640 | 0.002 |
compensating_delta | 32 | 640 | 0.002 |
guarantee_model | 64 | 1 280 | 0.005 |
adversary_model | 64 | 1 280 | 0.005 |
macsec_overhead_model | 0 — combinational | 0 | 0 |
protected_parser | 96 | 1 920 | 0.007 |
integrity_telemetry | 432 | 8 640 | 0.030 |
integrity_conformance | 0 — combinational | 0 | 0 |
| this chapter's additions | 720 | 14 400 | 0.051 |
And the designs the blocks describe, in the two units they need.
| State, BCE | Logic, GE | |
|---|---|---|
| Chapter 19.4's CRC engine, 512-bit dual-core | 1 400 | 58 873 |
| AES-GCM, 100 Gb/s | — | 101 800 |
| AES-GCM, 400 Gb/s | — | 407 200 |
| MACsec SA state, 4 × 64 ports | 73 728 | — |
| packet-number gap detectors, 4 × 64 | 8 192 | — |
| Chapter 19.7 §19's MAC receive datapath | 283 320 | — |
20. Properties Worth Asserting, and One Worth Refusing
Fifty-one properties in six groups, and the refused one is the sentence the whole myth reduces to.
Group A — linearity (9).
// A1. The identity, on every frame and every difference.
p_ln_linearity: assert property (@(posedge clk) disable iff (!rst_n)
eval |-> (r_combined == (r_frame ^ r_diff)));
// A2. And the model reports it.
p_ln_holds: assert property (@(posedge clk) disable iff (!rst_n)
eval |-> linearity_holds);
// A3. A zero difference changes nothing.
p_ln_zero_diff: assert property (@(posedge clk) disable iff (!rst_n)
(diff_bits == '0) |-> (r_combined == r_frame));
// A4. The difference's check value does not depend on the frame.
p_ln_diff_independent: assert property (@(posedge clk) disable iff (!rst_n)
(diff_bits == $past(diff_bits)) |-> (r_diff == $past(r_diff)));
// A5. Weight is the count of set bits and nothing else.
p_ln_weight: assert property (@(posedge clk) disable iff (!rst_n)
(diff_bits == '0) |-> (diff_weight == 16'd0));
// A6. Span is zero only for a zero difference.
p_ln_span_zero: assert property (@(posedge clk) disable iff (!rst_n)
(diff_span_bits == 16'd0) |-> !diff_is_nonzero);
// A7. Span is at least the weight.
p_ln_span_ge_weight: assert property (@(posedge clk) disable iff (!rst_n)
diff_is_nonzero |-> (diff_span_bits >= diff_weight));
// A8. A difference inside 32 bits is inside the burst class.
p_ln_burst_class: assert property (@(posedge clk) disable iff (!rst_n)
(diff_is_nonzero && (diff_span_bits <= 16'd32)) |-> within_burst_bound);
// A9. Every evaluation is counted.
p_ln_counted: assert property (@(posedge clk) disable iff (!rst_n)
eval |=> (c_evaluations == $past(c_evaluations) + 32'd1));Group B — the compensation (8).
// B1. The delta is the difference's pure remainder.
p_cp_delta: assert property (@(posedge clk) disable iff (!rst_n)
eval |-> (delta == r0(diff_bits, int'(nbits))));
// B2. And the compensated value is one XOR away.
p_cp_xor: assert property (@(posedge clk) disable iff (!rst_n)
(fcs_compensated == (fcs_original ^ delta)));
// B3. A chosen nonzero difference leaves the check satisfied.
p_cp_passes: assert property (@(posedge clk) disable iff (!rst_n)
((threat == 2'(THREAT_CHOSEN)) && (diff_bits != '0)) |-> check_still_passes);
// B4. And a channel difference does not.
p_cp_noise_fails: assert property (@(posedge clk) disable iff (!rst_n)
(threat != 2'(THREAT_CHOSEN)) |-> !check_still_passes);
// B5. The mechanism needs no secret and has none.
p_cp_no_secret: assert property (@(posedge clk) disable iff (!rst_n)
(!requires_a_secret && (bits_of_secret_available == 16'd0)));
// B6. Every input is public.
p_cp_public: assert property (@(posedge clk) disable iff (!rst_n)
inputs_are_all_public);
// B7. The reported probability is 1 for a chosen difference.
p_cp_certainty: assert property (@(posedge clk) disable iff (!rst_n)
(threat == 2'(THREAT_CHOSEN)) |-> (detection_prob_recip == 32'd1));
// B8. And 2^32-ish otherwise.
p_cp_residual: assert property (@(posedge clk) disable iff (!rst_n)
(threat != 2'(THREAT_CHOSEN)) |-> (detection_prob_recip == 32'hFFFF_FFFF));Group C — the guarantees (9).
// C1. Chapter 6.1's burst clause.
p_gu_burst: assert property (@(posedge clk) disable iff (!rst_n)
((diff_span_bits != 16'd0) && (diff_span_bits <= 16'd32)) |-> in_burst_class);
// C2. The weight-3 clause holds at any length.
p_gu_weight3: assert property (@(posedge clk) disable iff (!rst_n)
((diff_weight != 16'd0) && (diff_weight <= 16'd3)) |-> in_weight3_class);
// C3. The weight-4 clause is length-bounded.
p_gu_weight4: assert property (@(posedge clk) disable iff (!rst_n)
((diff_weight == 16'd4) && (frame_octets > 16'd375)) |-> !in_weight4_class);
// C4. Guaranteed and residual partition the nonzero differences.
p_gu_partition: assert property (@(posedge clk) disable iff (!rst_n)
(diff_span_bits != 16'd0) |-> (guarantee_applies ^ residual_class));
// C5. The residual applies only to an unchosen difference.
p_gu_residual_qualified: assert property (@(posedge clk) disable iff (!rst_n)
(threat == 2'(THREAT_CHOSEN)) |-> !residual_is_meaningful);
// C6. Provenance is never observable.
p_gu_provenance: assert property (@(posedge clk) disable iff (!rst_n)
(provenance_observable == 1'b0));
// C7. The residual reciprocal is the width's bound.
p_gu_recip: assert property (@(posedge clk) disable iff (!rst_n)
(residual_recip == 32'hFFFF_FFFF));
// C8. Guaranteed differences are counted.
p_gu_counted: assert property (@(posedge clk) disable iff (!rst_n)
(eval && guarantee_applies) |=> (c_guaranteed == $past(c_guaranteed) + 32'd1));
// C9. A zero difference is in no class.
p_gu_zero: assert property (@(posedge clk) disable iff (!rst_n)
(diff_span_bits == 16'd0) |-> (!guarantee_applies && !residual_class));Group D — the adversary model (8).
// D1. Only a key makes a guarantee survive a chosen difference.
p_ad_key_required: assert property (@(posedge clk) disable iff (!rst_n)
((threat == 2'(THREAT_CHOSEN)) && (key_bits == 16'd0)) |-> !guarantee_survives);
// D2. And with a key it does.
p_ad_key_suffices: assert property (@(posedge clk) disable iff (!rst_n)
(key_bits != 16'd0) |-> guarantee_survives);
// D3. Effective security under a chosen difference is the key length.
p_ad_effective: assert property (@(posedge clk) disable iff (!rst_n)
(threat == 2'(THREAT_CHOSEN)) |-> (effective_security_bits == key_bits));
// D4. Widening the check does not help against a chosen difference.
p_ad_widening: assert property (@(posedge clk) disable iff (!rst_n)
(threat == 2'(THREAT_CHOSEN)) |-> !widening_helps);
// D5. Stacking public checks never helps.
p_ad_stacking: assert property (@(posedge clk) disable iff (!rst_n)
(stacking_helps == 1'b0));
// D6. And the work to defeat n of them is n.
p_ad_linear_work: assert property (@(posedge clk) disable iff (!rst_n)
check_inputs_public |-> (compensation_steps == stacked_public_checks));
// D7. Only a key is named as the remedy.
p_ad_only_key: assert property (@(posedge clk) disable iff (!rst_n)
(threat == 2'(THREAT_CHOSEN)) |-> only_a_key_helps);
// D8. Chosen and noise evaluations are counted apart.
p_ad_counted_apart: assert property (@(posedge clk) disable iff (!rst_n)
(eval && (threat == 2'(THREAT_CHOSEN))) |=>
((c_chosen == $past(c_chosen) + 32'd1) && (c_noise == $past(c_noise))));Group E — MACsec overhead and the parser (9).
// E1. The SecTAG size follows the SCI flag.
p_ms_sectag: assert property (@(posedge clk) disable iff (!rst_n)
include_sci |-> (sectag_octets == 16'(SECTAG_WITH_SCI)));
// E2. Overhead is the SecTAG plus the ICV.
p_ms_overhead: assert property (@(posedge clk) disable iff (!rst_n)
(overhead_octets == sectag_octets + 16'(ICV_OCTETS)));
// E3. Protection always costs wire.
p_ms_costs_wire: assert property (@(posedge clk) disable iff (!rst_n)
(wire_protected > wire_unprotected));
// E4. And the cost falls as the frame grows -- Chapter 8.3's hyperbola.
p_ms_hyperbola: assert property (@(posedge clk) disable iff (!rst_n)
(payload_octets > $past(payload_octets)) |->
(wire_cost_ppm <= $past(wire_cost_ppm)));
// E5. Efficiency falls under protection.
p_ms_eff_falls: assert property (@(posedge clk) disable iff (!rst_n)
(eff_protected_ppm < eff_unprotected_ppm));
// E6. The pipeline count rounds up.
p_ms_pipelines: assert property (@(posedge clk) disable iff (!rst_n)
(rate_gbps != 0) |-> (pipelines_needed >= 16'd1));
// E7. Two stops, and they are distinguishable.
p_pp_two_stops: assert property (@(posedge clk) disable iff (!rst_n)
stopped_by_protection |-> !classification_available);
// E8. Without a key only three fields are visible.
p_pp_visible: assert property (@(posedge clk) disable iff (!rst_n)
stopped_by_protection |-> (visible_fields_mask == 16'h0007));
// E9. A MACsec frame with a key parses on.
p_pp_with_key: assert property (@(posedge clk) disable iff (!rst_n)
(is_macsec && have_key) |-> !stopped_by_protection);Group F — telemetry and conformance (8).
// F1. The two failure kinds are mutually exclusive by construction.
p_tl_exclusive: assert property (@(posedge clk) disable iff (!rst_n)
(frame_valid && fcs_failed) |=> (c_icv_failures == $past(c_icv_failures)));
// F2. The counters are separated and the module says so.
p_tl_separated: assert property (@(posedge clk) disable iff (!rst_n)
counters_are_separated);
// F3. A protection claim needs enablement AND a key.
p_tl_claim: assert property (@(posedge clk) disable iff (!rst_n)
claim_is_supported |-> (protection_enabled && key_present));
// F4. The alarm fires only on a supported claim.
p_tl_alarm: assert property (@(posedge clk) disable iff (!rst_n)
integrity_alarm |-> claim_is_supported);
// F5. A packet-number gap accumulates the lost count.
p_tl_pn_gap: assert property (@(posedge clk) disable iff (!rst_n)
(frame_valid && pn_gap_detected) |=>
(c_pn_frames_lost == $past(c_pn_frames_lost) + 48'($past(pn_gap_size))));
// F6. An integrity claim with no key is a violation.
p_cf_no_key: assert property (@(posedge clk) disable iff (!rst_n)
(claims_integrity && (key_bits_i == 16'd0)) |-> v_claim_without_key);
// F7. The FCS must be checked before the ICV.
p_cf_order: assert property (@(posedge clk) disable iff (!rst_n)
icv_checked |-> fcs_checked_first);
// F8. Conformance is the disjunction of its six checks.
p_cf_vector: assert property (@(posedge clk) disable iff (!rst_n)
conformant |-> (violations == 6'b000000));Coverage — the cases a generator built around noise will not produce.
c_ln_burst32: cover property (@(posedge clk) diff_span_bits == 16'd32);
c_ln_burst33: cover property (@(posedge clk) diff_span_bits == 16'd33);
c_ln_weight4: cover property (@(posedge clk) diff_weight == 16'd4);
c_gu_residual: cover property (@(posedge clk) residual_class);
c_gu_not_mean: cover property (@(posedge clk) residual_class && !residual_is_meaningful);
c_cp_chosen: cover property (@(posedge clk) check_still_passes);
c_ad_no_key: cover property (@(posedge clk) effective_security_bits == 16'd0);
c_ad_stacked: cover property (@(posedge clk) stacked_public_checks >= 16'd3);
c_ms_min_frame: cover property (@(posedge clk) payload_octets <= 16'd46);
c_ms_jumbo: cover property (@(posedge clk) payload_octets >= 16'd9000);
c_pp_protected: cover property (@(posedge clk) stopped_by_protection);
c_pp_tag_macsec: cover property (@(posedge clk) is_vlan && is_macsec);
c_tl_icv_fail: cover property (@(posedge clk) icv_failed && !fcs_failed);
c_cf_merged: cover property (@(posedge clk) v_counters_merged);21. Verification Scenarios
Fifty-eight scenarios in six groups, plus one directed test random stimulus will not produce.
Group 1 — linearity (10).
| # | Scenario | Expect |
|---|---|---|
| 1 | any frame, any difference | linearity_holds high |
| 2 | a zero difference | r_combined == r_frame; span 0; no class |
| 3 | a single-bit difference | weight 1, span 1, burst class |
| 4 | a 32-bit burst | span 32, within_burst_bound high |
| 5 | a 33-bit burst | span 33, bound low — Chapter 6.1 §5's shortest escaper |
| 6 | the same difference on two different frames | r_diff identical — the frame does not enter |
| 7 | weight 3 at 1 518 octets | weight-3 class |
| 8 | weight 4 at 300 octets | weight-4 class |
| 9 | weight 4 at 1 518 octets | residual class — the length bound bites |
| 10 | weight 9, span 40 | residual class |
Group 2 — the compensation (9).
| # | Scenario | Expect |
|---|---|---|
| 11 | a chosen 40-bit difference | check_still_passes high |
| 12 | the same difference, THREAT_NOISE | passes low; the check does its job |
| 13 | delta for any difference | equals r0(diff); one XOR from the original FCS |
| 14 | bits_of_secret_available | 0, at every setting |
| 15 | requires_a_secret | 0 |
| 16 | detection_prob_recip, chosen | 1 |
| 17 | detection_prob_recip, noise | 4 294 967 295 |
| 18 | a 64-bit check width, chosen difference | still passes — width is not the variable |
| 19 | three stacked public checks, chosen | compensation_steps = 3 — linear work |
Group 3 — the guarantees (10).
| # | Scenario | Expect |
|---|---|---|
| 20 | span 32 | in_burst_class |
| 21 | span 33, weight 2 | weight-3 class, not burst |
| 22 | span 33, weight 12 | residual class |
| 23 | provenance_observable | 0, at every setting |
| 24 | residual class, THREAT_NOISE | residual_is_meaningful high |
| 25 | residual class, THREAT_CHOSEN | residual_is_meaningful LOW |
| 26 | 8.13 Mpps at 100 Gb/s, all frames errored | one undetected every 528 s |
| 27 | the same at a 10⁻¹² frame error rate | 5.28 × 10¹⁴ s — 16.7 million years |
| 28 | guaranteed and residual on one difference | exactly one — p_gu_partition |
| 29 | a zero difference | neither class; both counters still |
Group 4 — the adversary model (9).
| # | Scenario | Expect |
|---|---|---|
| 30 | CRC, chosen difference | guarantee_survives low; 0 effective bits |
| 31 | 128-bit key, chosen difference | survives; 128 effective bits |
| 32 | CRC, noise | survives; 32 effective bits |
| 33 | widening_helps, chosen | low |
| 34 | stacking_helps, any setting | low |
| 35 | only_a_key_helps, chosen | high |
| 36 | 5 public checks | 5 compensation steps |
| 37 | a keyed mechanism | compensation_steps = 65 535 — not a finite budget |
| 38 | chosen and noise counted | c_chosen and c_noise move apart |
Group 5 — MACsec overhead and the parser (10).
| # | Scenario | Expect |
|---|---|---|
| 39 | 46-octet payload, SecTAG 8 | wire 84 → 108; 285 714 ppm — 28.57% |
| 40 | 46-octet payload, SecTAG 16 | wire 84 → 116; 38.10% |
| 41 | 1 500-octet payload, SecTAG 8 | wire 1 538 → 1 562; 15 604 ppm — 1.56% |
| 42 | 9 000-octet payload | 0.27% |
| 43 | 100 Gb/s | 1 pipeline; 101 800 GE; ratio 173 |
| 44 | 400 Gb/s | 4 pipelines; 407 200 GE; ratio 692 |
| 45 | 4 SAs × 64 ports | 73 728 BCE; 0.260 datapaths |
| 46 | 0x88E5 at octet 12, no key | stopped_by_protection; mask 0x0007 |
| 47 | 0x88E5 with a key | parses on |
| 48 | three stacked VLAN tags, MAX_TAGS 2 | stopped_by_budget, not protection |
Group 6 — telemetry and conformance (10).
| # | Scenario | Expect |
|---|---|---|
| 49 | a frame failing FCS and ICV | only c_fcs_failures moves |
| 50 | a frame failing ICV alone | only c_icv_failures moves |
| 51 | 10⁻⁶ frame error rate, no attacker | fcs_fail_ppm = 1; icv_fail_ppm = 0 |
| 52 | protection enabled, no key | claim_is_supported low; no alarm |
| 53 | a packet-number gap of 5 | c_pn_gaps +1; c_pn_frames_lost +5 |
| 54 | claims_integrity with 0 key bits | v_claim_without_key |
| 55 | ICV checked before FCS | v_icv_before_fcs |
| 56 | merged counters | v_counters_merged |
| 57 | a 4-entry replay window, 8.13-frame skew | v_replay_window_short |
| 58 | all six checks clear | conformant high |
22. Debugging an Integrity Failure
Six symptoms, and the first question in every row is which mechanism reported this.
| Symptom | First question | Where to look |
|---|---|---|
| an integrity alarm that tracks the link's error rate | are the FCS and ICV counters merged? | Section 15 — the else if is the fix |
| MACsec enabled and no protected frames | did key agreement complete? | Section 16 — claims_integrity against c_protected_frames |
| one aggregation member carrying everything | does the hash fall back when no tuple is visible? | Section 12 — a protected link is 100% non-IP to a classifier |
| replay alarms on a multi-path fabric | is the window smaller than the path skew? | Section 15 — 8.13 frames at 100 Gb/s and 1 µs |
| a parse that stops on some frames only | budget or protection? | Section 13 — two bits, two meanings |
| a passing frame whose contents are wrong | is there an ICV at all? | Section 21's directed test — the FCS cannot report this |
Row six is the one that has no diagnostic procedure and that is the point of the chapter. A frame with a compensated check produces no evidence anywhere, and the investigation that would find it does not exist — which is why the answer is a mechanism rather than a counter.
23. Misconceptions
Misconception 1 — "a valid FCS means the frame was not altered."
The wrong model: the check covers the frame, so a frame that passes it is the frame that was sent.
What it costs: an integrity claim with nothing behind it. Section 4: FCS(m ⊕ d) = FCS(m) ⊕ R₀(d), so the compensating value is one XOR of quantities that are all public — the polynomial is in the standard, the frame is on the wire, and the original check value is its last four octets.
The corrected model: a passing check is evidence that the covered octets arrived as they left the last append point — Chapter 5.8 §7's exact statement, which is about a hop and about noise. It is not evidence about origin, intent or authorship.
Misconception 2 — "a stronger CRC would fix it."
The wrong model: 32 bits is not many; a 64-bit check would resist alteration.
What it costs: a design that spends area on a wider detector and believes it bought protection. Section 2: linearity is a consequence of the mechanism being polynomial division, and it holds at every width. A 64-bit CRC's residual against a chosen difference is 1, exactly as a 32-bit one's is.
The corrected model: width changes the residual against noise and nothing else. Section 9's widening_helps is low whenever the threat is a chosen difference, and only_a_key_helps is the output that names the remedy.
Misconception 3 — "several checks are harder to defeat than one."
The wrong model: a length field, a payload checksum and a sequence number together make alteration impractical.
What it costs: linear work presented as multiplied protection. Section 9: compensation_steps equals the number of public checks. Three public checks cost three compensations to a party who read the same specification the design did.
The corrected model: checks do not accumulate; secrets do. The question at a design review is not how many checks a frame carries but whether any of them depends on something only the sender and receiver know.
Misconception 4 — "the residual is one in four billion, so the risk is negligible."
The wrong model: 2⁻³² is small, therefore the mechanism is adequate.
What it costs: a correct number applied to the wrong distribution. Section 6: at a 10⁻¹² frame error rate an undetected error arrives about once every 16.7 million years — enormous overkill against noise. Section 8: against a chosen difference the residual is 1, and the two numbers are not points on a scale.
The corrected model: Chapter 6.1 §6's residual assumes a corrupted frame's value is effectively arbitrary. An adversary's entire contribution is to make that assumption false, after which the number computed from it has no content at all.
Misconception 5 — "MACsec is too expensive."
The wrong model: cryptography at line rate is prohibitive in silicon.
What it costs: a decision made on the wrong column. Section 10: 407 200 GE at 400 Gb/s — 6.92× a CRC engine, or 5.07× with a compact S-box — which is a few hundredths of a square millimetre at a modern node, against a switch that spends 91% of its die on a buffer.
The corrected model: the gates are the affordable part. The real costs are the wire overhead at small frame sizes — 28.57% at 64 octets against 1.56% at 1 518 — and the key management, the hop-by-hop scope, and the counter hygiene that Section 10's callout lists.
Misconception 6 — "MACsec makes the network opaque, so we cannot use it."
The wrong model: protection and observability are mutually exclusive.
What it costs: rejecting the mechanism for a reason that is a configuration choice. Section 11: 802.1AE permits integrity WITHOUT confidentiality, the gate cost is identical, and every tap, analyser and classifier on the path keeps working — Chapter 21.9's entire apparatus continues to function.
The corrected model: the choice is an observability decision presented as a security level. And Section 13's production note runs the argument the other way: the packet number is in the clear on every protected frame, so a protected link is the only link in the fabric that carries the attributable loss counter Chapter 21.6 §13 established does not exist.
24. Interview Questions
Six, with what a strong answer contains.
1. Does a valid frame check sequence tell you the frame was not altered?
No, and the reason is one algebraic property. CRC-32 is polynomial division over GF(2), so it is linear: R(a ⊕ b) = R(a) ⊕ R(b). A strong answer gives the consequence rather than the identity: a frame m changed to m ⊕ d has check value FCS(m) ⊕ R₀(d), so the compensating value depends on the difference alone — and the polynomial, the frame and the original check value are all public. The best answers say what a passing check IS evidence of: the covered octets arrived as they left the last append point, one hop.
2. Would a 64-bit CRC be better?
Against noise, yes; against a chosen difference, not at all. Linearity is a property of polynomial division, not of this polynomial, so a 64-bit CRC compensates exactly as easily. A strong answer names the distinction: width changes the residual against an arbitrary difference and does nothing about a difference somebody selected, because the mechanism has no secret at any width.
3. What does the CRC actually guarantee?
Chapter 6.1 §5's list with its bounds: every burst up to 32 bits, every pattern of weight 3 at any frame length, weight 4 up to about 375 octets, and a residual of about 2⁻³² for everything else. A strong answer puts the residual in context — at a 10⁻¹² frame error rate that is one undetected error every 16.7 million years — and then makes the sharp point: every clause is a statement about the difference's shape, and none is a statement about its provenance.
4. How big is the gap to a real integrity mechanism?
It is not a gap on a scale. CRC's residual against a chosen difference is 1; GCM's forgery bound is 2⁻¹²⁸. A strong answer distinguishes that from the ratio people quote: 2⁻³² against 2⁻¹²⁸ is 2⁹⁶, and that compares two mechanisms each doing its own job — a detector against noise and an integrity check against an adversary. The 1 is the number that matters and no amount of widening moves it.
5. What does MACsec cost?
Three things in three units. Gates: 407 200 GE at 400 Gb/s against a 58 873-GE CRC engine — 6.92×. Wire: 24 or 32 octets, which is 28.57% at a 64-octet frame and 1.56% at 1 518. State: 288 bits per secure association — 73 728 BCE at four associations across 64 ports. A strong answer adds that the gates are the easy part and names what is not: key agreement, hop-by-hop scope, and separating the FCS and ICV counters so the integrity alarm stays actionable.
6. Why does a MACsec deployment keep the FCS at all?
Triage. The FCS is verified first and discards frames the channel corrupted before the cryptographic engine touches them. A strong answer explains why the order is load-bearing: verifying the ICV on a noisy frame fails correctly and indistinguishably from an attack, so a merged counter tracks the link's error rate and the alarm is disabled within a month. The cheap check's job in a protected deployment is to keep the expensive check's failure signal clean, which is the same relationship Chapter 7.4's address filter has with a host CPU.
25. Questions and Answers
26. What's Next
Three myths down and three to go, and the next one is a conservation argument rather than an algebraic one.
Chapter 25.4 takes "VLANs improve performance automatically". The kill is arithmetic that fits on one line: segmentation moves no bits. A switch with aggregate capacity C offered load L has the same C and the same L after it is divided into V VLANs — and the tag adds four octets, so the only bandwidth effect a VLAN has is negative: 4.76% of the wire at minimum frame size and 0.044% at jumbo.
What a VLAN does change is Chapter 12.4's flood width, and that chapter's amplification arithmetic gives the number: on a 64-port switch, one VLAN floods to 63 ports and eight VLANs of eight ports flood to 7 — 11.1% of the unsegmented width. That is a real and large improvement in a specific quantity.
And the chapter's uncomfortable result is about the trunk. Chapter 12.4 §10's domain arithmetic says each station's broadcast burden falls with the domain size — 315 frames per second at one VLAN, 35 at eight — but the trunk carrying all eight VLANs sees every one of them, and the total is 320 per second regardless of how the ports are divided. Segmentation redistributes a load it does not reduce, which is conservation again, one layer up.
Continue learning
Related tutorials
- Related topic
Frame Check Sequence
The check sequence protects a range, and the range is shorter than the frame's journey — appended at one point in a transmitter, verified at one point in the next receiver, and recomputed at every hop, so a device's own memory is covered by nothing the frame carries.
- Related topic
What Error Detection Must Guarantee
A detector's specification is a set of bounded guarantees plus a probability, and neither can be stated without an error model — including the guarantee everybody cites and this polynomial does not provide.
- Related topic
CRC-32 Generation
The arithmetic is a shift register performing polynomial division. The four conventions wrapped around it — initial value, input reflection, output reflection, final complement — change no guarantee and every value, which is where interoperability fails.
- Related topic
CRC Checking and the Residue
A receiver feeds the data and the check sequence through one division and tests for a fixed constant — no held value, no captured FCS, no need to know where the payload ended. And that constant is a fingerprint of all four conventions at once.
Standards & specifications
- Governing standard
- IEEE Std 802.3 (Ethernet)(opens IEEE in a new tab)
Defines the Ethernet MAC, the media-independent interfaces and the physical-layer sublayers, including framing, access control, auto-negotiation and per-rate PHY specifications. VLAN tagging, priority and time-sensitive shaping are defined by IEEE 802.1, not by 802.3.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the Ethernet curriculum.
