Skip to content

Commit c4ea5f6

Browse files
stratosphersipa
andcommitted
[test/crypto] Add RFC 8439's ChaCha20Poly1305 AEAD
Co-authored-by: Pieter Wuille <pieter.wuille@gmail.com>
1 parent 9fc6e03 commit c4ea5f6

File tree

2 files changed

+131
-0
lines changed

2 files changed

+131
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2022 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
"""Test-only implementation of ChaCha20 Poly1305 AEAD Construction in RFC 8439
7+
8+
It is designed for ease of understanding, not performance.
9+
10+
WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for
11+
anything but tests.
12+
"""
13+
14+
import unittest
15+
16+
from .chacha20 import chacha20_block
17+
from .poly1305 import Poly1305
18+
19+
20+
def pad16(x):
21+
if len(x) % 16 == 0:
22+
return b''
23+
return b'\x00' * (16 - (len(x) % 16))
24+
25+
26+
def aead_chacha20_poly1305_encrypt(key, nonce, aad, plaintext):
27+
"""Encrypt a plaintext using ChaCha20Poly1305."""
28+
ret = bytearray()
29+
msg_len = len(plaintext)
30+
for i in range((msg_len + 63) // 64):
31+
now = min(64, msg_len - 64 * i)
32+
keystream = chacha20_block(key, nonce, i + 1)
33+
for j in range(now):
34+
ret.append(plaintext[j + 64 * i] ^ keystream[j])
35+
poly1305 = Poly1305(chacha20_block(key, nonce, 0)[:32])
36+
mac_data = aad + pad16(aad)
37+
mac_data += ret + pad16(ret)
38+
mac_data += len(aad).to_bytes(8, 'little') + msg_len.to_bytes(8, 'little')
39+
ret += poly1305.tag(mac_data)
40+
return bytes(ret)
41+
42+
43+
def aead_chacha20_poly1305_decrypt(key, nonce, aad, ciphertext):
44+
"""Decrypt a ChaCha20Poly1305 ciphertext."""
45+
if len(ciphertext) < 16:
46+
return None
47+
msg_len = len(ciphertext) - 16
48+
poly1305 = Poly1305(chacha20_block(key, nonce, 0)[:32])
49+
mac_data = aad + pad16(aad)
50+
mac_data += ciphertext[:-16] + pad16(ciphertext[:-16])
51+
mac_data += len(aad).to_bytes(8, 'little') + msg_len.to_bytes(8, 'little')
52+
if ciphertext[-16:] != poly1305.tag(mac_data):
53+
return None
54+
ret = bytearray()
55+
for i in range((msg_len + 63) // 64):
56+
now = min(64, msg_len - 64 * i)
57+
keystream = chacha20_block(key, nonce, i + 1)
58+
for j in range(now):
59+
ret.append(ciphertext[j + 64 * i] ^ keystream[j])
60+
return bytes(ret)
61+
62+
63+
# Test vectors from RFC8439 consisting of plaintext, aad, 32 byte key, 12 byte nonce and ciphertext
64+
AEAD_TESTS = [
65+
# RFC 8439 Example from section 2.8.2
66+
["4c616469657320616e642047656e746c656d656e206f662074686520636c6173"
67+
"73206f66202739393a204966204920636f756c64206f6666657220796f75206f"
68+
"6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73"
69+
"637265656e20776f756c642062652069742e",
70+
"50515253c0c1c2c3c4c5c6c7",
71+
"808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
72+
[7, 0x4746454443424140],
73+
"d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6"
74+
"3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36"
75+
"92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc"
76+
"3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd060"
77+
"0691"],
78+
# RFC 8439 Test vector A.5
79+
["496e7465726e65742d4472616674732061726520647261667420646f63756d65"
80+
"6e74732076616c696420666f722061206d6178696d756d206f6620736978206d"
81+
"6f6e74687320616e64206d617920626520757064617465642c207265706c6163"
82+
"65642c206f72206f62736f6c65746564206279206f7468657220646f63756d65"
83+
"6e747320617420616e792074696d652e20497420697320696e617070726f7072"
84+
"6961746520746f2075736520496e7465726e65742d4472616674732061732072"
85+
"65666572656e6365206d6174657269616c206f7220746f206369746520746865"
86+
"6d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67"
87+
"726573732e2fe2809d",
88+
"f33388860000000000004e91",
89+
"1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0",
90+
[0, 0x0807060504030201],
91+
"64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb2"
92+
"4c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf"
93+
"332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c855"
94+
"9797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4"
95+
"b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523e"
96+
"af4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a"
97+
"0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a10"
98+
"49e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29"
99+
"a6ad5cb4022b02709beead9d67890cbb22392336fea1851f38"],
100+
# Test vectors exercising aad and plaintext which are multiples of 16 bytes.
101+
["8d2d6a8befd9716fab35819eaac83b33269afb9f1a00fddf66095a6c0cd91951"
102+
"a6b7ad3db580be0674c3f0b55f618e34",
103+
"",
104+
"72ddc73f07101282bbbcf853b9012a9f9695fc5d36b303a97fd0845d0314e0c3",
105+
[0x3432b75f, 0xb3585537eb7f4024],
106+
"f760b8224fb2a317b1b07875092606131232a5b86ae142df5df1c846a7f6341a"
107+
"f2564483dd77f836be45e6230808ffe402a6f0a3e8be074b3d1f4ea8a7b09451"],
108+
["",
109+
"36970d8a704c065de16250c18033de5a400520ac1b5842b24551e5823a3314f3"
110+
"946285171e04a81ebfbe3566e312e74ab80e94c7dd2ff4e10de0098a58d0f503",
111+
"77adda51d6730b9ad6c995658cbd49f581b2547e7c0c08fcc24ceec797461021",
112+
[0x1f90da88, 0x75dafa3ef84471a4],
113+
"aaae5bb81e8407c94b2ae86ae0c7efbe"],
114+
]
115+
116+
117+
class TestFrameworkAEAD(unittest.TestCase):
118+
def test_aead(self):
119+
"""ChaCha20Poly1305 AEAD test vectors."""
120+
for test_vector in AEAD_TESTS:
121+
hex_plain, hex_aad, hex_key, hex_nonce, hex_cipher = test_vector
122+
plain = bytes.fromhex(hex_plain)
123+
aad = bytes.fromhex(hex_aad)
124+
key = bytes.fromhex(hex_key)
125+
nonce = hex_nonce[0].to_bytes(4, 'little') + hex_nonce[1].to_bytes(8, 'little')
126+
127+
ciphertext = aead_chacha20_poly1305_encrypt(key, nonce, aad, plain)
128+
self.assertEqual(hex_cipher, ciphertext.hex())
129+
plaintext = aead_chacha20_poly1305_decrypt(key, nonce, aad, ciphertext)
130+
self.assertEqual(plain, plaintext)

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
# the output of `git grep unittest.TestCase ./test/functional/test_framework`
7474
TEST_FRAMEWORK_MODULES = [
7575
"address",
76+
"crypto.bip324_cipher",
7677
"blocktools",
7778
"crypto.chacha20",
7879
"crypto.ellswift",

0 commit comments

Comments
 (0)