#import "@preview/ichigo:0.1.0": config, prob #import "@preview/algorithmic:0.1.0" #import algorithmic: algorithm #import "@preview/fletcher:0.5.1" as fletcher: diagram, node, edge #import fletcher.shapes: house, hexagon #let blob(pos, label, tint: white, ..args) = node( pos, align(center, label), width: 26mm, fill: tint.lighten(60%), stroke: 1pt + tint.darken(20%), corner-radius: 5pt, ..args, ) #show: config.with( course-name: "SMART CARDS & NFC", serial-str: "k12104785", author-info: [ Lukas Heiligenbrunner ], author-names: "Lukas Heiligenbrunner", ) #prob[ #figure( image("Screenshot From 2024-10-21 14-07-41.png", width: 80%), caption: [ Pin try counter. ], ) ][ - The PIN_TRY_COUNTER is prone to turn off attacks. Each time the chip resets the ram value of the counter is cleared and one gets basically infinite retries. Solution: - Store counter in non-volatile memory eg. EEPROM, flash, eMMC. - Store counter in a secure server backend. - Depending on the implementation of the comparison operation, it might leak side-channel information. For example, if the comparison is done byte-wise, the attacker can determine the correct byte by comparing the time it takes to compare the bytes. Solution: - Implement a constant time comparison operation. - PIN_TRY_COUNTER is incremented after the comparison operation. If the operation is interrupted due to a non atomic operation, the counter not incremented. Solution: - Do counter++ before comparison. This way the attacker can't determine if the counter is incremented or not. #sym.arrow.r See flowchart - Implement a atomic operation for the counter incrementation. #align(center)[ #diagram( spacing: 8pt, cell-size: (8mm, 10mm), edge-stroke: 1pt, edge-corner-radius: 5pt, mark-scale: 70%, debug: false, blob((2,0), [PIN Verification], tint: yellow, shape: fletcher.shapes.pill), edge("-|>"), blob((2,1), [PIN_Try_Counter < LIMIT], tint: green, shape: fletcher.shapes.hexagon, width: 35mm), edge("ll,dddd", "-|>", label: "No"), edge("-|>", "d"), blob((0,5), [Result: Card/Pin blocked], tint: yellow, shape: fletcher.shapes.pill), blob((2,2), [Pin_try_counter++], tint: blue, shape: fletcher.shapes.rect, width: auto), edge("-|>"), blob((2,3), [PIN == Ref_PIN?], tint: green, shape: fletcher.shapes.hexagon, width: auto), edge("l,d", "-|>", label: "Yes"), edge( "r,dd", "-|>", label: "No"), blob((1,4), [Pin_try_counter = 0], tint: blue, shape: fletcher.shapes.rect, width: auto), edge("-|>"), blob((1,5), [PIN Verification], tint: yellow, shape: fletcher.shapes.pill), blob((3,5), [PIN Verification], tint: yellow, shape: fletcher.shapes.pill), ) ] ] #prob[ #figure( image("Screenshot From 2024-10-21 14-10-15.png", width: 80%), caption: [ Pin comparision (PIN == REF_PIN). ], ) ][ - The comparison of the entered pin and the reference pin is array entry wise. If a entry doesn't match the comparison is short-handed and the function returns no match. This is prone to a timing side-channel attack. If a pin digit matches the comparison takes longer than if it doesn't. Solution: - Implement a constant time comparison operation. (no comparison shorthand) For example: #algorithm({ import algorithmic: * Function("Constant-Time-Compare", args: ("PIN", "Ref_PIN"), { Cmt[Check if lengths are equal] If(cond: $"length" ("PIN") != "length"("Ref_PIN")$, { Return[false] }) State[] Cmt[Initialize result variable to 0] Assign[$"result"$][$0$] State[] Cmt[Loop through each character in PIN and Ref_PIN] For(cond: [$i=0$; $i < "length"("PIN") - 1$], { Cmt[XOR corresponding characters and accumulate result] Assign[$"result"$][$"result" or ("PIN"[i] xor "Ref_PIN"[i])$] }) State[] Cmt[Return true if result is 0, else false] Return[$"result" == 0$] }) }) ]