From 00e2704d0039ed9dbbfec54b2643da395f642f66 Mon Sep 17 00:00:00 2001 From: cyfraeviolae Date: Sat, 27 Aug 2022 02:55:45 -0400 Subject: key commitment --- aesgcmanalysis.py | 150 +++++++++++++++- app.py | 91 ++++++++-- static/axolotl.jpg | Bin 0 -> 70374 bytes static/kitten.bmp | Bin 0 -> 23258 bytes static/styles.css | 8 + templates/index.html | 30 ++-- templates/key-commitment.html | 245 +++++++++++++++++++++++++ templates/mac-truncation.html | 389 ++++++++++++++++++++++++++++++++++++++++ templates/nonce-reuse.html | 8 +- templates/nonce-truncation.html | 386 --------------------------------------- 10 files changed, 879 insertions(+), 428 deletions(-) create mode 100644 static/axolotl.jpg create mode 100644 static/kitten.bmp create mode 100644 templates/key-commitment.html create mode 100644 templates/mac-truncation.html delete mode 100644 templates/nonce-truncation.html diff --git a/aesgcmanalysis.py b/aesgcmanalysis.py index 259cf69..b833376 100644 --- a/aesgcmanalysis.py +++ b/aesgcmanalysis.py @@ -1,3 +1,4 @@ +from binascii import unhexlify import random, struct, hmac, itertools, math from Crypto.Cipher import AES import cryptography @@ -600,7 +601,7 @@ def nonce_reuse_recover_secrets(nonce, aad1, aad2, c1, c2, mac1, mac2): secrets.append((h, s)) return secrets -## Nonce Truncation Attack +## MAC Truncation Attack def gen_blocks(n, js): blocks = b'\x00'*16*n @@ -689,7 +690,7 @@ def find_b(n, basis, ct, mac, nonce, aad, oracle): base = orig_base.copy() idx += 1 -def nonce_truncation_recover_secrets(ct, mac, nonce, mac_bytes, aad, oracle, compute_T_once=False): +def mac_truncation_recover_secrets(ct, mac, nonce, mac_bytes, aad, oracle, compute_T_once=False): orig_ct = ct ct = aad + ct n = compute_n(ct) @@ -743,6 +744,134 @@ def nonce_truncation_recover_secrets(ct, mac, nonce, mac_bytes, aad, oracle, com s = (np.array(zero_tag) - np.array(mac_vec)) % 2 return vec_to_gf128(h), vec_to_gf128(s) +# Key Commitment Attack + +def gmac_blind(k, nonce): + return bytes_to_gf128(ecb_encrypt(k, nonce + b'\x00\x00\x00\x01')) +def encode_lengths(ad_length, ct_length): + return struct.pack('>QQ', ad_length*8, ct_length*8) +def collide(k1, k2, nonce, c): + h1 = gmac_key(k1) + h2 = gmac_key(k2) + p1 = gmac_blind(k1, nonce) + p2 = gmac_blind(k2, nonce) + assert len(c) % 16 == 0 + mlen = len(c)//16+1 + lens = bytes_to_gf128(encode_lengths(0, len(c) + 16)) + acc = gf128_mul(lens, gf128_add(h1, h2)) + acc = gf128_add(acc, gf128_add(p1, p2)) + for i in range(1, mlen): + if i % 2048 == 0: + print(i, end=', ') + hi = gf128_add(gf128_exp(h1, mlen+2-i), gf128_exp(h2, mlen+2-i)) + acc = gf128_add(acc, gf128_mul(bytes_to_gf128(c[(i-1)*16:((i-1)+1)*16]), hi)) + inv = gf128_inv(gf128_add(gf128_mul(h1, h1), gf128_mul(h2, h2))) + c_append = gf128_mul(acc, inv) + c_ = c + gf128_to_bytes(c_append) + mac = gmac(h1, p1, b'', c_) + return (c_, mac) + +def collide_penultimate(k1, k2, nonce, c): + h1 = authentication_key(k1) + h2 = authentication_key(k2) + p1 = gmac_blind(k1, nonce) + p2 = gmac_blind(k2, nonce) + assert len(c) % 16 == 0 + mlen = len(c)//16 + lens = bytes_to_gf128(encode_lengths(0, len(c))) + acc = gf128_mul(lens, gf128_add(h1, h2)) + acc = gf128_add(acc, gf128_add(p1, p2)) + n=4 + h1Running = gf128_exp(h1, 4) + h2Running = gf128_exp(h2, 4) + for i in reversed(range(0, mlen-2)): + # print(mlen+1-(i)) + # i = mlen-2-1-i + hi = gf128_add(h1Running, h2Running) + h1Running = gf128_mul(h1Running, h1) + h2Running = gf128_mul(h2Running, h2) + n+=1 + # hi = gf128_add(gf128_exp(h1, mlen+1-i), gf128_exp(h2, mlen+1-i)) + #print('block', i, pt[i*16:(i+1)*16], 'exp', mlen+1-i) + acc = gf128_add(acc, gf128_mul(bytes_to_gf128(c[i*16:(i+1)*16]), hi)) + # for i in range(0, mlen-2): + # print(i,mlen+1-i) + # hi = gf128_add(gf128_exp(h1, mlen+1-i), gf128_exp(h2, mlen+1-i)) + # acc = gf128_add(acc, gf128_mul(bytes_to_gf128(c[i*16:(i+1)*16]), hi)) + hi = gf128_add(gf128_exp(h1, 2), gf128_exp(h2, 2)) + i = mlen-1 + #print('block', i, pt[i*16:(i+1)*16], 'exp', 2) + acc = gf128_add(acc, gf128_mul(bytes_to_gf128(c[i*16:(i+1)*16]), hi)) + inv = gf128_inv(gf128_add(gf128_exp(h1, 3), gf128_exp(h2, 3))) + c_append = gf128_mul(acc, inv) + c_ = c[:-32] + gf128_to_bytes(c_append) + c[-16:] + mac = gmac(h1, p1, b'', c_) + return (c_, mac) + +def gctr_oneblock(k, pt, nonce): + enckeyval = nonce + b'\x00\x00\x00\x02' + stream = ecb_encrypt(k, enckeyval) + return xor(pt, stream) + +def key_search(nonce, init_bytes1, init_bytes2): + seen1 = dict() + seen2 = dict() + while True: + k1 = secrets.token_bytes(16) + k2 = secrets.token_bytes(16) + ct1 = gctr_oneblock(k1, init_bytes1, nonce) + ct2 = gctr_oneblock(k2, init_bytes2, nonce) + seen1[ct1] = k1 + seen2[ct2] = k2 + if ct1 in seen2: + return k1, seen2[ct1] + if ct2 in seen1: + return seen1[ct2], k2 + +def att_merge_jpg_bmp(jpg, bmp, aad): + # Precomputed with key_search; works for any files + k1 = unhexlify('5c3cb198432b0903e58de9c9647bd241') + k2 = unhexlify('df923ae8976230008a081d23205d7a4f') + nonce = b'JORGELBORGES' + + total_len = 6 + (0xff<<8) + 0xff + len(jpg) + jpgstream, _ = gcm_encrypt(k1, nonce, aad, b'\x00'*total_len) + bmpstream, _ = gcm_encrypt(k2, nonce, aad, b'\x00'*total_len) + print("enc") + + # 5 bytes + r = xor(jpgstream, b'\xff\xd8\xff\xfe\xff') + + # 1 byte + r += bmpstream[5:6] + + # len(bmp) bytes + bmpenc = xor(bmp[6:], bmpstream[6:6+len(bmp)]) + r += bmpenc + + comlen = (0xff << 8) + (jpgstream[5] ^ bmpstream[5]) + + # finish comment with padding + r += b'\x00'*(comlen - len(bmpenc)) + + # jpg + r += xor(jpg[2:-2], jpgstream[6+comlen:]) + + # comment; include penultimate block to be overwritten; therefore must be at least 3 blocks long + # also serves to block-align to 14 bytes so the final ciphertext will be complete blocks + + endcomlen = (28 - (len(r) % 16)) + 16 + 14 + + tail = b'\xff\xfe' + struct.pack('>H', endcomlen) + b'\x00'*endcomlen + b'\xff\xd9' + tailx = xor(tail, jpgstream[6+comlen+len(jpg)-4:]) + r += tailx + assert len(r) % 16 == 0 + print("collide") + + cfin, macfin = collide_penultimate(k1, k2, nonce, r) + + return cfin, macfin + # Demos def forbidden_attack_demo(): @@ -777,7 +906,7 @@ def forbidden_attack_demo(): pass assert succeeded -def nonce_truncation_demo(): +def mac_truncation_demo(): # Doesn't work with non-block size multiples. # Need to modify to consider padding, but we can't mess with the bits in the padding, # nor can we extend ad/ct unless we also change length block. @@ -795,10 +924,17 @@ def nonce_truncation_demo(): decryptor.authenticate_additional_data(aad) decryptor.update(base) + decryptor.finalize() - h, s = nonce_truncation_recover_secrets(ct, mac, nonce, mac_bytes, aad, oracle, compute_T_once=mac_bytes==1) + h, s = mac_truncation_recover_secrets(ct, mac, nonce, mac_bytes, aad, oracle, compute_T_once=mac_bytes==1) assert h == authentication_key(k) if __name__ == "__main__": - import resource - nonce_truncation_demo() - print(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) + pass + # mac_truncation_demo() + jpg = open('static/axolotl.jpg', 'rb').read() + bmp = open('static/kitten.bmp', 'rb').read() + c, mac = att_merge_jpg_bmp(jpg, bmp, aad=b"") + print(mac.hex()) + f = open('c.txt', 'wb') + f.write(c) + f.write(mac) + f.close() diff --git a/app.py b/app.py index 9aade4e..800b263 100644 --- a/app.py +++ b/app.py @@ -1,12 +1,14 @@ -import binascii, secrets +import binascii, secrets, io -from flask import Flask, render_template, request, redirect, url_for +from flask import Flask, render_template, request, redirect, url_for, send_file from flask_wtf import FlaskForm -from wtforms import StringField -from wtforms.validators import DataRequired, Length, ValidationError +from flask_wtf.file import FileField, FileAllowed +from werkzeug.datastructures import CombinedMultiDict +from wtforms import StringField, RadioField +from wtforms.validators import DataRequired, Length, ValidationError, InputRequired from Crypto.Cipher import AES -from aesgcmanalysis import xor, gmac, gcm_encrypt, nonce_reuse_recover_secrets, gf128_to_bytes, nonce_truncation_recover_secrets +from aesgcmanalysis import xor, gmac, gcm_encrypt, nonce_reuse_recover_secrets, gf128_to_bytes, mac_truncation_recover_secrets, att_merge_jpg_bmp app = Flask(__name__) @@ -72,14 +74,14 @@ def solve_nonce_reuse(k, nonce, m1, m2, mf): macs.append((gf128_to_bytes(h), s, mac)) return c_forged, macs -class NonceTruncationForm(FlaskForm): +class MACTruncationForm(FlaskForm): key = StringField('key', validators=[DataRequired(), Length(min=32, max=32), hex_check]) nonce = StringField('nonce', validators=[DataRequired(), Length(min=24, max=24), hex_check]) mf = StringField('forged message', validators=[DataRequired(), Length(min=1, max=64)]) -@app.route('/nonce-truncation', methods=['GET', 'POST']) -def nonce_truncation(): - form = NonceTruncationForm(meta={'csrf': False}) +@app.route('/mac-truncation', methods=['GET', 'POST']) +def mac_truncation(): + form = MACTruncationForm(meta={'csrf': False}) key = nonce = None mf = '' h = c_forged = mac = None @@ -88,11 +90,11 @@ def nonce_truncation(): if form.validate(): skey = binascii.unhexlify(key) snonce = binascii.unhexlify(nonce) - h, c_forged, mac = solve_nonce_truncation(skey, snonce, bytes(mf, 'utf-8')) - return render_template('nonce-truncation.html', form=form, key=key, nonce=nonce, + h, c_forged, mac = solve_mac_truncation(skey, snonce, bytes(mf, 'utf-8')) + return render_template('mac-truncation.html', form=form, key=key, nonce=nonce, mf=mf, h=h, c_forged=c_forged, mac=mac) -def solve_nonce_truncation(k, nonce, mf): +def solve_mac_truncation(k, nonce, mf): m = secrets.token_bytes(512) aad = b"" c, mac = gcm_encrypt(k, nonce, aad, m, mac_bytes=1) @@ -103,7 +105,70 @@ def solve_nonce_truncation(k, nonce, mf): cipher = AES.new(k, mode=AES.MODE_GCM, nonce=nonce, mac_len=1) cipher.update(aad) cipher.decrypt_and_verify(base, mac) - h, s = nonce_truncation_recover_secrets(c, mac, nonce, 1, aad, oracle) + h, s = mac_truncation_recover_secrets(c, mac, nonce, 1, aad, oracle) c_forged, aad_forged = xor(c, xor(m, mf)), b"" mac = gmac(h, s, aad_forged, c_forged) return gf128_to_bytes(h), c_forged, mac[:1] + +class RequiredIf(InputRequired): + # adapted from https://stackoverflow.com/a/8464478 + # a validator which makes a field required if + # another field is set and has a truthy value + + def __init__(self, other_field_name, other_field_val, *args, **kwargs): + self.other_field_name = other_field_name + self.other_field_val = other_field_val + super(RequiredIf, self).__init__(*args, **kwargs) + + def __call__(self, form, field): + other_field = form._fields.get(self.other_field_name) + if other_field is None: + raise Exception('no field named "%s" in form' % self.other_field_name) + if other_field.data == self.other_field_val: + super(RequiredIf, self).__call__(form, field) + +def FileSizeLimit(max_bytes, magic_start=None, magic_end=None): + # adapted from https://stackoverflow.com/a/67172432 + def file_length_check(form, field): + if not field.data: + return + s = field.data.read() + n = len(field.data.read()) + if n > max_bytes: + raise ValidationError(f"File size must be less than {max_bytes}B") + if magic_start and not s.startswith(magic_start): + raise ValidationError(f"Not a valid file; wrong initial magic bytes") + if magic_end and not s.endswith(magic_end): + raise ValidationError(f"Not a valid file; wrong final magic bytes") + field.data.seek(0) + return file_length_check + +class KeyCommitmentForm(FlaskForm): + mode = RadioField('mode', choices=[('sample', 'sample'), ('custom', 'custom')], validators=[DataRequired()]) + jpeg = FileField('jpeg_file', validators=[FileAllowed(['jpg', 'jpeg']), + RequiredIf('mode', 'custom'), FileSizeLimit(150000, b'\xff\xd8', b'\xff\xd9')]) + bmp = FileField('bmp_file', validators=[FileAllowed(['bmp']), + RequiredIf('mode', 'custom'), FileSizeLimit(50000, b'\x42\x4d')]) + +@app.route('/key-commitment', methods=['GET', 'POST']) +def key_commitment(): + form = KeyCommitmentForm(CombinedMultiDict((request.files, request.form)), meta={'csrf': False}) + k1 = None + k2 = None + nonce = None + c = None + + if form.is_submitted(): + if form.validate(): + if form.mode.data == 'sample': + jpeg_bytes = open('static/axolotl.jpg', 'rb').read() + bmp_bytes = open('static/kitten.bmp', 'rb').read() + else: + jpeg_bytes = form.jpeg.data.read() + bmp_bytes = form.bmp.data.read() + c, mac = att_merge_jpg_bmp(jpeg_bytes, bmp_bytes, aad=b"") + ct = c + mac + f = io.BytesIO(ct) + return send_file(f, mimetype='application/octet-stream', as_attachment=True, download_name="polyglot.enc") + + return render_template('key-commitment.html', form=form, k1=k1, k2=k2, nonce=nonce, c=c) diff --git a/static/axolotl.jpg b/static/axolotl.jpg new file mode 100644 index 0000000..dbda080 Binary files /dev/null and b/static/axolotl.jpg differ diff --git a/static/kitten.bmp b/static/kitten.bmp new file mode 100644 index 0000000..16b553b Binary files /dev/null and b/static/kitten.bmp differ diff --git a/static/styles.css b/static/styles.css index 9688050..5ea8a95 100644 --- a/static/styles.css +++ b/static/styles.css @@ -46,3 +46,11 @@ input[type="text"] { code { word-wrap: anywhere; } + +.responsive-img { + max-height: 200px; + max-width: 300px; + min-height: 150px; + /* width: 48%; */ + /* height: auto; */ +} diff --git a/templates/index.html b/templates/index.html index 1dbcc79..accbca7 100644 --- a/templates/index.html +++ b/templates/index.html @@ -18,13 +18,11 @@
source code · - nonce reuse + key commitment · - nonce truncation - + mac truncation

@@ -36,6 +34,14 @@

Choose one of the following missions.

+

+ Key commitment. One of + our agents has infiltrated Roseacrucis’ inner circle, but all + secret keys are required to be surrendered to the + counterintelligence authority. Help her send ciphertexts back to + the Library that decrypt to confidential information under one key, + but innocuous banter under another. +

Nonce reuse. Due to rising entropy prices, Roseacrucis has @@ -43,22 +49,12 @@ recover the authentication key and forge arbitrary ciphertext.

- Nonce + MAC truncation. The sorcerer aims to conserve - bandwidth by truncating nonces. Use the enemy as a decryption + bandwidth by truncating MACs. Use the enemy as a decryption oracle to once again, recover the authentication key and forge arbitrary ciphertext.

-
diff --git a/templates/key-commitment.html b/templates/key-commitment.html new file mode 100644 index 0000000..619f010 --- /dev/null +++ b/templates/key-commitment.html @@ -0,0 +1,245 @@ + + + + Forbidden Salamanders · Key Commitment + + + + + + + +
+ +

+ Key commitment. One of + our agents has infiltrated Roseacrucis’ inner circle, but all + secret keys are required to be surrendered to the + counterintelligence authority. Help her send ciphertexts back to + the Library that decrypt to confidential information under one key, + but innocuous banter under another. +

+
+ {% if form.errors %} +
+ Errors: +
    + {% for name, errors in form.errors.items() %} + {% for error in errors %} +
  • {{name}}: {{ error }}
  • + {% endfor %} + {% endfor %} +
+
+ {% endif %} +
+
+ The Library’s agent chooses two images: a JPEG file containing + confidential information, and a BMP file that looks innocuous. +

+ + +
+
+ + +
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ The agent now computes two keys, a nonce and constructs a single + ciphertext. When decrypted under the first key, it will look + identical to the JPEG file; when decrypted under the second + key, it will look identical to the BMP file. +
+ +

+ Key 1: 5c3cb198432b0903e58de9c9647bd241 +
+ Key 2: df923ae8976230008a081d23205d7a4f +
+ Nonce: 4a4f5247454c424f52474553 +

+
+ +
+ +
+
+
+
+ +
+
+

+ You can test your ciphertext with Go. Run the following in a shell + and then try opening first.jpg and second.bmp in an image viewer. +

+
+ + Show testing code. + +
+TEMP="$(mktemp).go"
+cat > "$TEMP" <<EOF
+package main
+import ("crypto/aes"; "crypto/cipher"; "encoding/hex"; "os")
+func main() {
+  var key, nonce, ciphertext, plaintext []byte; var block cipher.Block; var aesgcm cipher.AEAD; var err error
+  if len(os.Args) < 4 { panic("usage: go run salamander.go   ") }
+  if key, err = hex.DecodeString(os.Args[1]); err != nil { panic(err.Error()) }
+  if nonce, err = hex.DecodeString(os.Args[2]); err != nil { panic(err.Error()) }
+  if ciphertext, err = os.ReadFile(os.Args[3]); err != nil { panic(err.Error()) }
+  if block, err = aes.NewCipher(key); err != nil { panic(err.Error()) }
+  if aesgcm, err = cipher.NewGCM(block); err != nil { panic(err.Error()) }
+  if plaintext, err = aesgcm.Open(nil, nonce, ciphertext, nil); err != nil { panic(err.Error()) }
+  if _, err = os.Stdout.Write(plaintext); err != nil { panic(err.Error()) }
+}
+EOF
+go run "$TEMP" 5c3cb198432b0903e58de9c9647bd241 4a4f5247454c424f52474553 polyglot.enc > first.jpg
+go run "$TEMP" df923ae8976230008a081d23205d7a4f 4a4f5247454c424f52474553 polyglot.enc > second.bmp
+
+
+
+
+ + Attack outline. + +

+ Recall that the AES-GCM ciphertext is computed as the XOR of the + keystream and the message. One can modify the bits of the + ciphertext arbitrarily to effect the same change in the decrypted + plaintext. +

+

+ Where certain bits of the plaintext are already known, the attacker + can fully determine the same bits of the forged plaintext. If + nonces are reused, the keystream will be identical, allowing us to + recover plaintext via + + crib dragging, which makes this attack particularly effective: + \[ + c' = c \oplus m \oplus m'. + \] +

+

+ However, we still need to compute a new MAC over the forged ciphertext. + Simplifying for a ciphertext \(c\) of two blocks and no additional + authenticated data, the GMAC MAC is computed as + \[ + mac = s + \vert c\vert h + c_1h^2 + c_0h^3, + \] + where \(s\) is a constant depending on the AES-GCM key and the nonce, and \(h\) + is the authentication key depending only on the AES-GCM key. +

+

+ If we intercept a second ciphertext \(c'\) encrypted under the same key and nonce, + we can compute + \[ + mac + mac' = (s + s') + (len + len')h + (c_1 + c'_1)h^2 + (c_0+c'_0)h^3, + \] + Since \(s = s'\) and \(x+x=0\) in \(\mathbb{F}_{2^{128}}\), we are + left with the polynomial equation + \[ + 0 = (mac + mac') + (len + len')h + (c_1 + c'_1)h^2 + (c_0+c'_0)h^3 + \] + where all variables are known other than \(h\). Thus, recovering \(h\) + is a matter of finding the roots by factoring the + polynomial. +

+

+ We plug \(h\) back into the first equation to recover \(s\), and we + can forge the MAC for arbitary ciphertext under the same nonce. + Note that there may be multiple possible monomial roots; in this + case, one can check each possibility against the enemy. +

+

+ One can use SageMath to compute factors of a polynomial: +

+
+K = GF(2**128, name='x', modulus=x^128+x^7+x^2+x+1)
+x = K.gen()
+S = PolynomialRing(K, 'y')
+y = S.gen()
+p = (1)*y^4 + (x^7)*y^3 + (x^9 + x^4 + 1)*y^2 + (x^12 + x^2)*y + (x^10 + x^5)
+for factor, _ in p.factor():
+    if factor.degree() == 1:
+        print('Authentication key:', factor - y)
+

+ However, the library powering this demonstration implements polynomial factoring over finite fields from scratch, which is an edifying exercise. +

+

+ We present advice for those who wish to implement polynomial factorization as well: +

+
    +
  • The gcd of two polynomials is unique only up to multiplication by a non-zero constant because “greater” is defined for polynomials in terms of degree. When used in algorithms, gcd refers to the monic gcd, which is unique.
  • +
  • The inverse Frobenius automorphism (i.e., square root) in \(\mathbb{F}_{2^{128}}\) is given by \(\sqrt{x} = x^{2^{127}}\).
  • +
+

+ Readers who wish to implement this attack themselves can try + Cryptopals; specifically + Set 8 Problem 62. +

+
+
+ + Show me the code. + +
+from aesgcmanalysis import xor, gmac, gcm_encrypt, gcm_decrypt, nonce_reuse_recover_secrets
+
+k = b"tlonorbistertius"
+nonce = b"jorgelborges"
+m1, aad1 = b"The universe (which others call the Library)", b""
+m2, aad2 = b"From any of the hexagons one can see, interminably", b""
+
+c1, mac1 = gcm_encrypt(k, nonce, aad1, m1)
+c2, mac2 = gcm_encrypt(k, nonce, aad2, m2)
+
+# Recover the authentication key and blind from public information
+possible_secrets = nonce_reuse_recover_secrets(nonce, aad1, aad2, c1, c2, mac1, mac2)
+
+# Forge the ciphertext
+m_forged = b"As was natural, this inordinate hope"
+c_forged, aad_forged = xor(c1, xor(m1, m_forged)), b""
+
+for h, s in possible_secrets:
+    print("MAC candidate": gmac(h, s, aad_forged, c_forged))
+ + + + diff --git a/templates/mac-truncation.html b/templates/mac-truncation.html new file mode 100644 index 0000000..cf91149 --- /dev/null +++ b/templates/mac-truncation.html @@ -0,0 +1,389 @@ + + + + Forbidden Salamanders · MAC Truncation + + + + + + + +
+ +

+ MAC truncation. The sorcerer aims to conserve + bandwidth by truncating MACs. Use the enemy as a decryption + oracle to once again, recover the authentication key and forge + arbitrary ciphertext. +

+
+ {% if form.errors %} +
+ Errors: +
    + {% for name, errors in form.errors.items() %} + {% for error in errors %} +
  • {{name}}: {{ error }}
  • + {% endfor %} + {% endfor %} +
+
+ {% endif %} +
+
+ Roseacrucis chooses a key, a nonce, and a truncated MAC length; + then encrypts an arbitrary 512-byte length message. +

+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ After intercepting the ciphertext, you create new + specially-crafted messages and guess their corresponding MACs. + You send your guesses to Roseacrucis; whether he accepts gives + you enough information to recover the authentication key. +
+ +
+ Finally, you choose a new message to forge under the same key + and nonce. +

+ +
+ + +
+
+ +
+
+
+
+ +
+
+ {% if h %} +
+

+ Forged ciphertext: {{ c_forged.hex() }} +
+ Forged MAC: {{mac.hex()}} +
+ Authentication key: {{h.hex()}} +

+
+ {% endif %} +
+
+ + Attack outline. + +

+ Note that this attack (and library) should work for up to a MAC + length of 4 bytes, which is an allowed parameter by NIST and many + cryptography libraries. However, the attack is too slow to demonstrate + on the web: download the library to run it locally. +

+

+ Review the nonce reuse attack + to learn why recovering the authentication key is enough to forge MACs over + arbitrary ciphertexts. +

+

+ After intercepting a ciphertext and MAC, our initial goal is to compute + a variant of the ciphertext that has the same MAC. + Simplifying for a ciphertext \(c\) of four blocks and no additional + authenticated data, the GMAC MAC is computed as + \[ + mac = s + \vert c\vert{}h + c_3h^2 + c_2h^3 + c_1h^4 + c_0h^5, + \] + where \(s\) is a constant depending on the AES-GCM key and the nonce, and \(h\) + is the authentication key depending only on the AES-GCM key. +

+

+ Given a different ciphertext \(c'\) of the same length encrypted + with the same key and nonce, the difference in their MACs can be computed + as + \[ + mac-mac' = (c_3-c_3')h^2 + (c_2-c_2')h^3 + (c_1-c_1')h^4 + (c_0-c_0')h^5, + \] +

+

Let \(e\) be the difference between two MACs, and \(d_i\) be + the difference between two blocks at position \(i\): + \[ + e(d_i, h) = (d_3)h^2 + (d_2)h^3 + (d_1)h^4 + (d_0)h^5, + \] + We want to achieve \(e=0\) but with at least one \(d_i \not= 0\) in order + to obtain a different ciphertext with the same MAC. +

+

Linear Operations in \(\mathbb{K}=\mathbb{F}_{2^{128}}\)

+

+ As described in the mission home page, + each block of ciphertext, the authentication key \(h\), and the MAC are 16-byte + blocks that can be interpreted as elements of the finite field + \(\mathbb{K}=\mathbb{F}_{2^{128}}\). +

+

+ The elements of \(\mathbb{K}\) are usually represented as + polynomials with coefficients in \(\mathbb{F}_2\) of degree less + than 128, where multiplication is performed modulo an irreducible + polynomial given by the AES-GCM specification. This gives us a way + to multiply, add, and even divide two blocks. +

+

+ For this problem we will use an alternate representation: a + 128-length bit vector, where the \(i\)th bit of the vector + represents the coefficient of \(\alpha^i\) in the polynomial + representation. +

+

+ The transformation \(f(a) = ca\) for \(c, a \in \mathbb{K}\) is + linear; thus, it can be represented as matrix \(M_c\). We set each + column to the transformation by \(f\) of the basis vectors \(1, \alpha, + \alpha^2, \ldots\): + \[ + M_c = \begin{bmatrix} + c & c\alpha & c\alpha^2 & \ldots & c\alpha^{127} + \end{bmatrix}. + \] +

+

+ The squaring operation \(g(a) = a^2\) is also linear: since \(2 = 0 \in \mathbb{K}\), + \[(a+b)^2 = a^2 + 2ab + b^2 = a^2 + b^2,\] + and for \(k \in \mathbb{F}_2\), + \[(ka)^2 = k^2a^2 = ka^2. \] +

+

+ Thus we can construct + \[ + S = \begin{bmatrix} + 1^2 & \alpha^2 & (\alpha^2)^2 & \ldots & (\alpha^{127})^2 + \end{bmatrix}, + \] + and \(g(a) = a^2\) can alternately be written \(g'(a) = Sa\), interpreting \(a\) + as a vector. +

+

Reframing the Problem

+

+ We can now rewrite our equation for \(e\) in terms of matrices and vectors: + \[ + e(d_i, h) = M_{d_3}h^2 + M_{d_2}h^3 + M_{d_1}h^4 + M_{d_0}h^5, + \] + after which we replace \(h^{2^i}\) by \(S^ih\): + \[ + e(d_i, h) = M_{d_3}Sh + M_{d_2}h^3 + M_{d_1}S^2h + M_{d_0}h^5. + \] +

+

+ To ensure that \(e\) will be a linear transformation on \(h\), we set + \(d_i = 0\) if the corresponding \(h^j\) term does not have \(j\) as + a power of 2, resulting in + \[ + e(d_i, h) = M_{d_3}Sh + M_{d_1}S^2h = (M_{d_3}S + M_{d_1}S^2)h + \] +

+

+ Let \(d^*\) represent the concatenation of all the remaining \(d_i\)s. + The length of \(d^*\) will be logarithmic in the size of the original + ciphertext as we only consider the power-of-2-indexed blocks. +

+

+ \(A_{d^*}\) represents the action on \(h\) by \(d^*\): + \[ + A_{d^*} = (M_{d_3}S + M_{d_1}S^2) + \] + \[ + e(d^*, h) = A_{d^*}h + \] +

+

+ In order for a full forgery, we need \(e=0\), but if the MAC length + is reduced to \(N\) bits, we only need the first \(N\) bits of + \(e\) to be zero, rather than all 128 bits. This will be satisfied if the first \(N\) + rows of \(A_{d^*}\) are all zero, regardless of \(h\). In + practice, zeroing out all \(N\) rows will be too difficult, so we + settle for zeroing out \(M \lt N\) rows instead. +

+ The remaining \(N-M\) relevant bits of \(e\) will be random, but we + shall see it will be small enough to brute force. +

+

Zeroing Out Rows of \(A_{d^*}\)

+

+ Changing the bits of \(d^*\) effects a linear change on the + bits of \(A_{d^*}\). Consider a set of \(8\vert d^* \vert\) “basis + vectors” for \(d^*\) (one for each bit), the \(i\)th basis + vector having a 1 in the \(i\)th position and 0 everywhere else. +

+

+ For each basis vector, compute the corresponding \(A_{d^*}\). Concatenate + the first \(M\) rows into a column vector of a dependency matrix \(T\). + Thus, \(T\) will have \(8\vert d^*\vert\) columns and \(128M\) rows. +

+

+ We want to compute some \(d^*\) that result in the first + \(M\) rows of \(A_{d^*}\) equaling zero, which is equivalent to saying + \[ Td^* = 0. \] +

+

+ The solution space is given by the null space (or kernel) of + \(T\). Note that we need \(T\) to have more columns than rows for + the matrix to be linearly dependent and thus have a non-trivial + kernel. +

+

+ The vectors of \(\ker T\) each represents a potential \(d^*\) + that sends the first \(M\) rows of \(A_{d^*}\) to zero. Any linear + combination of the vectors of the kernel will have the same effect. +

+

Executing the Attack

+

+ Consider a random linear combination of \(\ker T\). Since these + are difference vectors, they can be thought of as specifying + bit flips of the original ciphertext at the appropriate positions + (remember to leave the non-power-of-2 blocks alone). +

+

+ By design, the first \(M\) bits of \(e\) will be zero, meaning that + the first \(M\) bits of the MAC of the modified ciphertext will + equal the original MAC. The remaining bits of the MAC will match + with \(\frac{1}{2^{N-M}}\) probability. +

+

+ Say the MAC length is \(N=32\) bits, and we let \(M=16\) (this requires + intercepting a ciphertext of length \(2^{16+1}\)). We can compute + random linear combinations of \(\ker T\), sending the modified ciphertexts + and original MAC to Roseacrucis. If they accept (which they should after + roughly \(2^{16}=65536\) attempts), we've succeeded in a forgery. +

+

+ In addition to the forgery, the acceptance gives us + information on \(h\): for the successful \(d^*\), since + \[ e(d^*, h)[0:N-1] = 0 = A_{d^*}[0:N-1]h \] + we know that \(h\) is in the kernel of the first \(N\) rows of + \(A_{d^*}\). The first \(M\) rows of \(A_{d^*}\) are zero, + but the next \(N-M\) rows are likely linearly independent rows. If + we put these rows into a new matrix \(K\), we have \[ 0 = Kh. \] + The dimensions of \(K\) are \((N-M, 128) = (16, 128)\). The kernel + of such a matrix is 112-dimensional by the rank-nullity theorem, + which is not enough to guess \(h\) yet. +

+

+ However, each additional forgery (with good probability) gives us + more linearly independent vectors to add into \(K\). Once we + collect 127 linearly independent vectors (there can be no more + since we know the kernel is non-trivial), the kernel of \(K\) will be 1-dimensional, + and the only vector in the kernel will be \(h\). +

+

Speeding up the Attack

+

+ Once we have our first successful forgery, we have \(K\) such that + \(h \in \ker K\), meaning that some linear combination + of basis vectors of \(\ker K\) equals \(h\). Let \(X\) be + a basis set for \(K\), and so write + \[ h = Xh'. \] + We don't know \(h'\), but it is a 112-dimensional vector. + Now rewrite our equation for \(e\): + \[ e(A_{d^*}, h) = A_{d^*}h = A_{d^*}(Xh') = (A_{d^*}X)h', \] + where \(A_{d^*}X\) is a matrix of dimensions 128 by 112. +

+

+ When we construct the corresponding dependency matrix \(T\), + we still have \(8\vert d^*\vert\) columns, but each row + only takes 112 bits to zero out rather than 128. This lets us + set more rows to zero, which in turn gives us a better chance of + succeeding in the next \( d^*\) forgery. +

+

+ We can continue in this fashion, each step getting more and more + efficient until we collect 127 vectors in \(K\). Remember to leave + at least one relevant row of \(A_{d^*}\) to be non-zero; otherwise, the + forgery will succeed but won't tell us any more information about + \(h\). +

+

+ To complete the attack, one can recover the first \(N\) rows of \(s\) + and compute a forged MAC for arbitrary ciphertext under the same nonce + as in the nonce reuse attack. +

+

Addendum

+

+ This attack was first shown by Dutch cryptographer Niels Ferguson + in his paper Authentication weaknesses in GCM. + He notes that a (then-)competing mode, CWC, avoids this attack by + encrypting the GMAC polynomial with the block cipher before adding + \(s\). This breaks the linear relationship between the ciphertext + and the MAC. +

+

+ Readers who wish to implement this attack themselves can try + Cryptopals; specifically + Set 8 Problem 64. +

+
+
+ + Show me the code. + +
+from aesgcmanalysis import xor, gmac, gcm_encrypt, mac_truncation_recover_secrets
+from Crypto.Cipher import AES
+
+k = b"tlonorbistertius"
+mac_bytes = 4
+m, aad, = b"yellow_submarine"*(2**17) = b""
+nonce = b"jorgelborges"
+c, mac = gcm_encrypt(k, nonce, aad, m, mac_bytes=MACBYTES)
+def oracle(c, aad, mac, nonce):
+	cipher = AES.new(k, mode=AES.MODE_GCM, nonce=nonce, mac_len=mac_bytes)
+	cipher.update(aad)
+	cipher.decrypt_and_verify(c, mac)
+
+h, s = mac_truncation_recover_secrets(c, mac, nonce, mac_bytes, aad, oracle)
+
+m_forged = b"As was natural, this inordinate hope"
+c_forged, aad_forged = xor(c, xor(m, m_forged)), b""
+mac_forged = gmac(h, s, aad_forged, c_forged)
+ + + + + diff --git a/templates/nonce-reuse.html b/templates/nonce-reuse.html index 94dcb2c..249d00e 100644 --- a/templates/nonce-reuse.html +++ b/templates/nonce-reuse.html @@ -18,13 +18,11 @@

diff --git a/templates/nonce-truncation.html b/templates/nonce-truncation.html deleted file mode 100644 index 0095bb1..0000000 --- a/templates/nonce-truncation.html +++ /dev/null @@ -1,386 +0,0 @@ - - - - Forbidden Salamanders · Nonce Truncation - - - - - - - -

- -

- Nonce - truncation. The sorcerer aims to conserve - bandwidth by truncating nonces. Use the enemy as a decryption - oracle to once again, recover the authentication key and forge - arbitrary ciphertext. -

-
- {% if form.errors %} -
- Errors: -
    - {% for name, errors in form.errors.items() %} - {% for error in errors %} -
  • {{name}}: {{ error }}
  • - {% endfor %} - {% endfor %} -
-
- {% endif %} -
-
- Roseacrucis chooses a key, a nonce, and a truncated MAC length; - then encrypts an arbitrary 512-byte length message. -

- -
- - -
- -
- - -
- -
- - - -
- -
- After intercepting the ciphertext, you create new - specially-crafted messages and guess their corresponding MACs. - You send your guesses to Roseacrucis; whether he accepts gives - you enough information to recover the authentication key. -
- -
- Finally, you choose a new message to forge under the same key - and nonce. -

- -
- - -
-
- -
-
-
-
- -
-
- {% if h %} -
-

- Forged ciphertext: {{ c_forged.hex() }} -
- Forged MAC: {{mac.hex()}} -
- Authentication key: {{h.hex()}} -

-
- {% endif %} -
-
- - Attack outline. - -

- Review the nonce reuse attack - to learn why recovering the authentication key is enough to forge MACs over - arbitrary ciphertexts. -

-

- After intercepting a ciphertext and MAC, our initial goal is to compute - a variant of the ciphertext that has the same MAC. - Simplifying for a ciphertext \(c\) of four blocks and no additional - authenticated data, the GMAC MAC is computed as - \[ - mac = s + \vert c\vert{}h + c_3h^2 + c_2h^3 + c_1h^4 + c_0h^5, - \] - where \(s\) is a constant depending on the AES-GCM key and the nonce, and \(h\) - is the authentication key depending only on the AES-GCM key. -

-

- Given a different ciphertext \(c'\) of the same length encrypted - with the same key and nonce, the difference in their MACs can be computed - as - \[ - mac-mac' = (c_3-c_3')h^2 + (c_2-c_2')h^3 + (c_1-c_1')h^4 + (c_0-c_0')h^5, - \] -

-

Let \(e\) be the difference between two MACs, and \(d_i\) be - the difference between two blocks at position \(i\): - \[ - e(d_i, h) = (d_3)h^2 + (d_2)h^3 + (d_1)h^4 + (d_0)h^5, - \] - We want to achieve \(e=0\) but with at least one \(d_i \not= 0\) in order - to obtain a different ciphertext with the same MAC. -

-

Linear Operations in \(\mathbb{K}=\mathbb{F}_{2^{128}}\)

-

- As described in the mission home page, - each block of ciphertext, the authentication key \(h\), and the MAC are 16-byte - blocks that can be interpreted as elements of the finite field - \(\mathbb{K}=\mathbb{F}_{2^{128}}\). -

-

- The elements of \(\mathbb{K}\) are usually represented as - polynomials with coefficients in \(\mathbb{F}_2\) of degree less - than 128, where multiplication is performed modulo an irreducible - polynomial given by the AES-GCM specification. This gives us a way - to multiply, add, and even divide two blocks. -

-

- For this problem we will use an alternate representation: a - 128-length bit vector, where the \(i\)th bit of the vector - represents the coefficient of \(\alpha^i\) in the polynomial - representation. -

-

- The transformation \(f(a) = ca\) for \(c, a \in \mathbb{K}\) is - linear; thus, it can be represented as matrix \(M_c\). We set each - column to the transformation by \(f\) of the basis vectors \(1, \alpha, - \alpha^2, \ldots\): - \[ - M_c = \begin{bmatrix} - c & c\alpha & c\alpha^2 & \ldots & c\alpha^{127} - \end{bmatrix}. - \] -

-

- The squaring operation \(g(a) = a^2\) is also linear: since \(2 = 0 \in \mathbb{K}\), - \[(a+b)^2 = a^2 + 2ab + b^2 = a^2 + b^2,\] - and for \(k \in \mathbb{F}_2\), - \[(ka)^2 = k^2a^2 = ka^2. \] -

-

- Thus we can construct - \[ - S = \begin{bmatrix} - 1^2 & \alpha^2 & (\alpha^2)^2 & \ldots & (\alpha^{127})^2 - \end{bmatrix}, - \] - and \(g(a) = a^2\) can alternately be written \(g'(a) = Sa\), interpreting \(a\) - as a vector. -

-

Reframing the Problem

-

- We can now rewrite our equation for \(e\) in terms of matrices and vectors: - \[ - e(d_i, h) = M_{d_3}h^2 + M_{d_2}h^3 + M_{d_1}h^4 + M_{d_0}h^5, - \] - after which we replace \(h^{2^i}\) by \(S^ih\): - \[ - e(d_i, h) = M_{d_3}Sh + M_{d_2}h^3 + M_{d_1}S^2h + M_{d_0}h^5. - \] -

-

- To ensure that \(e\) will be a linear transformation on \(h\), we set - \(d_i = 0\) if the corresponding \(h^j\) term does not have \(j\) as - a power of 2, resulting in - \[ - e(d_i, h) = M_{d_3}Sh + M_{d_1}S^2h = (M_{d_3}S + M_{d_1}S^2)h - \] -

-

- Let \(d^*\) represent the concatenation of all the remaining \(d_i\)s. - The length of \(d^*\) will be logarithmic in the size of the original - ciphertext as we only consider the power-of-2-indexed blocks. -

-

- \(A_{d^*}\) represents the action on \(h\) by \(d^*\): - \[ - A_{d^*} = (M_{d_3}S + M_{d_1}S^2) - \] - \[ - e(d^*, h) = A_{d^*}h - \] -

-

- In order for a full forgery, we need \(e=0\), but if the MAC length - is reduced to \(N\) bits, we only need the first \(N\) bits of - \(e\) to be zero, rather than all 128 bits. This will be satisfied if the first \(N\) - rows of \(A_{d^*}\) are all zero, regardless of \(h\). In - practice, zeroing out all \(N\) rows will be too difficult, so we - settle for zeroing out \(M \lt N\) rows instead. -

- The remaining \(N-M\) relevant bits of \(e\) will be random, but we - shall see it will be small enough to brute force. -

-

Zeroing Out Rows of \(A_{d^*}\)

-

- Changing the bits of \(d^*\) effects a linear change on the - bits of \(A_{d^*}\). Consider a set of \(8\vert d^* \vert\) “basis - vectors” for \(d^*\) (one for each bit), the \(i\)th basis - vector having a 1 in the \(i\)th position and 0 everywhere else. -

-

- For each basis vector, compute the corresponding \(A_{d^*}\). Concatenate - the first \(M\) rows into a column vector of a dependency matrix \(T\). - Thus, \(T\) will have \(8\vert d^*\vert\) columns and \(128M\) rows. -

-

- We want to compute some \(d^*\) that result in the first - \(M\) rows of \(A_{d^*}\) equaling zero, which is equivalent to saying - \[ Td^* = 0. \] -

-

- The solution space is given by the null space (or kernel) of - \(T\). Note that we need \(T\) to have more columns than rows for - the matrix to be linearly dependent and thus have a non-trivial - kernel. -

-

- The vectors of \(\ker T\) each represents a potential \(d^*\) - that sends the first \(M\) rows of \(A_{d^*}\) to zero. Any linear - combination of the vectors of the kernel will have the same effect. -

-

Executing the Attack

-

- Consider a random linear combination of \(\ker T\). Since these - are difference vectors, they can be thought of as specifying - bit flips of the original ciphertext at the appropriate positions - (remember to leave the non-power-of-2 blocks alone). -

-

- By design, the first \(M\) bits of \(e\) will be zero, meaning that - the first \(M\) bits of the MAC of the modified ciphertext will - equal the original MAC. The remaining bits of the MAC will match - with \(\frac{1}{2^{N-M}}\) probability. -

-

- Say the MAC length is \(N=32\) bits, and we let \(M=16\) (this requires - intercepting a ciphertext of length \(2^{16+1}\)). We can compute - random linear combinations of \(\ker T\), sending the modified ciphertexts - and original MAC to Roseacrucis. If they accept (which they should after - roughly \(2^{16}=65536\) attempts), we've succeeded in a forgery. -

-

- In addition to the forgery, the acceptance gives us - information on \(h\): for the successful \(d^*\), since - \[ e(d^*, h)[0:N-1] = 0 = A_{d^*}[0:N-1]h \] - we know that \(h\) is in the kernel of the first \(N\) rows of - \(A_{d^*}\). The first \(M\) rows of \(A_{d^*}\) are zero, - but the next \(N-M\) rows are likely linearly independent rows. If - we put these rows into a new matrix \(K\), we have \[ 0 = Kh. \] - The dimensions of \(K\) are \((N-M, 128) = (16, 128)\). The kernel - of such a matrix is 112-dimensional by the rank-nullity theorem, - which is not enough to guess \(h\) yet. -

-

- However, each additional forgery (with good probability) gives us - more linearly independent vectors to add into \(K\). Once we - collect 127 linearly independent vectors (there can be no more - since we know the kernel is non-trivial), the kernel of \(K\) will be 1-dimensional, - and the only vector in the kernel will be \(h\). -

-

Speeding up the Attack

-

- Once we have our first successful forgery, we have \(K\) such that - \(h \in \ker K\), meaning that some linear combination - of basis vectors of \(\ker K\) equals \(h\). Let \(X\) be - a basis set for \(K\), and so write - \[ h = Xh'. \] - We don't know \(h'\), but it is a 112-dimensional vector. - Now rewrite our equation for \(e\): - \[ e(A_{d^*}, h) = A_{d^*}h = A_{d^*}(Xh') = (A_{d^*}X)h', \] - where \(A_{d^*}X\) is a matrix of dimensions 128 by 112. -

-

- When we construct the corresponding dependency matrix \(T\), - we still have \(8\vert d^*\vert\) columns, but each row - only takes 112 bits to zero out rather than 128. This lets us - set more rows to zero, which in turn gives us a better chance of - succeeding in the next \( d^*\) forgery. -

-

- We can continue in this fashion, each step getting more and more - efficient until we collect 127 vectors in \(K\). Remember to leave - at least one relevant row of \(A_{d^*}\) to be non-zero; otherwise, the - forgery will succeed but won't tell us any more information about - \(h\). -

-

- To complete the attack, one can recover the first \(N\) rows of \(s\) - and compute a forged MAC for arbitrary ciphertext under the same nonce - as in the nonce reuse attack. -

-

Addendum

-

- This attack was first shown by Dutch cryptographer Niels Ferguson - in his paper Authentication weaknesses in GCM. - He notes that a (then-)competing mode, CWC, avoids this attack by - encrypting the GMAC polynomial with the block cipher before adding - \(s\). This breaks the linear relationship between the ciphertext - and the MAC. -

-

- Readers who wish to implement this attack themselves can try - Cryptopals; specifically - Set 8 Problem 64. -

-
-
- - Show me the code. - -
-from aesgcmanalysis import xor, gmac, gcm_encrypt, nonce_truncation_recover_secrets
-from Crypto.Cipher import AES
-
-k = b"tlonorbistertius"
-mac_bytes = 4
-m, aad, = b"yellow_submarine"*(2**17) = b""
-nonce = b"jorgelborges"
-c, mac = gcm_encrypt(k, nonce, aad, m, mac_bytes=MACBYTES)
-def oracle(c, aad, mac, nonce):
-	cipher = AES.new(k, mode=AES.MODE_GCM, nonce=nonce, mac_len=mac_bytes)
-	cipher.update(aad)
-	cipher.decrypt_and_verify(c, mac)
-
-h, s = nonce_truncation_recover_secrets(c, mac, nonce, mac_bytes, aad, oracle)
-
-m_forged = b"As was natural, this inordinate hope"
-c_forged, aad_forged = xor(c, xor(m, m_forged)), b""
-mac_forged = gmac(h, s, aad_forged, c_forged)
- - - - - -- cgit v1.2.3