Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package libcotp for openSUSE:Factory checked in at 2026-04-16 19:25:09 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/libcotp (Old) and /work/SRC/openSUSE:Factory/.libcotp.new.11940 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "libcotp" Thu Apr 16 19:25:09 2026 rev:17 rq:1347296 version:4.0.1 Changes: -------- --- /work/SRC/openSUSE:Factory/libcotp/libcotp.changes 2026-03-04 21:10:11.844795584 +0100 +++ /work/SRC/openSUSE:Factory/.libcotp.new.11940/libcotp.changes 2026-04-16 19:25:09.567760686 +0200 @@ -1,0 +2,19 @@ +Thu Apr 16 13:58:55 UTC 2026 - Paolo Stivanin <[email protected]> + +- Update to 4.0.1: + Security Fixes + * Fixed timing side-channel in validate_totp_in_window: comparison now uses min(gen_len, user_len) bytes to prevent reading past buffer bounds when lengths differ + * Normalized secret key is now zeroed with cotp_secure_memzero before freeing in compute_hmac + * Fixed memory leak in OpenSSL backend: EVP_MAC not freed when calloc fails in whmac_gethandle + * Fixed memory leak in OpenSSL backend: EVP_MAC_CTX not freed in whmac_freehandle and on buffer-too-small error path in whmac_finalize + * Added negative algorithm index validation (algo < 0) in all three HMAC backends (gcrypt, OpenSSL, MbedTLS) + Hardening + * All public symbols now use explicit __attribute__((visibility("default"))); library compiled with -fvisibility=hidden to minimize exported symbol surface + * Added linker hardening flags: full RELRO (-Wl,-z,relro,-z,now) and non-executable stack (-Wl,-z,noexecstack) + * REVERSE_BYTES macro wrapped in do { ... } while (0) for safe use in all statement contexts + * CMake now detects explicit_bzero at configure time via check_function_exists + Improvements + * Base32 validation (valid_b32_str) now enforces RFC 4648 padding rules: rejects data characters after padding, validates padding count (0, 1, 3, 4, or 6), and requires padded strings to have length divisible by 8 + * check_input max_len parameter changed from int32_t to size_t for type correctness + +------------------------------------------------------------------- Old: ---- v4.0.0.tar.gz v4.0.0.tar.gz.asc New: ---- v4.0.1.tar.gz v4.0.1.tar.gz.asc ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ libcotp.spec ++++++ --- /var/tmp/diff_new_pack.8b0HBl/_old 2026-04-16 19:25:10.463797621 +0200 +++ /var/tmp/diff_new_pack.8b0HBl/_new 2026-04-16 19:25:10.467797786 +0200 @@ -24,7 +24,7 @@ %define libsoname %{name}4 Name: libcotp -Version: 4.0.0 +Version: 4.0.1 Release: 0 Summary: C library for generating TOTP and HOTP License: Apache-2.0 ++++++ v4.0.0.tar.gz -> v4.0.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/CMakeLists.txt new/libcotp-4.0.1/CMakeLists.txt --- old/libcotp-4.0.0/CMakeLists.txt 2026-02-24 11:20:31.000000000 +0100 +++ new/libcotp-4.0.1/CMakeLists.txt 2026-04-15 16:33:19.000000000 +0200 @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(cotp VERSION "4.0.0" LANGUAGES "C") +project(cotp VERSION "4.0.1" LANGUAGES "C") set(CMAKE_C_STANDARD 11) @@ -7,6 +7,9 @@ include(GNUInstallDirs) include(CMakePackageConfigHelpers) +include(CheckFunctionExists) + +check_function_exists(explicit_bzero HAVE_EXPLICIT_BZERO) find_package(PkgConfig) @@ -63,6 +66,10 @@ add_library(cotp ${SOURCE_FILES}) +if (HAVE_EXPLICIT_BZERO) + target_compile_definitions(cotp PRIVATE HAVE_EXPLICIT_BZERO) +endif() + if (COTP_ENABLE_VALIDATION) target_compile_definitions(cotp PUBLIC COTP_ENABLE_VALIDATION) endif() @@ -78,7 +85,10 @@ target_compile_options(cotp PRIVATE -Wall -Wextra -O3 -Wformat=2 -Wmissing-format-attribute -fstack-protector-strong -Wundef -Wmissing-format-attribute -fdiagnostics-color=always -Wstrict-prototypes -Wunreachable-code -Wchar-subscripts -Wwrite-strings -Wpointer-arith -Wbad-function-cast - -Wcast-align -Werror=format-security -Werror=implicit-function-declaration -Wno-sign-compare -Wno-format-nonliteral -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3) + -Wcast-align -Werror=format-security -Werror=implicit-function-declaration -Wno-sign-compare -Wno-format-nonliteral -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 + -fvisibility=hidden) + +target_link_options(cotp PRIVATE -Wl,-z,relro,-z,now -Wl,-z,noexecstack) set_target_properties(cotp PROPERTIES VERSION ${CMAKE_PROJECT_VERSION} SOVERSION ${CMAKE_PROJECT_VERSION_MAJOR}) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/README.md new/libcotp-4.0.1/README.md --- old/libcotp-4.0.0/README.md 2026-02-24 11:20:31.000000000 +0100 +++ new/libcotp-4.0.1/README.md 2026-04-15 16:33:19.000000000 +0200 @@ -71,7 +71,7 @@ - `base32_secret`: Base32 encoded (may contain spaces). `NULL` is invalid. - `digits`: 4–10 inclusive - `period`: 1–120 seconds inclusive -- `algo`: `SHA1`, `SHA256`, `SHA512` +- `algo`: `COTP_SHA1`, `COTP_SHA256`, `COTP_SHA512` - `counter`: non-negative - `timestamp`: UNIX epoch seconds @@ -93,7 +93,7 @@ ```c cotp_error_t err; -char *code = get_totp("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", 6, 30, SHA1, &err); +char *code = get_totp("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", 6, 30, COTP_SHA1, &err); if (!code) { // handle error } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/src/cotp.h new/libcotp-4.0.1/src/cotp.h --- old/libcotp-4.0.0/src/cotp.h 2026-02-24 11:20:31.000000000 +0100 +++ new/libcotp-4.0.1/src/cotp.h 2026-04-15 16:33:19.000000000 +0200 @@ -2,6 +2,12 @@ #include <stdint.h> #include <stdbool.h> +#if defined(__GNUC__) || defined(__clang__) + #define COTP_API __attribute__((visibility("default"))) +#else + #define COTP_API +#endif + #define COTP_SHA1 0 #define COTP_SHA256 1 #define COTP_SHA512 2 @@ -40,7 +46,7 @@ * Returns 1 if it matches for any offset in [-window, +window], 0 otherwise. * On success and match, sets matched_delta to the offset that matched (may be 0). On general failure, returns 0 and sets err_code. */ -int validate_totp_in_window(const char* user_code, +COTP_API int validate_totp_in_window(const char* user_code, const char* base32_encoded_secret, long timestamp, int digits, @@ -52,10 +58,10 @@ #endif // Context helpers -cotp_ctx* cotp_ctx_create(int digits, int period, int sha_algo); -void cotp_ctx_free(cotp_ctx* ctx); -char* cotp_ctx_totp_at(cotp_ctx* ctx, const char* base32_encoded_secret, long timestamp, cotp_error_t* err); -char* cotp_ctx_totp(cotp_ctx* ctx, const char* base32_encoded_secret, cotp_error_t* err); +COTP_API cotp_ctx* cotp_ctx_create(int digits, int period, int sha_algo); +COTP_API void cotp_ctx_free(cotp_ctx* ctx); +COTP_API char* cotp_ctx_totp_at(cotp_ctx* ctx, const char* base32_encoded_secret, long timestamp, cotp_error_t* err); +COTP_API char* cotp_ctx_totp(cotp_ctx* ctx, const char* base32_encoded_secret, cotp_error_t* err); /** * base32_encode @@ -63,7 +69,7 @@ * Ownership: returns a newly allocated, NUL-terminated string on success; caller must free() it. * On error: returns NULL and sets err_code. */ -char *base32_encode (const uint8_t *user_data, +COTP_API char *base32_encode (const uint8_t *user_data, size_t data_len, cotp_error_t *err_code); @@ -74,7 +80,7 @@ * The returned data preserves the input NUL when the original encoded content represented it. * On error: returns NULL and sets err_code. */ -uint8_t *base32_decode (const char *user_data_untrimmed, +COTP_API uint8_t *base32_decode (const char *user_data_untrimmed, size_t data_len, cotp_error_t *err_code); @@ -83,7 +89,7 @@ * * Checks whether a string is valid Base32 (ignoring ASCII spaces). Does not allocate. */ -bool is_string_valid_b32 (const char *user_data); +COTP_API bool is_string_valid_b32 (const char *user_data); /** * get_hotp @@ -91,7 +97,7 @@ * Ownership: returns a newly allocated, zero-padded OTP string of requested width; caller must free(). * On error: returns NULL and sets err_code. */ -char *get_hotp (const char *base32_encoded_secret, +COTP_API char *get_hotp (const char *base32_encoded_secret, long counter, int digits, int sha_algo, @@ -103,7 +109,7 @@ * Ownership: returns a newly allocated, zero-padded OTP string; caller must free(). * On error: returns NULL and sets err_code. */ -char *get_totp (const char *base32_encoded_secret, +COTP_API char *get_totp (const char *base32_encoded_secret, int digits, int period, int sha_algo, @@ -115,7 +121,7 @@ * Ownership: returns a newly allocated Steam-style OTP string; caller must free(). * On error: returns NULL and sets err_code. */ -char *get_steam_totp (const char *base32_encoded_secret, +COTP_API char *get_steam_totp (const char *base32_encoded_secret, int period, cotp_error_t *err_code); @@ -125,7 +131,7 @@ * Ownership: returns a newly allocated, zero-padded OTP string; caller must free(). * On error: returns NULL and sets err_code. */ -char *get_totp_at (const char *base32_encoded_secret, +COTP_API char *get_totp_at (const char *base32_encoded_secret, long time, int digits, int period, @@ -138,7 +144,7 @@ * Ownership: returns a newly allocated Steam-style OTP string; caller must free(). * On error: returns NULL and sets err_code. */ -char *get_steam_totp_at (const char *base32_encoded_secret, +COTP_API char *get_steam_totp_at (const char *base32_encoded_secret, long timestamp, int period, cotp_error_t *err_code); @@ -150,7 +156,7 @@ * the returned integer will naturally drop them; err_code is set to MISSING_LEADING_ZERO in that case. * On invalid input returns -1 and sets err_code to INVALID_USER_INPUT. */ -int64_t otp_to_int (const char *otp, +COTP_API int64_t otp_to_int (const char *otp, cotp_error_t *err_code); #ifdef __cplusplus diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/src/otp.c new/libcotp-4.0.1/src/otp.c --- old/libcotp-4.0.0/src/otp.c 2026-02-24 11:20:31.000000000 +0100 +++ new/libcotp-4.0.1/src/otp.c 2026-04-15 16:33:19.000000000 +0200 @@ -21,14 +21,18 @@ #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ #define REVERSE_BYTES(C, C_reverse_byte_order) \ - for (int j = 0, i = 7; j < 8; j++, i--) { \ + do { \ + for (int j = 0, i = 7; j < 8; j++, i--) { \ (C_reverse_byte_order)[i] = ((unsigned char *)&(C))[j]; \ - } + } \ + } while (0) #elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ #define REVERSE_BYTES(C, C_reverse_byte_order) \ + do { \ for (int j = 0; j < 8; j++) { \ (C_reverse_byte_order)[j] = ((unsigned char *)&(C))[j]; \ - } + } \ + } while (0) #else #error "Unknown endianness" #endif @@ -374,6 +378,7 @@ } if (normalized_K[0] == '\0') { + cotp_secure_memzero(normalized_K, 1); free(normalized_K); *err_code = EMPTY_STRING; return NULL; @@ -381,7 +386,9 @@ size_t secret_len = b32_decoded_len_from_str(normalized_K); - unsigned char *secret = base32_decode (normalized_K, strlen(normalized_K), err_code); + size_t normalized_K_len = strlen(normalized_K); + unsigned char *secret = base32_decode (normalized_K, normalized_K_len, err_code); + cotp_secure_memzero(normalized_K, normalized_K_len); free (normalized_K); if (secret == NULL) { return NULL; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/src/utils/base32.c new/libcotp-4.0.1/src/utils/base32.c --- old/libcotp-4.0.0/src/utils/base32.c 2026-02-24 11:20:31.000000000 +0100 +++ new/libcotp-4.0.1/src/utils/base32.c 2026-04-15 16:33:19.000000000 +0200 @@ -50,7 +50,7 @@ static cotp_error_t check_input (const uint8_t *user_data, size_t data_len, - int32_t max_len); + size_t max_len); static int strip_char (char *str); @@ -225,14 +225,37 @@ return false; } + size_t len = 0; + size_t pad_count = 0; + bool seen_pad = false; + while (*str) { uint8_t c = (uint8_t)*str; if (c >= 128 || !b32_valid[c]) { return false; } + if (c == '=') { + seen_pad = true; + pad_count++; + } else if (seen_pad) { + // data character after padding is invalid per RFC 4648 + return false; + } + len++; str++; } + // If padding is present, validate count: valid padding counts are 0, 1, 3, 4, 6 + if (seen_pad) { + if (pad_count == 2 || pad_count == 5 || pad_count == 7 || pad_count > 6) { + return false; + } + // total length (data + padding) must be a multiple of 8 + if (len % 8 != 0) { + return false; + } + } + return true; } @@ -290,7 +313,7 @@ static cotp_error_t check_input (const uint8_t *user_data, size_t data_len, - int32_t max_len) + size_t max_len) { if (!user_data || data_len > max_len) { return INVALID_USER_INPUT; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/src/utils/secure_zero.h new/libcotp-4.0.1/src/utils/secure_zero.h --- old/libcotp-4.0.0/src/utils/secure_zero.h 2026-02-24 11:20:31.000000000 +0100 +++ new/libcotp-4.0.1/src/utils/secure_zero.h 2026-04-15 16:33:19.000000000 +0200 @@ -1,15 +1,21 @@ #pragma once #include <stddef.h> +#if defined(__GNUC__) || defined(__clang__) + #define COTP_INTERNAL_API __attribute__((visibility("default"))) +#else + #define COTP_INTERNAL_API +#endif + #ifdef __cplusplus extern "C" { #endif // Wipes memory in a way that compilers must not elide. Use for secrets and HMAC outputs. -void cotp_secure_memzero(void *ptr, size_t len); +COTP_INTERNAL_API void cotp_secure_memzero(void *ptr, size_t len); // Constant-time comparison. Returns 0 if equal, non-zero otherwise. -int cotp_timing_safe_memcmp(const void *a, const void *b, size_t len); +COTP_INTERNAL_API int cotp_timing_safe_memcmp(const void *a, const void *b, size_t len); #ifdef __cplusplus } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/src/utils/validation.c new/libcotp-4.0.1/src/utils/validation.c --- old/libcotp-4.0.0/src/utils/validation.c 2026-02-24 11:20:31.000000000 +0100 +++ new/libcotp-4.0.1/src/utils/validation.c 2026-04-15 16:33:19.000000000 +0200 @@ -36,7 +36,9 @@ return 0; } size_t gen_len = strlen(gen); - int ok = (gen_len == user_len) && (cotp_timing_safe_memcmp(gen, user_code, gen_len) == 0); + int len_match = (gen_len == user_len); + int cmp_match = (cotp_timing_safe_memcmp(gen, user_code, gen_len < user_len ? gen_len : user_len) == 0); + int ok = len_match && cmp_match; free(gen); if (ok) { if (matched_delta) *matched_delta = delta; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/src/utils/whmac_gcrypt.c new/libcotp-4.0.1/src/utils/whmac_gcrypt.c --- old/libcotp-4.0.0/src/utils/whmac_gcrypt.c 2026-02-24 11:20:31.000000000 +0100 +++ new/libcotp-4.0.1/src/utils/whmac_gcrypt.c 2026-04-15 16:33:19.000000000 +0200 @@ -39,7 +39,7 @@ whmac_handle_t *whmac_handle = NULL; gcry_md_hd_t hd; - if (algo > 2) { + if (algo < 0 || algo > 2) { return NULL; } gpg_error_t gpg_err = gcry_md_open (&hd, gcrypt_algo[algo], GCRY_MD_FLAG_HMAC); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/src/utils/whmac_mbedtls.c new/libcotp-4.0.1/src/utils/whmac_mbedtls.c --- old/libcotp-4.0.0/src/utils/whmac_mbedtls.c 2026-02-24 11:20:31.000000000 +0100 +++ new/libcotp-4.0.1/src/utils/whmac_mbedtls.c 2026-04-15 16:33:19.000000000 +0200 @@ -39,7 +39,7 @@ return NULL; } - if (algo > 2) { + if (algo < 0 || algo > 2) { free (whmac_handle); return NULL; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/src/utils/whmac_openssl.c new/libcotp-4.0.1/src/utils/whmac_openssl.c --- old/libcotp-4.0.0/src/utils/whmac_openssl.c 2026-02-24 11:20:31.000000000 +0100 +++ new/libcotp-4.0.1/src/utils/whmac_openssl.c 2026-04-15 16:33:19.000000000 +0200 @@ -37,7 +37,7 @@ }; whmac_handle_t *whmac_handle = NULL; - if (algo > 2) { + if (algo < 0 || algo > 2) { return NULL; } @@ -45,6 +45,7 @@ if (mac != NULL) { whmac_handle = calloc (1, sizeof(*whmac_handle)); if (whmac_handle == NULL) { + EVP_MAC_free (mac); return NULL; } whmac_handle->mac = mac; @@ -62,6 +63,9 @@ whmac_freehandle (whmac_handle_t *hd) { if (!hd) return; + if (hd->ctx) { + EVP_MAC_CTX_free (hd->ctx); + } EVP_MAC_free (hd->mac); free (hd); } @@ -103,6 +107,8 @@ } if (dlen > buflen) { + EVP_MAC_CTX_free (hd->ctx); + hd->ctx = NULL; return -MEMORY_ALLOCATION_ERROR; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/tests/CMakeLists.txt new/libcotp-4.0.1/tests/CMakeLists.txt --- old/libcotp-4.0.0/tests/CMakeLists.txt 2026-02-24 11:20:31.000000000 +0100 +++ new/libcotp-4.0.1/tests/CMakeLists.txt 2026-04-15 16:33:19.000000000 +0200 @@ -12,3 +12,9 @@ add_test (NAME TestBase32Encode COMMAND test_base32encode) add_test (NAME TestBase32Decode COMMAND test_base32decode) add_test (NAME TestBase32Roundtrip COMMAND test_base32_roundtrip) + +if (COTP_ENABLE_VALIDATION) + add_executable (test_validation test_validation.c) + target_link_libraries (test_validation PRIVATE cotp criterion) + add_test (NAME TestValidation COMMAND test_validation) +endif() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/tests/test_base32decode.c new/libcotp-4.0.1/tests/test_base32decode.c --- old/libcotp-4.0.0/tests/test_base32decode.c 2026-02-24 11:20:31.000000000 +0100 +++ new/libcotp-4.0.1/tests/test_base32decode.c 2026-04-15 16:33:19.000000000 +0200 @@ -125,4 +125,42 @@ } free (binary); +} + + +Test(b32_decode_test, b32_padding_in_middle) { + cotp_error_t err; + const char *k = "MY=W======"; + + uint8_t *dk = base32_decode (k, strlen(k), &err); + + cr_expect_null (dk, "Expected NULL for padding in middle of string"); + cr_expect_eq (err, INVALID_B32_INPUT); +} + + +Test(b32_decode_test, b32_invalid_padding_count) { + cotp_error_t err; + // 2 padding chars is never valid in base32 + const char *k = "MZXW6Y=="; + + uint8_t *dk = base32_decode (k, strlen(k), &err); + + cr_expect_null (dk, "Expected NULL for invalid padding count"); + cr_expect_eq (err, INVALID_B32_INPUT); +} + + +Test(b32_decode_test, b32_no_padding_valid) { + cotp_error_t err; + // No padding at all is accepted (common for OTP secrets) + const char *k = "MZXW6YTBOI"; + const char *expected = "foobar"; + + uint8_t *dk = base32_decode (k, strlen(k), &err); + + cr_expect_eq (err, NO_ERROR); + cr_expect_str_eq ((char *)dk, expected, "Expected %s to be equal to %s", dk, expected); + + free (dk); } \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/tests/test_otp.c new/libcotp-4.0.1/tests/test_otp.c --- old/libcotp-4.0.0/tests/test_otp.c 2026-02-24 11:20:31.000000000 +0100 +++ new/libcotp-4.0.1/tests/test_otp.c 2026-04-15 16:33:19.000000000 +0200 @@ -31,8 +31,10 @@ cotp_error_t err; for (int i = 0; i < 6; i++) { - int64_t totp = otp_to_int (get_totp_at (K_base32, counter[i], 8, 30, COTP_SHA1, &err), &err); + char *totp_str = get_totp_at (K_base32, counter[i], 8, 30, COTP_SHA1, &err); + int64_t totp = otp_to_int (totp_str, &err); cr_expect_eq (totp, expected_totp[i], "Expected %08ld to be equal to %08ld\n", totp, expected_totp[i]); + free (totp_str); } free (K_base32); } @@ -63,9 +65,11 @@ char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); cotp_error_t err; - int64_t totp = otp_to_int (get_totp_at (K_base32, counter, 10, 30, COTP_SHA1, &err), &err); + char *totp_str = get_totp_at (K_base32, counter, 10, 30, COTP_SHA1, &err); + int64_t totp = otp_to_int (totp_str, &err); cr_expect_eq (totp, expected_totp, "Expected %010ld to be equal to %010ld\n", totp, expected_totp); + free (totp_str); free (K_base32); } @@ -274,9 +278,11 @@ char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); cotp_error_t err; - int64_t totp = otp_to_int (get_totp_at (K_base32, counter, 10, 30, COTP_SHA1, &err), &err); + char *totp_str = get_totp_at (K_base32, counter, 10, 30, COTP_SHA1, &err); + int64_t totp = otp_to_int (totp_str, &err); cr_expect_eq (err, MISSING_LEADING_ZERO, "Expected %d to be equal to %d\n", err, MISSING_LEADING_ZERO); + free (totp_str); free (K_base32); } @@ -312,3 +318,214 @@ cr_expect_eq (err, INVALID_USER_INPUT, "Expected %d to be equal to %d\n", err, INVALID_USER_INPUT); cr_assert_null (totp); } + + +Test(totp_generic, test_empty_secret) { + cotp_error_t err; + char *totp = get_totp ("", 6, 30, COTP_SHA1, &err); + + cr_expect_eq (err, EMPTY_STRING, "Expected %d to be equal to %d\n", err, EMPTY_STRING); + cr_assert_null (totp); +} + + +Test(totp_boundary, test_min_digits) { + const char *K = "12345678901234567890"; + + cotp_error_t cotp_err; + char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); + + cotp_error_t err; + char *totp = get_totp_at (K_base32, 59, 4, 30, COTP_SHA1, &err); + cr_expect_eq (err, NO_ERROR, "Expected %d to be equal to %d\n", err, NO_ERROR); + cr_assert_not_null (totp); + cr_expect_eq (strlen(totp), 4, "Expected length 4, got %zu\n", strlen(totp)); + + free (totp); + free (K_base32); +} + + +Test(totp_boundary, test_max_digits) { + const char *K = "12345678901234567890"; + const char *expected_totp = "0689005924"; + + cotp_error_t cotp_err; + char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); + + cotp_error_t err; + char *totp = get_totp_at (K_base32, 1234567890, 10, 30, COTP_SHA1, &err); + cr_expect_eq (err, NO_ERROR, "Expected %d to be equal to %d\n", err, NO_ERROR); + cr_assert_not_null (totp); + cr_expect_eq (strlen(totp), 10, "Expected length 10, got %zu\n", strlen(totp)); + cr_expect_str_eq (totp, expected_totp, "Expected %s to be equal to %s\n", totp, expected_totp); + + free (totp); + free (K_base32); +} + + +Test(totp_boundary, test_min_period) { + const char *K = "12345678901234567890"; + + cotp_error_t cotp_err; + char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); + + cotp_error_t err; + char *totp = get_totp_at (K_base32, 59, 6, 1, COTP_SHA1, &err); + cr_expect_eq (err, NO_ERROR, "Expected %d to be equal to %d\n", err, NO_ERROR); + cr_assert_not_null (totp); + + free (totp); + free (K_base32); +} + + +Test(totp_boundary, test_max_period) { + const char *K = "12345678901234567890"; + + cotp_error_t cotp_err; + char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); + + cotp_error_t err; + char *totp = get_totp_at (K_base32, 59, 6, 120, COTP_SHA1, &err); + cr_expect_eq (err, NO_ERROR, "Expected %d to be equal to %d\n", err, NO_ERROR); + cr_assert_not_null (totp); + + free (totp); + free (K_base32); +} + + +Test(totp_boundary, test_period_over_max) { + const char *K = "12345678901234567890"; + + cotp_error_t cotp_err; + char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); + + cotp_error_t err; + char *totp = get_totp_at (K_base32, 59, 6, 121, COTP_SHA1, &err); + cr_expect_eq (err, INVALID_PERIOD, "Expected %d to be equal to %d\n", err, INVALID_PERIOD); + cr_assert_null (totp); + + free (K_base32); +} + + +Test(totp_boundary, test_digits_below_min) { + const char *K = "12345678901234567890"; + + cotp_error_t cotp_err; + char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); + + cotp_error_t err; + char *totp = get_totp_at (K_base32, 59, 3, 30, COTP_SHA1, &err); + cr_expect_eq (err, INVALID_DIGITS, "Expected %d to be equal to %d\n", err, INVALID_DIGITS); + cr_assert_null (totp); + + free (K_base32); +} + + +Test(totp_boundary, test_digits_above_max) { + const char *K = "12345678901234567890"; + + cotp_error_t cotp_err; + char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); + + cotp_error_t err; + char *totp = get_totp_at (K_base32, 59, 11, 30, COTP_SHA1, &err); + cr_expect_eq (err, INVALID_DIGITS, "Expected %d to be equal to %d\n", err, INVALID_DIGITS); + cr_assert_null (totp); + + free (K_base32); +} + + +// Context API tests (T2) +Test(ctx_api, test_create_valid) { + cotp_ctx *ctx = cotp_ctx_create (6, 30, COTP_SHA1); + cr_assert_not_null (ctx); + cotp_ctx_free (ctx); +} + + +Test(ctx_api, test_create_all_algos) { + cotp_ctx *ctx1 = cotp_ctx_create (6, 30, COTP_SHA1); + cotp_ctx *ctx2 = cotp_ctx_create (6, 30, COTP_SHA256); + cotp_ctx *ctx3 = cotp_ctx_create (6, 30, COTP_SHA512); + cr_assert_not_null (ctx1); + cr_assert_not_null (ctx2); + cr_assert_not_null (ctx3); + cotp_ctx_free (ctx1); + cotp_ctx_free (ctx2); + cotp_ctx_free (ctx3); +} + + +Test(ctx_api, test_create_invalid_digits) { + cotp_ctx *ctx = cotp_ctx_create (3, 30, COTP_SHA1); + cr_assert_null (ctx); + + ctx = cotp_ctx_create (11, 30, COTP_SHA1); + cr_assert_null (ctx); +} + + +Test(ctx_api, test_create_invalid_period) { + cotp_ctx *ctx = cotp_ctx_create (6, 0, COTP_SHA1); + cr_assert_null (ctx); + + ctx = cotp_ctx_create (6, -1, COTP_SHA1); + cr_assert_null (ctx); + + ctx = cotp_ctx_create (6, 121, COTP_SHA1); + cr_assert_null (ctx); +} + + +Test(ctx_api, test_create_invalid_algo) { + cotp_ctx *ctx = cotp_ctx_create (6, 30, 3); + cr_assert_null (ctx); + + ctx = cotp_ctx_create (6, 30, -1); + cr_assert_null (ctx); +} + + +Test(ctx_api, test_totp_at) { + const char *K = "12345678901234567890"; + const char *expected_totp = "94287082"; + + cotp_error_t cotp_err; + char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); + + cotp_ctx *ctx = cotp_ctx_create (8, 30, COTP_SHA1); + cr_assert_not_null (ctx); + + cotp_error_t err; + char *totp = cotp_ctx_totp_at (ctx, K_base32, 59, &err); + cr_expect_eq (err, NO_ERROR, "Expected %d to be equal to %d\n", err, NO_ERROR); + cr_expect_str_eq (totp, expected_totp, "Expected %s to be equal to %s\n", totp, expected_totp); + + free (totp); + cotp_ctx_free (ctx); + free (K_base32); +} + + +Test(ctx_api, test_null_ctx) { + cotp_error_t err; + char *totp = cotp_ctx_totp_at (NULL, "secret", 59, &err); + cr_expect_eq (err, INVALID_USER_INPUT, "Expected %d to be equal to %d\n", err, INVALID_USER_INPUT); + cr_assert_null (totp); + + totp = cotp_ctx_totp (NULL, "secret", &err); + cr_expect_eq (err, INVALID_USER_INPUT, "Expected %d to be equal to %d\n", err, INVALID_USER_INPUT); + cr_assert_null (totp); +} + + +Test(ctx_api, test_free_null) { + cotp_ctx_free (NULL); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libcotp-4.0.0/tests/test_validation.c new/libcotp-4.0.1/tests/test_validation.c --- old/libcotp-4.0.0/tests/test_validation.c 1970-01-01 01:00:00.000000000 +0100 +++ new/libcotp-4.0.1/tests/test_validation.c 2026-04-15 16:33:19.000000000 +0200 @@ -0,0 +1,101 @@ +#include <criterion/criterion.h> +#include <string.h> +#include "../src/cotp.h" + +#ifdef COTP_ENABLE_VALIDATION + +Test(validation, test_exact_match) { + const char *K = "12345678901234567890"; + + cotp_error_t cotp_err; + char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); + + cotp_error_t err; + char *totp = get_totp_at (K_base32, 59, 8, 30, COTP_SHA1, &err); + cr_assert_not_null (totp); + + int matched_delta = -999; + int result = validate_totp_in_window (totp, K_base32, 59, 8, 30, COTP_SHA1, 1, &matched_delta, &err); + cr_expect_eq (result, 1, "Expected match\n"); + cr_expect_eq (matched_delta, 0, "Expected delta 0, got %d\n", matched_delta); + cr_expect_eq (err, VALID, "Expected VALID, got %d\n", err); + + free (totp); + free (K_base32); +} + + +Test(validation, test_no_match) { + const char *K = "12345678901234567890"; + + cotp_error_t cotp_err; + char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); + + cotp_error_t err; + int matched_delta = -999; + int result = validate_totp_in_window ("00000000", K_base32, 59, 8, 30, COTP_SHA1, 0, &matched_delta, &err); + cr_expect_eq (result, 0, "Expected no match\n"); + + free (K_base32); +} + + +Test(validation, test_window_offset_match) { + const char *K = "12345678901234567890"; + + cotp_error_t cotp_err; + char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); + + // Generate TOTP for timestamp 59 + cotp_error_t err; + char *totp = get_totp_at (K_base32, 59, 8, 30, COTP_SHA1, &err); + cr_assert_not_null (totp); + + // Validate at timestamp 59+30=89 (one period later) with window=1 should still match at delta=-1 + int matched_delta = -999; + int result = validate_totp_in_window (totp, K_base32, 89, 8, 30, COTP_SHA1, 1, &matched_delta, &err); + cr_expect_eq (result, 1, "Expected match within window\n"); + cr_expect_eq (matched_delta, -1, "Expected delta -1, got %d\n", matched_delta); + + free (totp); + free (K_base32); +} + + +Test(validation, test_null_user_code) { + cotp_error_t err; + int result = validate_totp_in_window (NULL, "secret", 59, 8, 30, COTP_SHA1, 1, NULL, &err); + cr_expect_eq (result, 0); + cr_expect_eq (err, INVALID_USER_INPUT); +} + + +Test(validation, test_null_secret) { + cotp_error_t err; + int result = validate_totp_in_window ("12345678", NULL, 59, 8, 30, COTP_SHA1, 1, NULL, &err); + cr_expect_eq (result, 0); + cr_expect_eq (err, INVALID_USER_INPUT); +} + + +Test(validation, test_negative_window) { + const char *K = "12345678901234567890"; + + cotp_error_t cotp_err; + char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, &cotp_err); + + cotp_error_t err; + char *totp = get_totp_at (K_base32, 59, 8, 30, COTP_SHA1, &err); + cr_assert_not_null (totp); + + // Negative window should be normalized to positive + int matched_delta = -999; + int result = validate_totp_in_window (totp, K_base32, 59, 8, 30, COTP_SHA1, -2, &matched_delta, &err); + cr_expect_eq (result, 1, "Expected match with negative window\n"); + cr_expect_eq (matched_delta, 0, "Expected delta 0, got %d\n", matched_delta); + + free (totp); + free (K_base32); +} + +#endif // COTP_ENABLE_VALIDATION
