diff --git a/autotest/SecureRandomGenerator/SecureRandomGenerator.cpp b/autotest/SecureRandomGenerator/SecureRandomGenerator.cpp new file mode 100644 index 000000000..bd070dc22 --- /dev/null +++ b/autotest/SecureRandomGenerator/SecureRandomGenerator.cpp @@ -0,0 +1,233 @@ +#include +#include + +using namespace Upp; + +CONSOLE_APP_MAIN +{ + StdLogSetup(LOG_COUT | LOG_FILE); + + auto Test = [](const String& name, const Function& fn) { + String txt = "---" + name + ": "; + try { + fn(); + txt << "PASSED"; + } + catch(...) { + txt << "FAILED"; + } + LOG(txt); + }; + + Test("Basic functionality", [] { + ASSERT(SecureNonce(16).GetSize() == 16); + ASSERT(SecureNonce(64).GetSize() == 64); + ASSERT(SecureNonce(12).GetSize() == 12); + ASSERT(!SecureNonce(16).IsEmpty()); + ASSERT(SecureNonce(1).GetSize() == 12); // Enforce minimum + ASSERT(SecureRandom(0).GetSize() == 1); // Enforce minimum + }); + + Test("Standard nonce helpers, length check", [] { + ASSERT(GetAESGCMNonce().GetSize() == 12); + ASSERT(GetChaChaPoly1305Nonce().GetSize() == 12); + ASSERT(GetTLSNonce().GetSize() == 12); + ASSERT(GetAESCCMNonce().GetSize() == 13); + ASSERT(GetJWTNonce().GetSize() == 16); + ASSERT(GetOAuthNonce().GetSize() == 16); + ASSERT(GetOCSPNonce().GetSize() == 20); + ASSERT(GetECDSANonce().GetSize() == 32); + ASSERT(GetDTLSCookie().GetSize() == 32); + }); + + Test("SecureRandom basic checks", [] { + auto buf = SecureRandom(32); + ASSERT(buf.GetSize() == 32); + ASSERT(!buf.IsEmpty()); + + // Verify it's not all zeros + bool has_nonzero = false; + for(size_t i = 0; i < buf.GetSize(); i++) { + if(buf[i] != 0) { + has_nonzero = true; + break; + } + } + ASSERT(has_nonzero); + }); + + Test("Uniqueness (single-threaded)", [] { + const int NONCE_COUNT = 1000; + Vector nonces; + nonces.Reserve(NONCE_COUNT); + + for(int i = 0; i < NONCE_COUNT; i++) { + auto buf = SecureNonce(12); + nonces.Add(String((const char*)~buf, buf.GetSize())); + } + + Sort(nonces); + for(int i = 1; i < nonces.GetCount(); i++) + ASSERT(nonces[i] != nonces[i - 1]); + }); + + Test("Uniqueness (multi-threaded)", [] { + const int THREAD_COUNT = CPU_Cores(); + const int NONCES_PER_THREAD = 100000; + Vector all_nonces; + + CoFor(THREAD_COUNT, [&all_nonces](int n) { + Vector nonces; + nonces.Reserve(NONCES_PER_THREAD); + + for(int i = 0; i < NONCES_PER_THREAD; i++) { + auto buf = SecureNonce(12); + nonces.Add(String((const char*)~buf, buf.GetSize())); + } + + CoWork::FinLock(); + all_nonces.AppendPick(pick(nonces)); + }); + + ASSERT(all_nonces.GetCount() == THREAD_COUNT * NONCES_PER_THREAD); + Sort(all_nonces); + + for(int i = 1; i < all_nonces.GetCount(); i++) + ASSERT(all_nonces[i] != all_nonces[i - 1]); + }); + + Test("Verify nonce internal structure (12-15 byte nonces)", [] { + auto nonce1 = SecureNonce(12); + auto nonce2 = SecureNonce(12); + + // First 4 bytes (process ID) should be identical + ASSERT(memcmp(~nonce1, ~nonce2, 4) == 0); + + // Next 8 bytes (counter) should differ + uint64 counter1 = Peek64(~nonce1 + 4); + uint64 counter2 = Peek64(~nonce2 + 4); + ASSERT(counter1 != counter2); + + // Expect sequential or very close counters + // Allow for other threads potentially getting nonces in between + uint64 diff = (counter2 > counter1) ? (counter2 - counter1) : (counter1 - counter2); + ASSERT(diff <= 100); + }); + + Test("Verify nonce internal structure (16+ byte nonces)", [] { + auto nonce1 = SecureNonce(16); + auto nonce2 = SecureNonce(16); + + // First 8 bytes (process ID) should be identical + ASSERT(memcmp(~nonce1, ~nonce2, 8) == 0); + + // Next 8 bytes (counter) should differ + uint64 counter1 = Peek64(~nonce1 + 8); + uint64 counter2 = Peek64(~nonce2 + 8); + ASSERT(counter1 != counter2); + + uint64 diff = (counter2 > counter1) ? (counter2 - counter1) : (counter1 - counter2); + ASSERT(diff <= 100); + }); + + Test("Verify nonce entropy (using chi-square method)", [] { + const int NONCE_SIZE = 32; // Total nonce size + const int RANDOM_OFFSET = 16; // Skip 8B PID + 8B counter + const int RANDOM_SIZE = NONCE_SIZE - RANDOM_OFFSET; + const int SAMPLE_COUNT = 1000; + const double CHI_SQUARE_THRESHOLD = 350.0; // 99% confidence + + String random_bytes; + random_bytes.Reserve(SAMPLE_COUNT * RANDOM_SIZE); + + // Generate samples + for(int i = 0; i < SAMPLE_COUNT; ++i) { + auto nonce = SecureNonce(NONCE_SIZE); + random_bytes.Cat((const char*)(~nonce + RANDOM_OFFSET), RANDOM_SIZE); + } + + // Frequency analysis + Vector freq(256, 0); + const byte* data = (const byte*)(const char*)random_bytes; + for(int i = 0; i < random_bytes.GetLength(); ++i) + freq[data[i]]++; + + // Chi-square test + double expected = random_bytes.GetLength() / 256.0; + double chi2 = 0.0; + for(int count : freq) { + double delta = count - expected; + chi2 += (delta * delta) / expected; + } + + ASSERT(chi2 < CHI_SQUARE_THRESHOLD); + }); + + Test("Verify different nonce sizes use correct layouts", [] { + // 12-byte nonce: [4B PID | 8B counter] + auto nonce12 = SecureNonce(12); + ASSERT(nonce12.GetSize() == 12); + + // 14-byte nonce: [4B PID | 8B counter | 2B random] + auto nonce14 = SecureNonce(14); + ASSERT(nonce14.GetSize() == 14); + + // 16-byte nonce: [8B PID | 8B counter] + auto nonce16 = SecureNonce(16); + ASSERT(nonce16.GetSize() == 16); + + // 32-byte nonce: [8B PID | 8B counter | 16B random] + auto nonce32 = SecureNonce(32); + ASSERT(nonce32.GetSize() == 32); + + // Verify PID portions match where expected + // For <16 byte nonces, compare first 4 bytes + ASSERT(memcmp(~nonce12, ~nonce14, 4) == 0); + + // For >=16 byte nonces, compare first 8 bytes + ASSERT(memcmp(~nonce16, ~nonce32, 8) == 0); + }); + + Test("Concurrent nonce generation stress test", [] { + const int THREAD_COUNT = 16; + const int NONCES_PER_THREAD = 10000; + std::atomic total_generated{0}; + + CoFor(THREAD_COUNT, [&total_generated](int n) { + for(int i = 0; i < NONCES_PER_THREAD; i++) { + auto nonce = SecureNonce(16); + ASSERT(nonce.GetSize() == 16); + ASSERT(!nonce.IsEmpty()); + } + total_generated += NONCES_PER_THREAD; + }); + + ASSERT(total_generated == THREAD_COUNT * NONCES_PER_THREAD); + }); + + Test("Helper functions return correct types", [] { + // Verify all helpers return SecureBuffer + auto gcm = GetAESGCMNonce(); + auto chacha = GetChaChaPoly1305Nonce(); + auto tls = GetTLSNonce(); + auto ccm = GetAESCCMNonce(); + auto jwt = GetJWTNonce(); + auto oauth = GetOAuthNonce(); + auto ocsp = GetOCSPNonce(); + auto ecdsa = GetECDSANonce(); + auto dtls = GetDTLSCookie(); + + // All should be non-empty + ASSERT(!gcm.IsEmpty()); + ASSERT(!chacha.IsEmpty()); + ASSERT(!tls.IsEmpty()); + ASSERT(!ccm.IsEmpty()); + ASSERT(!jwt.IsEmpty()); + ASSERT(!oauth.IsEmpty()); + ASSERT(!ocsp.IsEmpty()); + ASSERT(!ecdsa.IsEmpty()); + ASSERT(!dtls.IsEmpty()); + }); + + LOG("=== All tests completed ==="); +} \ No newline at end of file diff --git a/autotest/SecureRandomGenerator/SecureRandomGenerator.upp b/autotest/SecureRandomGenerator/SecureRandomGenerator.upp new file mode 100644 index 000000000..fc3c36a64 --- /dev/null +++ b/autotest/SecureRandomGenerator/SecureRandomGenerator.upp @@ -0,0 +1,10 @@ +uses + Core, + Core/SSL; + +file + SecureRandomGenerator.cpp; + +mainconfig + "" = ""; + diff --git a/uppsrc/Core/SSL/Random.cpp b/uppsrc/Core/SSL/Random.cpp new file mode 100644 index 000000000..5c2159a8b --- /dev/null +++ b/uppsrc/Core/SSL/Random.cpp @@ -0,0 +1,146 @@ +#include "SSL.h" + +#define LLOG(x) // DLOG("SecureRandomGenerator: " << x) + +namespace Upp { + +namespace { + +std::atomic sForked(false); +std::atomic sId(0); +std::atomic sCounter(0); +SpinLock sLock; + +constexpr const int NONCE_MIN = 12; +constexpr const int NONCE_STRUCTURED_MIN = 16; + +inline void FillRandom(void* ptr, int len) +{ + if(len <= 0) + return; +#if OPENSSL_VERSION_NUMBER < 0x10100000L + if(RAND_status() != 1) { + RAND_poll(); + if(RAND_status() != 1) + throw Exc("SecureRandom: RNG not seeded"); + } +#endif + if(RAND_bytes(reinterpret_cast(ptr), len) != 1) + throw Exc("SecureRandom: RAND_bytes failed"); +} + +void Init() +{ + static_assert(sizeof(uint64) == 8, "Secure random/nonce generator requires 64-bit integers"); + + SslInitThread(); + + ONCELOCK { + uint32 seed = 0; + FillRandom(&seed, sizeof(seed)); + sCounter = (uint64) seed; +#ifdef PLATFORM_POSIX + pthread_atfork(nullptr, nullptr, [] { + sForked = true; + #if OPENSSL_VERSION_NUMBER < 0x10100000L + RAND_cleanup(); + #endif + }); +#endif + } +} + + +void HandleFork() +{ +#ifdef PLATFORM_POSIX + if(!sForked.load()) + return; + // After fork(), child inherits RNG state. We must reseed once to avoid + // nonce/counter reuse. SpinLock ensures only one thread performs reseed + // while others wait until state becomes consistent. + SpinLock::Lock __(sLock); + if(sForked.load()) { + uint32 seed = 0; + FillRandom(&seed, sizeof(seed)); + sCounter = (uint64) seed; + sId = 0; + sForked = false; + } +#endif +} + +uint64 GetNonceDomainId() +{ + if(uint64 v = sId.load(); v) + return v; + + uint64 x = 0; + FillRandom(&x, sizeof(x)); + if(!x) x = 1; + + uint64 expected = 0; + if(sId.compare_exchange_strong(expected, x)) + return x; + + return sId.load(); +} + +uint64 NextCounter() +{ + // simple atomic increment is enough here + uint64 v = ++sCounter; + if(v == 0) + throw Exc("SecureRandom: counter overflow"); + return v; +} + +} + +SecureBuffer SecureRandom(int n) +{ + Init(); + HandleFork(); + n = max(1, n); + SecureBuffer out(n); + FillRandom(~out, n); + return pick(out); +} + +SecureBuffer SecureNonce(int n) +{ + Init(); + HandleFork(); + uint64 did = GetNonceDomainId(); + uint64 cnt = NextCounter(); + + n = max(n, NONCE_MIN); + SecureBuffer out(n); + + byte *p = ~out; + + // 12-15 byte layout + // 4 bytes PID | 8 bytes counter | [random tail] + if(n < NONCE_STRUCTURED_MIN) { + Poke32(p, (dword) did); + p += sizeof(dword); + Poke64(p, (int64) cnt); + p += sizeof(int64); + if(int len = n - NONCE_MIN; len > 0) + FillRandom(p, len); + return pick(out); + } + + // 16-byte structured layout + // 8 bytes PID | 8 bytes counter | [random tail] + Poke64(p, (int64) did); + p += sizeof(int64); + Poke64(p, (int64) cnt); + p += sizeof(int64); + if(int len = n - NONCE_STRUCTURED_MIN; len > 0) + FillRandom(p, len); + return pick(out); +} + + +} diff --git a/uppsrc/Core/SSL/SSL.h b/uppsrc/Core/SSL/SSL.h index 1c14a4478..bae2155d2 100644 --- a/uppsrc/Core/SSL/SSL.h +++ b/uppsrc/Core/SSL/SSL.h @@ -185,4 +185,21 @@ bool AES256Decrypt(Stream& in, const String& password, Stream& out, Gate SecureRandom(int n); +SecureBuffer SecureNonce(int n); + +inline SecureBuffer GetAESGCMNonce() { return SecureNonce(12); } // 12 bytes, optimal for AES-GCM +inline SecureBuffer GetChaChaPoly1305Nonce() { return SecureNonce(12); } // 12 bytes, standard for ChaCha20-Poly1305 +inline SecureBuffer GetTLSNonce() { return SecureNonce(12); } // 12 bytes, used in TLS 1.2/1.3 +inline SecureBuffer GetAESCCMNonce() { return SecureNonce(13); } // 13 bytes, max size for AES-CCM +inline SecureBuffer GetJWTNonce() { return SecureNonce(16); } // 16 bytes, good for JWT +inline SecureBuffer GetOAuthNonce() { return SecureNonce(16); } // 16 bytes, common for OAuth +inline SecureBuffer GetOCSPNonce() { return SecureNonce(20); } // 20 bytes, OCSP nonce extension +inline SecureBuffer GetECDSANonce() { return SecureNonce(32); } // 32 bytes, for ECDSA signatures +inline SecureBuffer GetDTLSCookie() { return SecureNonce(32); } // 32 bytes, DTLS cookie + } diff --git a/uppsrc/Core/SSL/SSL.upp b/uppsrc/Core/SSL/SSL.upp index e667f1781..783364bd2 100644 --- a/uppsrc/Core/SSL/SSL.upp +++ b/uppsrc/Core/SSL/SSL.upp @@ -14,6 +14,7 @@ file P7S.cpp, AES.cpp, Buffer.hpp, + Random.cpp, SSL.icpp, Docs readonly separator, src.tpp, diff --git a/uppsrc/Core/SSL/src.tpp/Upp_SSL_Random_en-us.tpp b/uppsrc/Core/SSL/src.tpp/Upp_SSL_Random_en-us.tpp new file mode 100644 index 000000000..00b619526 --- /dev/null +++ b/uppsrc/Core/SSL/src.tpp/Upp_SSL_Random_en-us.tpp @@ -0,0 +1,69 @@ +topic "Secure random data and nonce generation"; +[i448;a25;kKO9;2 $$1,0#37138531426314131252341829483380:class] +[l288;2 $$2,2#27521748481378242620020725143825:desc] +[0 $$3,0#96390100711032703541132217272105:end] +[H6;0 $$4,0#05600065144404261032431302351956:begin] +[i448;a25;kKO9;2 $$5,0#37138531426314131252341829483370:item] +[l288;a4;*@5;1 $$6,6#70004532496200323422659154056402:requirement] +[l288;i1121;b17;O9;~~~.1408;2 $$7,0#10431211400427159095818037425705:param] +[i448;b42;O9;2 $$8,8#61672508125594000341940100500538:tparam] +[b42;2 $$9,9#13035079074754324216151401829390:normal] +[2 $$0,0#00000000000000000000000000000000:Default] +[{_} +[ {{10000@(113.42.0) [s0;%% [*@7;4 Secure Random and Nonce Generators]]}}&] +[s2; &] +[s2;%% These functions provides a cryptographically secure random +number and nonces compliant with NIST SP 800`-38D, tailored for +high`-security applications that demand guaranteed uniqueness +and strong collision resistance. The implementation ensures +process`-unique nonces and is fork`-safe on POSIX systems, automatically +reseeding after a fork to avoid duplication. &] +[s2;%% &] +[s2;%% The implementation is [/ thread`-safe], supporting concurrent +initialization and generation across threads without race conditions. +It enforces a minimum nonce size of 12 bytes, aligning with cryptographic +standards. &] +[s2;%% &] +[s2;%% The generator offers two distinct modes: one for producing +unique, non`-repeating nonces, and another for extracting purely +random data suitable for general`-purpose cryptographic use.&] +[s2; &] +[s3; &] +[ {{10000F(128)G(128)@1 [s0;%% [* Function List]]}}&] +[s3; &] +[s5;:Upp`:`:SecureRandom`(int`): SecureBuffer<[@(0.255.0) byte]> [* SecureRandom]([@(0.0.255) i +nt] [*@3 n])&] +[s2;%% Generates [%-*@3 n] cryptographically secure random bytes. Enforces +a minimum size of 1 bytes. Throws [^topic`:`/`/Core`/src`/Exc`_en`-us`#Exc`:`:class^ e +xception ]on failure.&] +[s3; &] +[s4; &] +[s5;:Upp`:`:SecureNonce`(int`): SecureBuffer<[@(0.255.0) byte]> [* SecureNonce]([@(0.0.255) i +nt] [*@3 n])&] +[s0;l288;%% Generates a secure nonce of [%-*@3 n] bytes. Enforces a +minimum size of 12 bytes. Throws [^topic`:`/`/Core`/src`/Exc`_en`-us`#Exc`:`:class^ e +xception ]on failure. The returned value is a structured binary +nonce produced from internal&] +[s2;%% domain, counter, and optional entropy components. All internal +multi`-byte fields are encoded using little`-endian layout.&] +[s3; &] +[ {{10000F(128)G(128)@1 [s0;%% [* Standard Nonce Helpers]]}}&] +[s3; &] +[s5;:Upp`:`:GetAESGCMNonce`(`): SecureBuffer<[@(0.255.0) byte]> [* GetAESGCMNonce]()&] +[s5;:Upp`:`:GetChaChaPoly1305Nonce`(`): SecureBuffer<[@(0.255.0) byte]> +[* GetChaChaPoly1305Nonce]()&] +[s5;:Upp`:`:GetTLSNonce`(`): SecureBuffer<[@(0.255.0) byte]> [* GetTLSNonce]()&] +[s5;:Upp`:`:GetAESCCMNonce`(`): SecureBuffer<[@(0.255.0) byte]> [* GetAESCCMNonce]()&] +[s5;:Upp`:`:GetJWTNonce`(`): SecureBuffer<[@(0.255.0) byte]> [* GetJWTNonce]()&] +[s5;:Upp`:`:GetOAuthNonce`(`): SecureBuffer<[@(0.255.0) byte]> [* GetOAuthNonce]()&] +[s5;:Upp`:`:GetOCSPNonce`(`): SecureBuffer<[@(0.255.0) byte]> [* GetOCSPNonce]()&] +[s5;:Upp`:`:GetECDSANonce`(`): SecureBuffer<[@(0.255.0) byte]> [* GetECDSANonce]()&] +[s5;:Upp`:`:GetDTLSCookie`(`): SecureBuffer<[@(0.255.0) byte]> [* GetDTLSCookie]()&] +[s2;%% These helper functions generate cryptographically secure nonces +of commonly required lengths for widely used protocols and standards +such as AES`-GCM, ChaCha20`-Poly1305, TLS, JWT, and ECDSA. Each +helper ensures the nonce meets the expected size and uniqueness +guarantees of its respective use case. Each helper throws [^topic`:`/`/Core`/src`/Exc`_en`-us`#Exc`:`:class^ e +xception ]on failure.&] +[s3; &] +[s0;%% ]] \ No newline at end of file