@@ -63,13 +63,32 @@ [new Claim("tenant", "acme", ClaimValueTypes.String, "https://issuer.example")],
6363 public void Rejects_a_tampered_ticket ( )
6464 {
6565 var protector = new TicketProtector ( Key ) ;
66- var value = protector . Protect ( Ticket ( ) ) ;
6766
68- // Flip one character of the payload, past the version and key id.
69- var chars = value . ToCharArray ( ) ;
70- chars [ ^ 1 ] = chars [ ^ 1 ] == 'A' ? 'B' : 'A' ;
67+ Assert . True ( Base64Url . TryDecode ( protector . Protect ( Ticket ( ) ) , out var payload ) ) ;
7168
72- Assert . Null ( protector . Unprotect ( new string ( chars ) ) ) ;
69+ // Flip a bit of the ciphertext — past the version, key id, nonce and tag. Tampering with a
70+ // byte rather than a character of the encoding is the point: base64url packs 6 bits per
71+ // character, so when the payload length is not a multiple of three the final character
72+ // carries unused low bits that decode to nothing. Editing *that* character leaves the bytes
73+ // identical, the ticket verifies, and the test fails — which it did, intermittently, because
74+ // the payload length moves with the timestamps in the ticket.
75+ payload [ ^ 1 ] ^= 0x01 ;
76+
77+ Assert . Null ( protector . Unprotect ( Base64Url . Encode ( payload ) ) ) ;
78+ }
79+
80+ /// <summary>The tag is the integrity check, so an edit there has to be refused just as flatly.</summary>
81+ [ Fact ]
82+ public void Rejects_a_ticket_with_a_tampered_tag ( )
83+ {
84+ var protector = new TicketProtector ( Key ) ;
85+
86+ Assert . True ( Base64Url . TryDecode ( protector . Protect ( Ticket ( ) ) , out var payload ) ) ;
87+
88+ // version (1) + key id (4) + nonce (12) — the first byte of the 16-byte tag.
89+ payload [ 17 ] ^= 0x01 ;
90+
91+ Assert . Null ( protector . Unprotect ( Base64Url . Encode ( payload ) ) ) ;
7392 }
7493
7594 [ Fact ]
0 commit comments