diff --git a/Makefile.inc b/Makefile.inc index aebe6b1..94c57a3 100644 --- a/Makefile.inc +++ b/Makefile.inc @@ -3,6 +3,7 @@ LC_SRCS+= auth.c auth_hmac.c auth_poly1305.c LC_SRCS+= cipher.c cipher_chacha20.c LC_SRCS+= hash.c hash_sha224_sha256.c hash_sha384_sha512.c LC_SRCS+= impl_chacha20.c impl_poly1305.c impl_sha256.c impl_sha512.c +LC_SRCS+= kdf.c kdf_hkdf.c LC_SRCS+= ct.c util.c WARNINGS= Yes diff --git a/README b/README index 5ae6dce..9f62e7b 100644 --- a/README +++ b/README @@ -52,6 +52,12 @@ ECC - [ ] Ed25519 (EdDSA) - [ ] X25519 (ECDH) +KDF +--- + +- [x] HKDF + + Nice-to-haves ============= @@ -85,8 +91,3 @@ AEAD - [ ] Camellia-GCM - [ ] Salsa20-Poly1305 (no Wycherproof test vector suite) - [ ] XSalsa20-Poly1305 (no Wycherproof test vector suite) - -KDF ---- - -- [ ] HKDF diff --git a/internal.h b/internal.h index f8c53de..4895723 100644 --- a/internal.h +++ b/internal.h @@ -27,6 +27,7 @@ /* Authentitcation. */ #define HMAC_BLOCKLEN_MAX LC_SHA512_BLOCKLEN +#define HMAC_HASHLEN_MAX LC_SHA512_HASHLEN #define POLY1305_TAGLEN_WORDS (LC_POLY1305_TAGLEN / sizeof(uint32_t)) @@ -108,6 +109,12 @@ struct lc_hash_impl { size_t hashlen; }; +struct lc_kdf_impl { + int (*kdf)(uint8_t *, size_t *, void *, size_t); + + size_t argsz; +}; + /* * *_ctx binds an *_impl with an state, effectively representing an instance of a diff --git a/kdf.c b/kdf.c new file mode 100644 index 0000000..2bc740e --- /dev/null +++ b/kdf.c @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 Lucas Gabriel Vuotto + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "internal.h" + + +int +lc_kdf(const struct lc_kdf_impl *impl, uint8_t *out, size_t *outlen, + void *initargs, size_t len) +{ + return impl->kdf(out, outlen, initargs, len); +} diff --git a/kdf_hkdf.c b/kdf_hkdf.c new file mode 100644 index 0000000..54d6736 --- /dev/null +++ b/kdf_hkdf.c @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2024 Lucas Gabriel Vuotto + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include "internal.h" + + +static uint8_t zeros[HMAC_HASHLEN_MAX]; + + +static int +hkdf_kdf(uint8_t *out, size_t *outlen, void *initparams, size_t len) +{ + struct lc_hkdf_params *params = initparams; + struct lc_hmac_params hmacparams; + uint8_t prk[HMAC_HASHLEN_MAX]; + uint8_t t[HMAC_HASHLEN_MAX], tn[HMAC_HASHLEN_MAX]; + size_t hashlen, olen; + uint8_t ctr; + + /* Only accept HMAC as auth_impl. */ + if (params->hmac == NULL || + params->hmac->impl != lc_auth_impl_hmac()) + return 0; + + hashlen = params->hash->impl->hashlen; + + /* XXX *outlen = 0 ? */ + if (len > hashlen * 255) + return 0; + + if (out == NULL) { + *outlen = len; + return 1; + } + *outlen = 0; + + hmacparams.hash = params->hash; + if (params->saltlen == 0) { + hmacparams.key = zeros; + hmacparams.keylen = hashlen; + } else { + hmacparams.key = params->salt; + hmacparams.keylen = params->saltlen; + } + + if (!lc_auth(params->hmac->impl, prk, &olen, &hmacparams, params->ikm, + params->ikmlen)) + return 0; + + hmacparams.key = prk; + hmacparams.keylen = olen; + olen = 0; + ctr = 1; + while (len >= hashlen) { + memcpy(t, tn, olen); + if (!lc_auth_init(params->hmac, &hmacparams) || + !lc_auth_update(params->hmac, t, olen) || + !lc_auth_update(params->hmac, params->info, + params->infolen) || + !lc_auth_update(params->hmac, &ctr, sizeof(ctr)) || + !lc_auth_final(params->hmac, tn, &olen)) + return 0; + ctr++; + + memcpy(out, tn, hashlen); + *outlen += hashlen; + out += hashlen; + len -= hashlen; + } + + memcpy(t, tn, olen); + if (!lc_auth_init(params->hmac, &hmacparams) || + !lc_auth_update(params->hmac, t, olen) || + !lc_auth_update(params->hmac, params->info, params->infolen) || + !lc_auth_update(params->hmac, &ctr, sizeof(ctr)) || + !lc_auth_final(params->hmac, tn, &olen)) + return 0; + + memcpy(out, tn, len); + *outlen += len; + + return 1; +} + + +const struct lc_kdf_impl * +lc_kdf_impl_hkdf(void) +{ + static struct lc_kdf_impl hkdf_impl = { + .kdf = &hkdf_kdf, + }; + + return &hkdf_impl; +} diff --git a/lilcrypto.h b/lilcrypto.h index c76cedb..e6fa55d 100644 --- a/lilcrypto.h +++ b/lilcrypto.h @@ -69,6 +69,8 @@ struct lc_cipher_impl; struct lc_hash_ctx; struct lc_hash_impl; +struct lc_kdf_impl; + /* * Parameters. */ @@ -111,6 +113,19 @@ struct lc_xchacha20_poly1305_params { uint8_t nonce[LC_XCHACHA20_NONCELEN]; }; +/* KDF. */ + +struct lc_hkdf_params { + struct lc_hash_ctx *hash; + struct lc_auth_ctx *hmac; + uint8_t *ikm; + size_t ikmlen; + uint8_t *info; + size_t infolen; + uint8_t *salt; + size_t saltlen; +}; + /* * Constant-time operations. @@ -194,6 +209,16 @@ const struct lc_aead_impl *lc_aead_impl_chacha20_poly1305(void); const struct lc_aead_impl *lc_aead_impl_xchacha20_poly1305(void); +/* + * Key derivation functions. + */ + +int lc_kdf(const struct lc_kdf_impl *, uint8_t *, size_t *, void *, + size_t); + +const struct lc_kdf_impl *lc_kdf_impl_hkdf(void); + + /* * Utilities. */ diff --git a/wycheproof/Makefile b/wycheproof/Makefile index 5e41cbc..84aadb6 100644 --- a/wycheproof/Makefile +++ b/wycheproof/Makefile @@ -1,19 +1,21 @@ .PATH: ${.CURDIR}/.. AEAD= wycheproof_aead +HKDF= wycheproof_hkdf MAC= wycheproof_mac -PROGS= ${AEAD} ${MAC} +PROGS= ${AEAD} ${HKDF} ${MAC} NOMAN= noman SRCS_wycheproof_aead= wycheproof_aead.c +SRCS_wycheproof_hkdf= wycheproof_hkdf.c SRCS_wycheproof_mac= wycheproof_mac.c DPADD+= ${.CURDIR}/../lib/obj/liblilcrypto.a LDADD+= ${.CURDIR}/../lib/obj/liblilcrypto.a -tests: all tests-aead tests-mac +tests: all tests-aead tests-hkdf tests-mac tests-aead: .ifndef WYCHEPROOF_DIR @@ -27,6 +29,22 @@ tests-aead: ${WYCHEPROOF_DIR}/testvectors_v1/xchacha20_poly1305_test.json .endfor +tests-hkdf: +.ifndef WYCHEPROOF_DIR + @echo Undefined WYCHEPROOF_DIR; false +.endif +.for p in ${HKDF} + perl ${.CURDIR}/hkdf.pl ${TESTOPTS} -x ./${p} \ + ${WYCHEPROOF_DIR}/testvectors/hkdf_sha256_test.json \ + ${WYCHEPROOF_DIR}/testvectors_v1/hkdf_sha256_test.json \ + ${WYCHEPROOF_DIR}/testvectors/hkdf_sha384_test.json \ + ${WYCHEPROOF_DIR}/testvectors_v1/hkdf_sha384_test.json \ + ${WYCHEPROOF_DIR}/testvectors/hkdf_sha512_test.json \ + ${WYCHEPROOF_DIR}/testvectors_v1/hkdf_sha512_test.json +.endfor + +.include + tests-mac: .ifndef WYCHEPROOF_DIR @echo Undefined WYCHEPROOF_DIR; false diff --git a/wycheproof/hkdf.pl b/wycheproof/hkdf.pl new file mode 100644 index 0000000..6c0c7fb --- /dev/null +++ b/wycheproof/hkdf.pl @@ -0,0 +1,69 @@ +#!/usr/bin/env perl +use v5.38;; +use strict; +use warnings; + +use Getopt::Std; +use JSON::PP; + +my $progname = $0 =~ s@.*/@@r; + +sub slurp ($fh) { local $/; <$fh> } + +sub usage () +{ + say STDERR "Usage: $progname [-Cv] -x runner json_file ", + "[json_files ...]"; + exit 1; +} + +sub main () +{ + my %opts; + my $rc = 0; + + getopts("Cvx:", \%opts) && @ARGV > 0 or usage; + usage unless defined $opts{"x"}; + + for my $f (@ARGV) { + open(my $fh, "<", $f) or die "open failed: $!"; + + my $json = decode_json(slurp($fh)); + for my $testgroup ($json->{testGroups}->@*) { + for my $test ($testgroup->{tests}->@*) { + my @args; + + push(@args, $json->{algorithm}); + push(@args, "-i", $test->{info}); + push(@args, "-K", $testgroup->{keySize}); + push(@args, "-k", $test->{ikm}); + push(@args, "-o", $test->{okm}); + push(@args, "-s", $test->{salt}); + push(@args, "-z", $test->{size}); + push(@args, "-v") if $opts{"v"}; + + open(my $th, "-|", $opts{"x"}, @args) or die; + my $result = slurp($th); + close($th); + + chomp($result); + if ($result ne $test->{result}) { + $rc = 1; + say STDERR "case $test->{tcId}: ", + "expected $test->{result}: ", + "$test->{comment} [", + join(",", $test->{flags}->@*), + "]"; + exit 1 unless $opts{"C"}; + } + } + } + + close($fh); + } + + say "ALL TESTS PASSED!" if $rc == 0; + return $rc; +} + +exit main; diff --git a/wycheproof_hkdf.c b/wycheproof_hkdf.c new file mode 100644 index 0000000..83b22c5 --- /dev/null +++ b/wycheproof_hkdf.c @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2024 Lucas Gabriel Vuotto + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "lilcrypto.h" + + +#define nelems(_a) (sizeof((_a)) / sizeof((_a)[0])) + + +struct testcase { + uint8_t *ikm; + size_t ikmlen; + size_t ikmlenarg; + uint8_t *info; + size_t infolen; + uint8_t *okm; + size_t okmlen; + uint8_t *salt; + size_t saltlen; + size_t size; +}; + +struct kwrunner { + const char *kw; + int (*runner)(const struct testcase *, int); +}; + + +static int hkdf_sha2_runner(const struct lc_hash_impl *, + const struct testcase *, int); +static int hkdf_sha224_runner(const struct testcase *, int); +static int hkdf_sha256_runner(const struct testcase *, int); +static int hkdf_sha384_runner(const struct testcase *, int); +static int hkdf_sha512_runner(const struct testcase *, int); + + +static inline uint8_t +hex2num(char s) +{ + return s >= 'A' ? 10 + (s >= 'a' ? s - 'a' : s - 'A') : s - '0'; +} + +static int +hexparse(const char *s, uint8_t *out, size_t *outlen) +{ + size_t l; + + l = strlen(s); + if (l % 2 != 0) + return 0; + + if (out == NULL) { + *outlen = l / 2; + return 1; + } + + *outlen = 0; + while (*s != '\0') { + if (!isxdigit(s[0]) || !isxdigit(s[1])) + return 0; + *out++ = (hex2num(s[0]) << 4) | hex2num(s[1]); + (*outlen)++; + s += 2; + } + + return 1; +} + +static int +kwrunner_cmp(const void *k0, const void *h0) +{ + const struct kwrunner *h = h0; + const char *k = k0; + + return strcmp(k, h->kw); +} + +static int +(*kw2runner(const char *s))(const struct testcase *, int) +{ + /* Needs to be sorted. */ + static const struct kwrunner tbl[] = { + { "HKDF-SHA-224", &hkdf_sha224_runner }, + { "HKDF-SHA-256", &hkdf_sha256_runner }, + { "HKDF-SHA-384", &hkdf_sha384_runner }, + { "HKDF-SHA-512", &hkdf_sha512_runner }, + }; + struct kwrunner *match; + + match = bsearch(s, tbl, nelems(tbl), sizeof(struct kwrunner), + &kwrunner_cmp); + + return match != NULL ? match->runner : NULL; +} + +static void +usage(void) +{ + fprintf(stderr, "Usage: %s [options]\n", getprogname()); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + int (*runner)(const struct testcase *, int); + const char *errstr; + struct testcase c; + size_t l; + int iflag, Kflag, kflag, oflag, sflag, zflag; + int ch, verbose; + + if (argc < 2) + usage(); + + runner = kw2runner(argv[1]); + if (runner == NULL) + errx(1, "unsupported algorithm: %s", argv[1]); + + optind = 2; + iflag = Kflag = kflag = oflag = sflag = zflag = 0; + verbose = 0; + while ((ch = getopt(argc, argv, "i:K:k:o:s:vz:")) != -1) { + switch (ch) { + case 'i': + iflag = 1; + (void)hexparse(optarg, NULL, &c.infolen); + if (c.infolen != 0) { + c.info = malloc(c.infolen); + if (c.info == NULL) + err(1, "out of memory"); + } else + c.info = NULL; + if (!hexparse(optarg, c.info, &l) || l != c.infolen) + errx(1, "invalid hex string: %s", optarg); + break; + case 'K': + Kflag = 1; + c.ikmlenarg = strtonum(optarg, 0, LLONG_MAX, &errstr); + if (errstr != NULL) + errx(1, "keylen is %s: %s", errstr, optarg); + if (c.ikmlenarg % 8 != 0) + errx(1, "unsupport K value: %zu", c.ikmlenarg); + c.ikmlenarg /= 8; + break; + case 'k': + kflag = 1; + (void)hexparse(optarg, NULL, &c.ikmlen); + if (c.ikmlen != 0) { + c.ikm = malloc(c.ikmlen); + if (c.ikm == NULL) + err(1, "out of memory"); + } else + c.ikm = NULL; + if (!hexparse(optarg, c.ikm, &l) || l != c.ikmlen) + errx(1, "invalid hex string: %s", optarg); + break; + case 'o': + oflag = 1; + (void)hexparse(optarg, NULL, &c.okmlen); + if (c.okmlen != 0) { + c.okm = malloc(c.okmlen); + if (c.okm == NULL) + err(1, "out of memory"); + } else + c.okm = NULL; + if (!hexparse(optarg, c.okm, &l) || l != c.okmlen) + errx(1, "invalid hex string: %s", optarg); + break; + case 's': + sflag = 1; + (void)hexparse(optarg, NULL, &c.saltlen); + if (c.saltlen != 0) { + c.salt = malloc(c.saltlen); + if (c.salt == NULL) + err(1, "out of memory"); + } else + c.salt = NULL; + if (!hexparse(optarg, c.salt, &l) || l != c.saltlen) + errx(1, "invalid hex string: %s", optarg); + break; + case 'v': + verbose = 1; + break; + case 'z': + zflag = 1; + c.size = strtonum(optarg, 0, LLONG_MAX, &errstr); + if (errstr != NULL) + errx(1, "taglen is %s: %s", errstr, optarg); + break; + default: + usage(); + break; + } + } + argc -= optind; + argv += optind; + + if (!(iflag && Kflag && kflag && oflag && sflag && zflag)) + errx(1, "missing required arguments"); + + if (runner(&c, verbose)) { + puts("valid"); + return 0; + } else { + puts("invalid"); + return 0; + } + + puts("valid"); + return 0; +} + +static int +hkdf_sha2_runner(const struct lc_hash_impl *impl, const struct testcase *c, + int verbose) +{ + struct lc_hkdf_params params; + uint8_t *buf; + size_t olen; + + if (c->ikmlen != c->ikmlenarg) + return 0; + params.ikm = c->ikm; + params.ikmlen = c->ikmlen; + params.info = c->info; + params.infolen = c->infolen; + params.salt = c->salt; + params.saltlen = c->saltlen; + + params.hash = lc_hash_ctx_new(impl); + if (params.hash == NULL) + return 0; + params.hmac = lc_auth_ctx_new(lc_auth_impl_hmac()); + if (params.hmac == NULL) + return 0; + + buf = malloc(c->size); + if (buf == NULL) + err(1, "out of memory"); + + if (!lc_kdf(lc_kdf_impl_hkdf(), buf, &olen, ¶ms, c->size)) + return 0; + + if (c->okmlen != olen || + !lc_ct_cmp(buf, c->okm, olen)) { + if (verbose) { + fprintf(stderr, "okmlen (%zu, %zu)\n", c->okmlen, + olen); + lc_hexdump_fp(stderr, c->okm, c->okmlen); + fprintf(stderr, "\n"); + lc_hexdump_fp(stderr, buf, olen); + fprintf(stderr, "\n"); + } + return 0; + } + + free(buf); + lc_hash_ctx_free(params.hash); + lc_auth_ctx_free(params.hmac); + + return 1; +} + +static int +hkdf_sha224_runner(const struct testcase *c, int verbose) +{ + return hkdf_sha2_runner(lc_hash_impl_sha224(), c, verbose); +} + +static int +hkdf_sha256_runner(const struct testcase *c, int verbose) +{ + return hkdf_sha2_runner(lc_hash_impl_sha256(), c, verbose); +} + +static int +hkdf_sha384_runner(const struct testcase *c, int verbose) +{ + return hkdf_sha2_runner(lc_hash_impl_sha384(), c, verbose); +} + +static int +hkdf_sha512_runner(const struct testcase *c, int verbose) +{ + return hkdf_sha2_runner(lc_hash_impl_sha512(), c, verbose); +}