From fa0b7bbe90b4bf262d80c00b21bb37e9d1c75855 Mon Sep 17 00:00:00 2001 From: Thomas Pornin Date: Tue, 14 Aug 2018 17:02:22 +0200 Subject: [PATCH] Added generic HKDF implementation. --- inc/bearssl.h | 2 + inc/bearssl_hmac.h | 30 ++++++++ inc/bearssl_kdf.h | 185 +++++++++++++++++++++++++++++++++++++++++++++ mk/Rules.mk | 6 +- mk/mkrules.sh | 2 + src/kdf/hkdf.c | 107 ++++++++++++++++++++++++++ test/test_crypto.c | 102 +++++++++++++++++++++++++ 7 files changed, 433 insertions(+), 1 deletion(-) create mode 100644 inc/bearssl_kdf.h create mode 100644 src/kdf/hkdf.c diff --git a/inc/bearssl.h b/inc/bearssl.h index 3d5e63a..4f4797c 100644 --- a/inc/bearssl.h +++ b/inc/bearssl.h @@ -39,6 +39,7 @@ * | :-------------- | :------------------------------------------------ | * | bearssl_hash.h | Hash functions | * | bearssl_hmac.h | HMAC | + * | bearssl_kdf.h | Key Derivation Functions | * | bearssl_rand.h | Pseudorandom byte generators | * | bearssl_prf.h | PRF implementations (for SSL/TLS) | * | bearssl_block.h | Symmetric encryption | @@ -125,6 +126,7 @@ #include "bearssl_hash.h" #include "bearssl_hmac.h" +#include "bearssl_kdf.h" #include "bearssl_rand.h" #include "bearssl_prf.h" #include "bearssl_block.h" diff --git a/inc/bearssl_hmac.h b/inc/bearssl_hmac.h index 14147d8..4dc01ca 100644 --- a/inc/bearssl_hmac.h +++ b/inc/bearssl_hmac.h @@ -84,6 +84,21 @@ typedef struct { void br_hmac_key_init(br_hmac_key_context *kc, const br_hash_class *digest_vtable, const void *key, size_t key_len); +/* + * \brief Get the underlying hash function. + * + * This function returns a pointer to the implementation vtable of the + * hash function used for this HMAC key context. + * + * \param kc HMAC key context. + * \return the hash function implementation. + */ +static inline const br_hash_class *br_hmac_key_get_digest( + const br_hmac_key_context *kc) +{ + return kc->dig_vtable; +} + /** * \brief HMAC computation context. * @@ -142,6 +157,21 @@ br_hmac_size(br_hmac_context *ctx) return ctx->out_len; } +/* + * \brief Get the underlying hash function. + * + * This function returns a pointer to the implementation vtable of the + * hash function used for this HMAC context. + * + * \param hc HMAC context. + * \return the hash function implementation. + */ +static inline const br_hash_class *br_hmac_get_digest( + const br_hmac_context *hc) +{ + return hc->dig.vtable; +} + /** * \brief Inject some bytes in HMAC. * diff --git a/inc/bearssl_kdf.h b/inc/bearssl_kdf.h new file mode 100644 index 0000000..f018d7e --- /dev/null +++ b/inc/bearssl_kdf.h @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2018 Thomas Pornin + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef BR_BEARSSL_KDF_H__ +#define BR_BEARSSL_KDF_H__ + +#include +#include + +#include "bearssl_hash.h" +#include "bearssl_hmac.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** \file bearssl_kdf.h + * + * # Key Derivation Functions + * + * KDF are functions that takes a variable length input, and provide a + * variable length output, meant to be used to derive subkeys from a + * master key. + * + * ## HKDF + * + * HKDF is a KDF defined by [RFC 5869](https://tools.ietf.org/html/rfc5869). + * It is based on HMAC, itself using an underlying hash function. Any + * hash function can be used, as long as it is compatible with the rules + * for the HMAC implementation (i.e. output size is 64 bytes or less, hash + * internal state size is 64 bytes or less, and the internal block length is + * a power of 2 between 16 and 256 bytes). HKDF has two phases: + * + * - HKDF-Extract: the input data in ingested, along with a "salt" value. + * + * - HKDF-Expand: the output is produced, from the result of processing + * the input and salt, and using an extra non-secret parameter called + * "info". + * + * The "salt" and "info" strings are non-secret and can be empty. Their role + * is normally to bind the input and output, respectively, to conventional + * identifiers that qualifu them within the used protocol or application. + * + * The implementation defined in this file uses the following functions: + * + * - `br_hkdf_init()`: initialize an HKDF context, with a hash function, + * and the salt. This starts the HKDF-Extract process. + * + * - `br_hkdf_inject()`: inject more input bytes. This function may be + * called repeatedly if the input data is provided by chunks. + * + * - `br_hkdf_flip()`: end the HKDF-Extract process, and start the + * HKDF-Expand process. + * + * - `br_hkdf_produce()`: get the next bytes of output. This function + * may be called several times to obtain the full output by chunks. + * For correct HKDF processing, the same "info" string must be + * provided for each call. + * + * Note that the HKDF total output size (the number of bytes that + * HKDF-Expand is willing to produce) is limited: if the hash output size + * is _n_ bytes, then the maximum output size is _255*n_. + */ + +/** + * \brief HKDF context. + * + * The HKDF context is initialized with a hash function implementation + * and a salt value. Contents are opaque (callers should not access them + * directly). The caller is responsible for allocating the context where + * appropriate. Context initialisation and usage incurs no dynamic + * allocation, so there is no release function. + */ +typedef struct { +#ifndef BR_DOXYGEN_IGNORE + union { + br_hmac_context hmac_ctx; + br_hmac_key_context prk_ctx; + } u; + unsigned char buf[64]; + size_t ptr; + size_t dig_len; + unsigned chunk_num; +#endif +} br_hkdf_context; + +/** + * \brief HKDF context initialization. + * + * The underlying hash function and salt value are provided. Arbitrary + * salt lengths can be used. + * + * HKDF makes a difference between a salt of length zero, and an + * absent salt (the latter being equivalent to a salt consisting of + * bytes of value zero, of the same length as the hash function output). + * If `salt_len` is zero, then this function assumes that the salt is + * present but of length zero. To specify an _absent_ salt, use + * `BR_HKDF_NO_SALT` as `salt` parameter (`salt_len` is then ignored). + * + * \param hc HKDF context to initialise. + * \param digest_vtable pointer to the hash function implementation vtable. + * \param salt HKDF-Extract salt. + * \param salt_len HKDF-Extract salt length (in bytes). + */ +void br_hkdf_init(br_hkdf_context *hc, const br_hash_class *digest_vtable, + const void *salt, size_t salt_len); + +/** + * \brief The special "absent salt" value for HKDF. + */ +#define BR_HKDF_NO_SALT (&br_hkdf_no_salt) + +#ifndef BR_DOXYGEN_IGNORE +extern const unsigned char br_hkdf_no_salt; +#endif + +/** + * \brief HKDF input injection (HKDF-Extract). + * + * This function injects some more input bytes ("key material") into + * HKDF. This function may be called several times, after `br_hkdf_init()` + * but before `br_hkdf_flip()`. + * + * \param hc HKDF context. + * \param ikm extra input bytes. + * \param ikm_len number of extra input bytes. + */ +void br_hkdf_inject(br_hkdf_context *hc, const void *ikm, size_t ikm_len); + +/** + * \brief HKDF switch to the HKDF-Expand phase. + * + * This call terminates the HKDF-Extract process (input injection), and + * starts the HKDF-Expand process (output production). + * + * \param hc HKDF context. + */ +void br_hkdf_flip(br_hkdf_context *hc); + +/** + * \brief HKDF output production (HKDF-Expand). + * + * Produce more output bytes from the current state. This function may be + * called several times, but only after `br_hkdf_flip()`. + * + * Returned value is the number of actually produced bytes. The total + * output length is limited to 255 times the output length of the + * underlying hash function. + * + * \param hc HKDF context. + * \param info application specific information string. + * \param info_len application specific information string length (in bytes). + * \param out destination buffer for the HKDF output. + * \param out_len the length of the requested output (in bytes). + * \return the produced output length (in bytes). + */ +size_t br_hkdf_produce(br_hkdf_context *hc, + const void *info, size_t info_len, void *out, size_t out_len); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/mk/Rules.mk b/mk/Rules.mk index 0a23fa5..b480bd6 100644 --- a/mk/Rules.mk +++ b/mk/Rules.mk @@ -123,6 +123,7 @@ OBJ = \ $(OBJDIR)$Pi32_sub$O \ $(OBJDIR)$Pi32_tmont$O \ $(OBJDIR)$Pi62_modpow2$O \ + $(OBJDIR)$Phkdf$O \ $(OBJDIR)$Phmac$O \ $(OBJDIR)$Phmac_ct$O \ $(OBJDIR)$Paesctr_drbg$O \ @@ -301,7 +302,7 @@ OBJTESTSPEED = \ $(OBJDIR)$Ptest_speed$O OBJTESTX509 = \ $(OBJDIR)$Ptest_x509$O -HEADERSPUB = inc$Pbearssl.h inc$Pbearssl_aead.h inc$Pbearssl_block.h inc$Pbearssl_ec.h inc$Pbearssl_hash.h inc$Pbearssl_hmac.h inc$Pbearssl_pem.h inc$Pbearssl_prf.h inc$Pbearssl_rand.h inc$Pbearssl_rsa.h inc$Pbearssl_ssl.h inc$Pbearssl_x509.h +HEADERSPUB = inc$Pbearssl.h inc$Pbearssl_aead.h inc$Pbearssl_block.h inc$Pbearssl_ec.h inc$Pbearssl_hash.h inc$Pbearssl_hmac.h inc$Pbearssl_kdf.h inc$Pbearssl_pem.h inc$Pbearssl_prf.h inc$Pbearssl_rand.h inc$Pbearssl_rsa.h inc$Pbearssl_ssl.h inc$Pbearssl_x509.h HEADERSPRIV = $(HEADERSPUB) src$Pconfig.h src$Pinner.h HEADERSTOOLS = $(HEADERSPUB) tools$Pbrssl.h T0SRC = T0$PBlobWriter.cs T0$PCPU.cs T0$PCodeElement.cs T0$PCodeElementJump.cs T0$PCodeElementUInt.cs T0$PCodeElementUIntExpr.cs T0$PCodeElementUIntInt.cs T0$PCodeElementUIntUInt.cs T0$PConstData.cs T0$POpcode.cs T0$POpcodeCall.cs T0$POpcodeConst.cs T0$POpcodeGetLocal.cs T0$POpcodeJump.cs T0$POpcodeJumpIf.cs T0$POpcodeJumpIfNot.cs T0$POpcodeJumpUncond.cs T0$POpcodePutLocal.cs T0$POpcodeRet.cs T0$PSType.cs T0$PT0Comp.cs T0$PTPointerBase.cs T0$PTPointerBlob.cs T0$PTPointerExpr.cs T0$PTPointerNull.cs T0$PTPointerXT.cs T0$PTValue.cs T0$PWord.cs T0$PWordBuilder.cs T0$PWordData.cs T0$PWordInterpreted.cs T0$PWordNative.cs @@ -723,6 +724,9 @@ $(OBJDIR)$Pi32_tmont$O: src$Pint$Pi32_tmont.c $(HEADERSPRIV) $(OBJDIR)$Pi62_modpow2$O: src$Pint$Pi62_modpow2.c $(HEADERSPRIV) $(CC) $(CFLAGS) $(INCFLAGS) $(CCOUT)$(OBJDIR)$Pi62_modpow2$O src$Pint$Pi62_modpow2.c +$(OBJDIR)$Phkdf$O: src$Pkdf$Phkdf.c $(HEADERSPRIV) + $(CC) $(CFLAGS) $(INCFLAGS) $(CCOUT)$(OBJDIR)$Phkdf$O src$Pkdf$Phkdf.c + $(OBJDIR)$Phmac$O: src$Pmac$Phmac.c $(HEADERSPRIV) $(CC) $(CFLAGS) $(INCFLAGS) $(CCOUT)$(OBJDIR)$Phmac$O src$Pmac$Phmac.c diff --git a/mk/mkrules.sh b/mk/mkrules.sh index c72b820..eea159b 100755 --- a/mk/mkrules.sh +++ b/mk/mkrules.sh @@ -171,6 +171,7 @@ coresrc=" \ src/int/i32_sub.c \ src/int/i32_tmont.c \ src/int/i62_modpow2.c \ + src/kdf/hkdf.c \ src/mac/hmac.c \ src/mac/hmac_ct.c \ src/rand/aesctr_drbg.c \ @@ -366,6 +367,7 @@ headerspub=" \ inc/bearssl_ec.h \ inc/bearssl_hash.h \ inc/bearssl_hmac.h \ + inc/bearssl_kdf.h \ inc/bearssl_pem.h \ inc/bearssl_prf.h \ inc/bearssl_rand.h \ diff --git a/src/kdf/hkdf.c b/src/kdf/hkdf.c new file mode 100644 index 0000000..6a36851 --- /dev/null +++ b/src/kdf/hkdf.c @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2018 Thomas Pornin + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "inner.h" + +const unsigned char br_hkdf_no_salt = 0; + +/* see bearssl_kdf.h */ +void +br_hkdf_init(br_hkdf_context *hc, const br_hash_class *digest_vtable, + const void *salt, size_t salt_len) +{ + br_hmac_key_context kc; + unsigned char tmp[64]; + + if (salt == BR_HKDF_NO_SALT) { + salt = tmp; + salt_len = br_digest_size(digest_vtable); + memset(tmp, 0, salt_len); + } + br_hmac_key_init(&kc, digest_vtable, salt, salt_len); + br_hmac_init(&hc->u.hmac_ctx, &kc, 0); + hc->dig_len = br_hmac_size(&hc->u.hmac_ctx); +} + +/* see bearssl_kdf.h */ +void +br_hkdf_inject(br_hkdf_context *hc, const void *ikm, size_t ikm_len) +{ + br_hmac_update(&hc->u.hmac_ctx, ikm, ikm_len); +} + +/* see bearssl_kdf.h */ +void +br_hkdf_flip(br_hkdf_context *hc) +{ + unsigned char tmp[64]; + + br_hmac_out(&hc->u.hmac_ctx, tmp); + br_hmac_key_init(&hc->u.prk_ctx, + br_hmac_get_digest(&hc->u.hmac_ctx), tmp, hc->dig_len); + hc->ptr = hc->dig_len; + hc->chunk_num = 0; +} + +/* see bearssl_kdf.h */ +size_t +br_hkdf_produce(br_hkdf_context *hc, + const void *info, size_t info_len, void *out, size_t out_len) +{ + size_t tlen; + + tlen = 0; + while (out_len > 0) { + size_t clen; + + if (hc->ptr == hc->dig_len) { + br_hmac_context hmac_ctx; + unsigned char x; + + hc->chunk_num ++; + if (hc->chunk_num == 256) { + return tlen; + } + x = hc->chunk_num; + br_hmac_init(&hmac_ctx, &hc->u.prk_ctx, 0); + if (x != 1) { + br_hmac_update(&hmac_ctx, hc->buf, hc->dig_len); + } + br_hmac_update(&hmac_ctx, info, info_len); + br_hmac_update(&hmac_ctx, &x, 1); + br_hmac_out(&hmac_ctx, hc->buf); + hc->ptr = 0; + } + clen = hc->dig_len - hc->ptr; + if (clen > out_len) { + clen = out_len; + } + memcpy(out, hc->buf + hc->ptr, clen); + out = (unsigned char *)out + clen; + out_len -= clen; + hc->ptr += clen; + tlen += clen; + } + return tlen; +} diff --git a/test/test_crypto.c b/test/test_crypto.c index dd2562c..740178d 100644 --- a/test/test_crypto.c +++ b/test/test_crypto.c @@ -1029,6 +1029,107 @@ test_HMAC(void) fflush(stdout); } +static void +test_HKDF_inner(const br_hash_class *dig, const char *ikmhex, + const char *salthex, const char *infohex, const char *okmhex) +{ + unsigned char ikm[100], saltbuf[100], info[100], okm[100], tmp[107]; + const unsigned char *salt; + size_t ikm_len, salt_len, info_len, okm_len; + br_hkdf_context hc; + size_t u; + + ikm_len = hextobin(ikm, ikmhex); + if (salthex == NULL) { + salt = BR_HKDF_NO_SALT; + salt_len = 0; + } else { + salt = saltbuf; + salt_len = hextobin(saltbuf, salthex); + } + info_len = hextobin(info, infohex); + okm_len = hextobin(okm, okmhex); + + br_hkdf_init(&hc, dig, salt, salt_len); + br_hkdf_inject(&hc, ikm, ikm_len); + br_hkdf_flip(&hc); + br_hkdf_produce(&hc, info, info_len, tmp, okm_len); + check_equals("KAT HKDF 1", tmp, okm, okm_len); + + br_hkdf_init(&hc, dig, salt, salt_len); + for (u = 0; u < ikm_len; u ++) { + br_hkdf_inject(&hc, &ikm[u], 1); + } + br_hkdf_flip(&hc); + for (u = 0; u < okm_len; u ++) { + br_hkdf_produce(&hc, info, info_len, &tmp[u], 1); + } + check_equals("KAT HKDF 2", tmp, okm, okm_len); + + br_hkdf_init(&hc, dig, salt, salt_len); + br_hkdf_inject(&hc, ikm, ikm_len); + br_hkdf_flip(&hc); + for (u = 0; u < okm_len; u += 7) { + br_hkdf_produce(&hc, info, info_len, &tmp[u], 7); + } + check_equals("KAT HKDF 3", tmp, okm, okm_len); + + printf("."); + fflush(stdout); +} + +static void +test_HKDF(void) +{ + printf("Test HKDF: "); + fflush(stdout); + + test_HKDF_inner(&br_sha256_vtable, + "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "000102030405060708090a0b0c", + "f0f1f2f3f4f5f6f7f8f9", + "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"); + + test_HKDF_inner(&br_sha256_vtable, + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f", + "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87"); + + test_HKDF_inner(&br_sha256_vtable, + "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "", + "", + "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8"); + + test_HKDF_inner(&br_sha1_vtable, + "0b0b0b0b0b0b0b0b0b0b0b", + "000102030405060708090a0b0c", + "f0f1f2f3f4f5f6f7f8f9", + "085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896"); + + test_HKDF_inner(&br_sha1_vtable, + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f", + "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + "0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4"); + + test_HKDF_inner(&br_sha1_vtable, + "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "", + "", + "0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0ea00033de03984d34918"); + + test_HKDF_inner(&br_sha1_vtable, + "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c", + NULL, + "", + "2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5673a081d70cce7acfc48"); + + printf(" done.\n"); + fflush(stdout); +} + static void test_HMAC_DRBG(void) { @@ -8266,6 +8367,7 @@ static const struct { STU(MD5_SHA1), STU(multihash), STU(HMAC), + STU(HKDF), STU(HMAC_DRBG), STU(AESCTR_DRBG), STU(PRF), -- 2.17.1