commit df6844010948a5a9e18096615a4b3b8ed3ac4fbc
parent 5032e8d3d90b428faacfc23f31781dd7c714d497
Author: Armin Preiml <apreiml@strohwolke.at>
Date: Tue, 23 May 2023 16:30:54 +0200
add crypto::chachapoly
Implements the AEAD stream ciphers Chacha20-Poly1305 and
XChacha20-Poly1305 with a similar interface as the GCM cipher mode.
Signed-off-by: Armin Preiml <apreiml@strohwolke.at>
Diffstat:
5 files changed, 419 insertions(+), 0 deletions(-)
diff --git a/crypto/chachapoly/README b/crypto/chachapoly/README
@@ -0,0 +1,18 @@
+This module provides Chacha20-Poly1305 and XChacha20-Poly1305 stream
+implementations as described in RFC 8439.
+
+A stream is created with [[chachapoly]]. [[init]] initializes a stream as a
+Chacha20-Poly1305 one where [[xinit]] will initialize it as a
+XChacha20-Poly1305 stream. After initializiation data can be encrypted by
+writing to or decrypted by reading from the stream. The user must call [[seal]]
+when encryption is done to create the authentication tag and [[verify]] in case
+of decryption to check if the dercypted data is valid. If the data is invalid
+it must not be processed further.
+
+This is a low-level module which implements cryptographic primitives. Direct use
+of cryptographic primitives is not recommended for non-experts, as incorrect use
+of these primitives can easily lead to the introduction of security
+vulnerabilities. Non-experts are advised to use the high-level operations
+available in the top-level [[crypto]] module.
+
+Be advised that Hare's cryptography implementations have not been audited.
diff --git a/crypto/chachapoly/chachapoly.ha b/crypto/chachapoly/chachapoly.ha
@@ -0,0 +1,175 @@
+// License: MPL-2.0
+// (c) 2023 Armin Preiml <apreiml@strohwolke.at>
+use bufio;
+use bytes;
+use crypto::chacha;
+use crypto::mac;
+use crypto::math;
+use crypto::poly1305;
+use endian;
+use errors;
+use io;
+use types;
+
+// Nonce size as required by [[init]].
+export def NONCESZ: size = chacha::NONCESIZE;
+
+// Nonce size as required by [[xinit]].
+export def XNONCESZ: size = chacha::XNONCESIZE;
+
+// Key size
+export def KEYSZ: size = chacha::KEYSIZE;
+
+// Tag size
+export def TAGSZ: size = poly1305::SIZE;
+
+export type stream = struct {
+ stream: io::stream,
+ h: io::teestream,
+ c: chacha::stream,
+ p: poly1305::state,
+ adsz: size,
+ msgsz: size,
+};
+
+// Create a stream that must be initialised by [[init]] or [[xinit]]. The user
+// must call [[io::close]] when they are done using the stream to securly erase
+// secret information stored in the stream state.
+export fn chachapoly() stream = {
+ return stream {
+ stream = &vtable,
+ c = chacha::chacha20(),
+ p = poly1305::poly1305(),
+ ...
+ };
+};
+
+const vtable: io::vtable = io::vtable {
+ writer = &writer,
+ reader = &reader,
+ closer = &closer,
+ ...
+};
+
+type initfunc = fn (s: *chacha::stream, h: io::handle, k: []u8, n: []u8) void;
+
+// Initialises the stream as Chacha20-Poly1305. Encrypts to or decrypts form
+// 'h'. 'nonce' must be a random value that will only be used once. Additional
+// data can be passed as 'ad'.
+export fn init(
+ s: *stream,
+ h: io::handle,
+ key: const []u8,
+ nonce: const []u8,
+ ad: const []u8...,
+) void = geninit(s, &chacha::chacha20_init, h, key, nonce, ad...);
+
+// Initialise the stream as XChacha20-Poly1305. Encrypts to or decrypts form
+// 'h'. 'nonce' must be a random value that will only be used once. Additional
+// data can be passed as 'ad'.
+export fn xinit(
+ s: *stream,
+ h: io::handle,
+ key: const []u8,
+ nonce: const []u8,
+ ad: const []u8...,
+) void = geninit(s, &chacha::xchacha20_init, h, key, nonce, ad...);
+
+fn geninit(
+ s: *stream,
+ finit: *initfunc,
+ h: io::handle,
+ key: const []u8,
+ nonce: const []u8,
+ ad: const []u8...,
+) void = {
+ assert(len(ad) <= types::U32_MAX);
+
+ let otk: poly1305::key = [0...];
+ defer bytes::zero(otk);
+
+ let otkbuf = bufio::fixed(otk, io::mode::WRITE);
+ finit(&s.c, &otkbuf, key, nonce);
+ io::writeall(&s.c, otk[..])!;
+
+ poly1305::init(&s.p, &otk);
+
+ s.adsz = 0z;
+ for (let i = 0z; i < len(ad); i += 1) {
+ s.adsz += len(ad[i]);
+ mac::write(&s.p, ad[i]);
+ };
+ polypad(&s.p, s.adsz);
+
+ s.h = io::tee(h, &s.p);
+ finit(&s.c, &s.h, key, nonce);
+ chacha::setctr(&s.c, 1);
+
+ s.msgsz = 0;
+};
+
+fn polypad(p: *poly1305::state, n: size) void = {
+ if (n % poly1305::BLOCKSIZE == 0) {
+ return;
+ };
+
+ const pad: [poly1305::BLOCKSIZE]u8 = [0...];
+ const padlen = poly1305::BLOCKSIZE - (n % poly1305::BLOCKSIZE);
+ mac::write(p, pad[..padlen]);
+};
+
+// Finishes encryption and writes the authentication code to 'tag'. After
+// calling seal, the user must not write any more data to the stream.
+export fn seal(s: *stream, tag: []u8) void = writemac(s, tag);
+
+fn writemac(s: *stream, tag: []u8) void = {
+ assert(len(tag) == TAGSZ);
+ assert(s.msgsz <= types::U32_MAX);
+
+ polypad(&s.p, s.msgsz);
+
+ let nbuf: [8]u8 = [0...];
+ endian::leputu64(nbuf, s.adsz: u32);
+ mac::write(&s.p, nbuf);
+
+ endian::leputu64(nbuf, s.msgsz: u32);
+ mac::write(&s.p, nbuf);
+
+ mac::sum(&s.p, tag[..]);
+};
+
+// Verifies the authentication tag against the decrypted data. Must be called
+// after reading all data from the stream to ensure that the data was not
+// modified. If the data was modified, [[errors::invalid]] will be returned and
+// the data must not be trusted.
+export fn verify(s: *stream, tag: []u8) (void | errors::invalid) = {
+ let ntag: [TAGSZ]u8 = [0...];
+ writemac(s, ntag);
+ if (crypto::math::eqslice(ntag, tag) == 0) {
+ return errors::invalid;
+ };
+};
+
+fn writer(s: *io::stream, buf: const []u8) (size | io::error) = {
+ let s = s: *stream;
+ const n = io::write(&s.c, buf)?;
+ s.msgsz += n;
+ return n;
+};
+
+fn reader(s: *io::stream, buf: []u8) (size | io::EOF | io::error) = {
+ let s = s: *stream;
+ match (io::read(&s.c, buf)?) {
+ case io::EOF =>
+ return io::EOF;
+ case let n: size =>
+ s.msgsz += n;
+ return n;
+ };
+};
+
+fn closer(s: *io::stream) (void | io::error) = {
+ let s = s: *stream;
+ io::close(&s.c)!;
+ mac::finish(&s.p);
+};
diff --git a/crypto/chachapoly/encryption+test.ha b/crypto/chachapoly/encryption+test.ha
@@ -0,0 +1,181 @@
+// License: MPL-2.0
+// (c) 2023 Armin Preiml <apreiml@strohwolke.at>
+use bufio;
+use bytes;
+use io;
+
+@test fn encrypt() void = {
+ let plain: [_]u8 = [
+ 0x4c, 0x61, 0x64, 0x69, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x64,
+ 0x20, 0x47, 0x65, 0x6e, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e,
+ 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c,
+ 0x61, 0x73, 0x73, 0x20, 0x6f, 0x66, 0x20, 0x27, 0x39, 0x39,
+ 0x3a, 0x20, 0x49, 0x66, 0x20, 0x49, 0x20, 0x63, 0x6f, 0x75,
+ 0x6c, 0x64, 0x20, 0x6f, 0x66, 0x66, 0x65, 0x72, 0x20, 0x79,
+ 0x6f, 0x75, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x6f, 0x6e,
+ 0x65, 0x20, 0x74, 0x69, 0x70, 0x20, 0x66, 0x6f, 0x72, 0x20,
+ 0x74, 0x68, 0x65, 0x20, 0x66, 0x75, 0x74, 0x75, 0x72, 0x65,
+ 0x2c, 0x20, 0x73, 0x75, 0x6e, 0x73, 0x63, 0x72, 0x65, 0x65,
+ 0x6e, 0x20, 0x77, 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x62, 0x65,
+ 0x20, 0x69, 0x74, 0x2e
+ ];
+
+ let ad: [_]u8 = [
+ 0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5,
+ 0xc6, 0xc7,
+ ];
+
+ let key: [_]u8 = [
+ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
+ 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93,
+ 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d,
+ 0x9e, 0x9f,
+ ];
+
+ let nonce: [_]u8 = [
+ 0x07, 0x00, 0x00, 0x00, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45,
+ 0x46, 0x47,
+ ];
+
+ let cipher: [_]u8 = [
+ 0xd3, 0x1a, 0x8d, 0x34, 0x64, 0x8e, 0x60, 0xdb, 0x7b, 0x86,
+ 0xaf, 0xbc, 0x53, 0xef, 0x7e, 0xc2, 0xa4, 0xad, 0xed, 0x51,
+ 0x29, 0x6e, 0x08, 0xfe, 0xa9, 0xe2, 0xb5, 0xa7, 0x36, 0xee,
+ 0x62, 0xd6, 0x3d, 0xbe, 0xa4, 0x5e, 0x8c, 0xa9, 0x67, 0x12,
+ 0x82, 0xfa, 0xfb, 0x69, 0xda, 0x92, 0x72, 0x8b, 0x1a, 0x71,
+ 0xde, 0x0a, 0x9e, 0x06, 0x0b, 0x29, 0x05, 0xd6, 0xa5, 0xb6,
+ 0x7e, 0xcd, 0x3b, 0x36, 0x92, 0xdd, 0xbd, 0x7f, 0x2d, 0x77,
+ 0x8b, 0x8c, 0x98, 0x03, 0xae, 0xe3, 0x28, 0x09, 0x1b, 0x58,
+ 0xfa, 0xb3, 0x24, 0xe4, 0xfa, 0xd6, 0x75, 0x94, 0x55, 0x85,
+ 0x80, 0x8b, 0x48, 0x31, 0xd7, 0xbc, 0x3f, 0xf4, 0xde, 0xf0,
+ 0x8e, 0x4b, 0x7a, 0x9d, 0xe5, 0x76, 0xd2, 0x65, 0x86, 0xce,
+ 0xc6, 0x4b, 0x61, 0x16,
+ ];
+
+ let tag: [_]u8 = [
+ 0x1a, 0xe1, 0x0b, 0x59, 0x4f, 0x09, 0xe2, 0x6a, 0x7e, 0x90,
+ 0x2e, 0xcb, 0xd0, 0x60, 0x06, 0x91,
+ ];
+
+ let out = bufio::dynamic(io::mode::RDWR);
+ defer io::close(&out)!;
+
+ let s = chachapoly();
+ init(&s, &out, key, nonce, ad);
+ io::writeall(&s, plain)!;
+
+ let outtag: [TAGSZ]u8 = [0...];
+ seal(&s, outtag);
+
+ let outbuf = bufio::buffer(&out);
+ assert(bytes::equal(outbuf, cipher));
+ assert(bytes::equal(outtag, tag));
+
+ let out = bufio::dynamic(io::mode::RDWR);
+ defer io::close(&out)!;
+ let in = bufio::fixed(cipher, io::mode::READ);
+
+ let s = chachapoly();
+ init(&s, &in, key, nonce, ad);
+ io::copy(&out, &s)!;
+
+ verify(&s, tag)!;
+
+ let outbuf = bufio::buffer(&out);
+ assert(bytes::equal(outbuf, plain));
+
+ io::close(&s)!;
+};
+
+type sample = struct {
+ msg: []u8,
+ cipher: []u8,
+ additional: []u8,
+ key: []u8,
+ nonce: []u8,
+ mac: []u8,
+};
+
+// test vector taken from the XChacha20-Poly1305-AEAD draft rfc
+const rfcsample: sample = sample {
+ msg = [
+ 0x4c, 0x61, 0x64, 0x69, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x64,
+ 0x20, 0x47, 0x65, 0x6e, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e,
+ 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c,
+ 0x61, 0x73, 0x73, 0x20, 0x6f, 0x66, 0x20, 0x27, 0x39, 0x39,
+ 0x3a, 0x20, 0x49, 0x66, 0x20, 0x49, 0x20, 0x63, 0x6f, 0x75,
+ 0x6c, 0x64, 0x20, 0x6f, 0x66, 0x66, 0x65, 0x72, 0x20, 0x79,
+ 0x6f, 0x75, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x6f, 0x6e,
+ 0x65, 0x20, 0x74, 0x69, 0x70, 0x20, 0x66, 0x6f, 0x72, 0x20,
+ 0x74, 0x68, 0x65, 0x20, 0x66, 0x75, 0x74, 0x75, 0x72, 0x65,
+ 0x2c, 0x20, 0x73, 0x75, 0x6e, 0x73, 0x63, 0x72, 0x65, 0x65,
+ 0x6e, 0x20, 0x77, 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x62, 0x65,
+ 0x20, 0x69, 0x74, 0x2e,
+ ],
+ additional = [
+ 0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5,
+ 0xc6, 0xc7,
+ ],
+ key = [
+ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
+ 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93,
+ 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d,
+ 0x9e, 0x9f,
+ ],
+ nonce = [
+ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
+ 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53,
+ 0x54, 0x55, 0x56, 0x57,
+ ],
+ cipher = [
+ 0xbd, 0x6d, 0x17, 0x9d, 0x3e, 0x83, 0xd4, 0x3b, 0x95, 0x76,
+ 0x57, 0x94, 0x93, 0xc0, 0xe9, 0x39, 0x57, 0x2a, 0x17, 0x00,
+ 0x25, 0x2b, 0xfa, 0xcc, 0xbe, 0xd2, 0x90, 0x2c, 0x21, 0x39,
+ 0x6c, 0xbb, 0x73, 0x1c, 0x7f, 0x1b, 0x0b, 0x4a, 0xa6, 0x44,
+ 0x0b, 0xf3, 0xa8, 0x2f, 0x4e, 0xda, 0x7e, 0x39, 0xae, 0x64,
+ 0xc6, 0x70, 0x8c, 0x54, 0xc2, 0x16, 0xcb, 0x96, 0xb7, 0x2e,
+ 0x12, 0x13, 0xb4, 0x52, 0x2f, 0x8c, 0x9b, 0xa4, 0x0d, 0xb5,
+ 0xd9, 0x45, 0xb1, 0x1b, 0x69, 0xb9, 0x82, 0xc1, 0xbb, 0x9e,
+ 0x3f, 0x3f, 0xac, 0x2b, 0xc3, 0x69, 0x48, 0x8f, 0x76, 0xb2,
+ 0x38, 0x35, 0x65, 0xd3, 0xff, 0xf9, 0x21, 0xf9, 0x66, 0x4c,
+ 0x97, 0x63, 0x7d, 0xa9, 0x76, 0x88, 0x12, 0xf6, 0x15, 0xc6,
+ 0x8b, 0x13, 0xb5, 0x2e,
+ ],
+ mac = [
+ 0xc0, 0x87, 0x59, 0x24, 0xc1, 0xc7, 0x98, 0x79, 0x47, 0xde,
+ 0xaf, 0xd8, 0x78, 0x0a, 0xcf, 0x49,
+ ],
+};
+
+@test fn xencrypt() void = {
+ let tc = rfcsample;
+
+ let out = bufio::dynamic(io::mode::RDWR);
+ defer io::close(&out)!;
+
+ let s = chachapoly();
+ xinit(&s, &out, tc.key, tc.nonce, tc.additional);
+ io::writeall(&s, tc.msg)!;
+
+ let outtag: [TAGSZ]u8 = [0...];
+ seal(&s, outtag);
+
+ let outbuf = bufio::buffer(&out);
+ assert(bytes::equal(outbuf, tc.cipher));
+ assert(bytes::equal(outtag, tc.mac));
+
+ let out = bufio::dynamic(io::mode::RDWR);
+ defer io::close(&out)!;
+ let in = bufio::fixed(tc.cipher, io::mode::READ);
+
+ let s = chachapoly();
+ xinit(&s, &in, tc.key, tc.nonce, tc.additional);
+ io::copy(&out, &s)!;
+
+ verify(&s, tc.mac)!;
+
+ let outbuf = bufio::buffer(&out);
+ assert(bytes::equal(outbuf, tc.msg));
+
+ io::close(&s)!;
+};
diff --git a/scripts/gen-stdlib b/scripts/gen-stdlib
@@ -341,6 +341,17 @@ crypto_chacha() {
fi
}
+crypto_chachapoly() {
+ if [ $testing -eq 0 ]
+ then
+ gen_srcs crypto::chachapoly chachapoly.ha
+ else
+ gen_srcs crypto::chachapoly chachapoly.ha encryption+test.ha
+ fi
+ gen_ssa crypto::chachapoly bufio bytes crypto::chacha crypto::mac \
+ crypto::math crypto::poly1305 endian errors io types
+}
+
crypto_cipher() {
gen_srcs crypto::cipher \
cipher.ha \
@@ -1551,6 +1562,7 @@ crypto::blake2b
crypto::blowfish
crypto::bigint
crypto::chacha
+crypto::chachapoly
crypto::cipher
crypto::hkdf
crypto::hmac
diff --git a/stdlib.mk b/stdlib.mk
@@ -217,6 +217,12 @@ stdlib_deps_any += $(stdlib_crypto_chacha_any)
stdlib_crypto_chacha_linux = $(stdlib_crypto_chacha_any)
stdlib_crypto_chacha_freebsd = $(stdlib_crypto_chacha_any)
+# gen_lib crypto::chachapoly (any)
+stdlib_crypto_chachapoly_any = $(HARECACHE)/crypto/chachapoly/crypto_chachapoly-any.o
+stdlib_deps_any += $(stdlib_crypto_chachapoly_any)
+stdlib_crypto_chachapoly_linux = $(stdlib_crypto_chachapoly_any)
+stdlib_crypto_chachapoly_freebsd = $(stdlib_crypto_chachapoly_any)
+
# gen_lib crypto::cipher (any)
stdlib_crypto_cipher_any = $(HARECACHE)/crypto/cipher/crypto_cipher-any.o
stdlib_env += HARE_TD_crypto::cipher=$(HARECACHE)/crypto/cipher/crypto_cipher.td
@@ -1025,6 +1031,16 @@ $(HARECACHE)/crypto/chacha/crypto_chacha-any.ssa: $(stdlib_crypto_chacha_any_src
@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Ncrypto::chacha \
-t$(HARECACHE)/crypto/chacha/crypto_chacha.td $(stdlib_crypto_chacha_any_srcs)
+# crypto::chachapoly (+any)
+stdlib_crypto_chachapoly_any_srcs = \
+ $(STDLIB)/crypto/chachapoly/chachapoly.ha
+
+$(HARECACHE)/crypto/chachapoly/crypto_chachapoly-any.ssa: $(stdlib_crypto_chachapoly_any_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_crypto_chacha_$(PLATFORM)) $(stdlib_crypto_mac_$(PLATFORM)) $(stdlib_crypto_math_$(PLATFORM)) $(stdlib_crypto_poly1305_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_types_$(PLATFORM))
+ @printf 'HAREC \t$@\n'
+ @mkdir -p $(HARECACHE)/crypto/chachapoly
+ @HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Ncrypto::chachapoly \
+ -t$(HARECACHE)/crypto/chachapoly/crypto_chachapoly.td $(stdlib_crypto_chachapoly_any_srcs)
+
# crypto::cipher (+any)
stdlib_crypto_cipher_any_srcs = \
$(STDLIB)/crypto/cipher/cipher.ha \
@@ -2608,6 +2624,12 @@ testlib_deps_any += $(testlib_crypto_chacha_any)
testlib_crypto_chacha_linux = $(testlib_crypto_chacha_any)
testlib_crypto_chacha_freebsd = $(testlib_crypto_chacha_any)
+# gen_lib crypto::chachapoly (any)
+testlib_crypto_chachapoly_any = $(TESTCACHE)/crypto/chachapoly/crypto_chachapoly-any.o
+testlib_deps_any += $(testlib_crypto_chachapoly_any)
+testlib_crypto_chachapoly_linux = $(testlib_crypto_chachapoly_any)
+testlib_crypto_chachapoly_freebsd = $(testlib_crypto_chachapoly_any)
+
# gen_lib crypto::cipher (any)
testlib_crypto_cipher_any = $(TESTCACHE)/crypto/cipher/crypto_cipher-any.o
testlib_env += HARE_TD_crypto::cipher=$(TESTCACHE)/crypto/cipher/crypto_cipher.td
@@ -3433,6 +3455,17 @@ $(TESTCACHE)/crypto/chacha/crypto_chacha-any.ssa: $(testlib_crypto_chacha_any_sr
@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncrypto::chacha \
-t$(TESTCACHE)/crypto/chacha/crypto_chacha.td $(testlib_crypto_chacha_any_srcs)
+# crypto::chachapoly (+any)
+testlib_crypto_chachapoly_any_srcs = \
+ $(STDLIB)/crypto/chachapoly/chachapoly.ha \
+ $(STDLIB)/crypto/chachapoly/encryption+test.ha
+
+$(TESTCACHE)/crypto/chachapoly/crypto_chachapoly-any.ssa: $(testlib_crypto_chachapoly_any_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_crypto_chacha_$(PLATFORM)) $(testlib_crypto_mac_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_crypto_poly1305_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_types_$(PLATFORM))
+ @printf 'HAREC \t$@\n'
+ @mkdir -p $(TESTCACHE)/crypto/chachapoly
+ @HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncrypto::chachapoly \
+ -t$(TESTCACHE)/crypto/chachapoly/crypto_chachapoly.td $(testlib_crypto_chachapoly_any_srcs)
+
# crypto::cipher (+any)
testlib_crypto_cipher_any_srcs = \
$(STDLIB)/crypto/cipher/cipher.ha \