From david at atsec.com Mon Oct 14 11:27:35 2024 From: david at atsec.com (David Sugar) Date: Mon, 14 Oct 2024 11:27:35 +0200 Subject: FIPS 140 service indicator revamp Message-ID: Hi, Libgcrypt implements a FIPS 140 service indicator to comply with the rules stipulated by FIPS 140-3. However, based on recent clarifications provided by NIST around the topic, the current implementation falls short of meeting these clarifications. The current service indicator "statically" returns the indicator whether an algorithm operates in a FIPS-compliant manner or not. This includes the indicator whether the cryptographic algorithm is an "approved" algorithm or not. The issue with this approach is that it is "static" in the sense that a caller asks libgcrypt whether algorithm X is approved or not. NIST clarified that such static behavior is not sufficient. NIST requests a "dynamic" indicator in the sense that during the processing of the actual request to perform a cryptographic operation, the indicator must be "generated" and "returned" to the caller. I.e. when the caller performs, say an RSA 1024 operation, that very API call is required to generate the indicator that the request was not FIPS compliant. Conversely, when the caller performs an RSA 2048 operation, that very API call must generate the indicator that it was an approved mechanism. The NIST requirement does not pre-scribe how exactly that indicator is to be implemented. This implies we have quite some freedom to implement it. This email offers a proposal to solve the service indicator non-compliance. The provided patch adjusts one cryptographic service to show the chosen approach. If we all agree, we will proceed in developing the patch series to cover the other services appropriately. The issue we have to solve is the following: the APIs defined by libcrypt return a status value with an integer field. That field is defined to be 0 for success and != 0 for all errors. With the presence of a FIPS service indicator, we must return a second return status which may not be equal to zero. Yet, we want consumers to not view the result of the FIPS service indicator as an error to ensure even in FIPS mode, non-approved services can be invoked without changing the caller. Thus, it is not possible to return the FIPS service indicator as part of the standard return status (e.g. by using the high bits in the integer field). Our solution we offer here is the use of the errno to convey the FIPS status indicator. As the errno is also used to communicate a real issue, we define the errno as a FIPS status indicator only if the API return code is zero, i.e. there is a successful API operation. I.e.: - if the API return code is 0 -> errno contains the FIPS service indicator - if the API return code is != 0 -> errno contains the "regular" error indicator (i.e. the patch will not touch the errno) To achieve that and not trample the errno during the libgcrypt operation, the patch suggests: 1. adding a new libgcrypt error code - this error code is only used internally 2. adding an instrumentation to each API call to mark the service as approved or non-approved at the very end of the operation if the operation was successful. In case of a non-approved service, the new libgcrypt error code is used. The placement at the end of the API call implies that (a) the API call performs all operations and (b) returns the service indicator. 3. in the interface layer of the visibility.c, the FIPS non-approved "error" is converted into an API return code of 0 and an errno of -EOPNOTSUPP. Otherwise the errno is set to 0. This implies that: 1. a caller even in FIPS mode will always have its API call succeeding (even if it is non-approved) 2. a caller can check the FIPS indicator if the API return code is 0 by checking the errno to contain -EOPNOTSUPP. If it contains this error code, the caller knows the serivce was non-approved. If it contains 0, the caller knows the service was approved. Question: The patch currently implements the logic that in case the new libgcrypt-internal error observed, the errno is set. This implies that for all non-approved services an appropriate instrumentation is required. Would it make sense to reverse the logic, i.e. return the libgcrypt-internal error if a FIPS-approved serivce is triggered? The visibility-layer would then set the errno to -EOPNOTSUPP if the libgcrypt-internal error is*not* seen. With this approach, we can have a white-list where we only need to instrument the API calls that we know are FIPS-approved. This would limit the amount of changes and with a white-list we many not forget non-approved serivces. Best regards David -- atsec information security GmbH, Ismaninger Str. 19, 81675 M?nchen, Germany Phone: +49-89-44249840 / Web: atsec.com HRB: 129439 (Amtsgericht M?nchen) Geschaeftsfuehrer: Staffan Persson, Dr. Michael Vogel -------------- next part -------------- --- libgcrypt/cipher/kdf.c 2024-10-10 11:13:04.000000000 +0200 +++ libgcrypt-fips2/cipher/kdf.c 2024-10-14 11:02:54.146310526 +0200 @@ -239,7 +239,11 @@ but certain KDF algorithm may use it differently. {SALT,SALTLEN} is a salt as needed by most KDF algorithms. ITERATIONS is a positive integer parameter to most KDFs. 0 is returned on success, - or an error code on failure. */ + or an error code on failure. + + FIPS: if fips_mode() is enabled, the function will return + GPG_ERR_FORBIDDEN if the function call violates one + or more fips requirements. */ gpg_err_code_t _gcry_kdf_derive (const void *passphrase, size_t passphraselen, int algo, int subalgo, @@ -285,19 +289,19 @@ { /* FIPS requires minimum passphrase length, see FIPS 140-3 IG D.N */ if (fips_mode () && passphraselen < 8) - return GPG_ERR_INV_VALUE; + return fips_not_compliant(); /* FIPS requires minimum salt length of 128 b (SP 800-132 sec. 5.1, p.6) */ if (fips_mode () && saltlen < 16) - return GPG_ERR_INV_VALUE; + return fips_not_compliant(); /* FIPS requires minimum iterations bound (SP 800-132 sec 5.2, p.6) */ if (fips_mode () && iterations < 1000) - return GPG_ERR_INV_VALUE; + return fips_not_compliant(); /* Check minimum key size */ if (fips_mode () && keysize < 14) - return GPG_ERR_INV_VALUE; + return fips_not_compliant(); ec = _gcry_kdf_pkdf2 (passphrase, passphraselen, subalgo, salt, saltlen, iterations, keysize, keybuffer); @@ -318,6 +322,10 @@ ec = GPG_ERR_UNKNOWN_ALGORITHM; break; } + + /* On success we verify that the kdf is allowed in fips_mode */ + if (ec == 0 && fips_mode () && !_gcry_fips_check_kdf_compliant(algo, subalgo)) + ec = fips_not_compliant(); leave: return ec; --- libgcrypt/src/fips.c 2024-10-10 11:13:04.000000000 +0200 +++ libgcrypt-fips2/src/fips.c 2024-10-14 10:58:54.243408712 +0200 @@ -1172,3 +1172,28 @@ abort (); /*NOTREACHED*/ } + +enum gcry_fips_kdf_valid_alg_idx + { + GCRY_FIPS_KDF_PBKDF2_IDX = 0, + /* This has to be the last element */ + GCRY_FIPS_KDF_TOTAL + }; + +int gcry_fips_kdf_valid_alg[GCRY_FIPS_KDF_TOTAL] = { + GCRY_KDF_PBKDF2, +}; + +/* This function should be called to ensure that the used kdf + is fips compliant. */ +int +_gcry_fips_check_kdf_compliant(int algo, int subalgo /* TODO: dead param */) +{ + for (int i = 0; i < GCRY_FIPS_KDF_TOTAL; i++) + { + if (gcry_fips_kdf_valid_alg[i] == algo) + return 1; + } + return 0; +} + --- libgcrypt/src/g10lib.h 2024-10-10 11:13:04.000000000 +0200 +++ libgcrypt-fips2/src/g10lib.h 2024-10-14 09:55:52.409417815 +0200 @@ -484,6 +484,9 @@ (!fips_mode () || _gcry_global_is_operational ())) #define fips_not_operational() (GPG_ERR_NOT_OPERATIONAL) +#define fips_not_compliant() (GPG_ERR_FORBIDDEN) + +int _gcry_fips_check_kdf_compliant(int algo, int subalgo); int _gcry_fips_test_operational (void); int _gcry_fips_test_error_or_operational (void); --- libgcrypt/src/visibility.c 2024-10-10 11:13:04.000000000 +0200 +++ libgcrypt-fips2/src/visibility.c 2024-10-14 11:01:26.000023647 +0200 @@ -20,6 +20,7 @@ #include #include +#include #define _GCRY_INCLUDED_BY_VISIBILITY_C #include "g10lib.h" @@ -1398,11 +1399,29 @@ unsigned long iterations, size_t keysize, void *keybuffer) { + gpg_err_code_t status; + if (!fips_is_operational ()) return gpg_error (fips_not_operational ()); - return gpg_error (_gcry_kdf_derive (passphrase, passphraselen, algo, hashalgo, - salt, saltlen, iterations, - keysize, keybuffer)); + status = _gcry_kdf_derive (passphrase, + passphraselen, algo, hashalgo, + salt, saltlen, iterations, + keysize, keybuffer); + /* + * A FIPS error is indicated by the errno EOPNOTSUPP, i.e., + * to check for success in fips_mode the caller must immediately + * check `errno == EOPNOTSUPP` and act upon it, even if the returned + * status code is success. + */ + if (status == fips_not_compliant()) + { + errno = EOPNOTSUPP; + status = 0; + } + else + errno = 0; + + return gpg_error (status); } gpg_error_t -------------- next part -------------- A non-text attachment was scrubbed... Name: OpenPGP_signature.asc Type: application/pgp-signature Size: 840 bytes Desc: OpenPGP digital signature URL: