From ca43d10cf24629755baf791cfbaf1cfb2787ff4b Mon Sep 17 00:00:00 2001 From: Augusto Alejandro Gomez Saa Date: Tue, 18 Aug 2026 02:11:21 -0300 Subject: [PATCH] Add API to install trusted CA certs in the open source build In the open source build there was no way to install a root of trust: CertStore_AddCertFromBase64 had no callers, the cert store only ever contained the hardcoded Valve root CA key, and with that key defined, dynamically added self-signed certs were always rejected. So the verification path could never succeed for certs issued by anybody else, and manually-issued certs (issue #237) were unusable. Add GameNetworkingSockets_AddTrustedCert, which accepts a PEM-like blob or the raw base64 body of one, and installs a cert in the trust store. Certs installed this way are marked as explicitly trusted by the app and may act as self-signed trusted roots, coexisting with the hardcoded root CA key, which remains trusted. Certs that arrive any other way are rejected exactly as before, so the hardcoded-root invariant is unchanged for certs the app didn't vouch for. The same function installs intermediate certs. (The wire format doesn't carry the chain, so the verifying side needs the whole chain in its store.) Also add GameNetworkingSockets_SetAppID. Certificates are authorized for particular app IDs and both ends check certs against their own app ID, but in the open source build there was no way to set it, so it was always 0 and those checks could never pass. Resurrect test_pki, which was added in 0da061c but never hooked up to the build. Its checked-in certs were signed by a root key that does not match the current hardcoded key, so the corpus could not be revived; the test now generates its PKI at runtime instead, which also removes the frozen timestamps. Hook it up to CI on all platforms. This is the first part of issue #237. Co-Authored-By: Claude Fable 5 --- .github/build.sh | 14 +- .github/run-single-config.py | 2 +- .github/workflows/build-ubuntu.yml | 21 +- .github/workflows/build-windows.yml | 5 + .github/workflows/macos.yml | 5 + include/steam/steamnetworkingsockets.h | 28 ++ .../clientlib/csteamnetworkingsockets.cpp | 12 + .../steamnetworkingsockets_certstore.cpp | 55 +++- .../steamnetworkingsockets_certstore.h | 7 + tests/CMakeLists.txt | 10 + tests/test_pki.cpp | 309 +++++++++++------- 11 files changed, 332 insertions(+), 136 deletions(-) diff --git a/.github/build.sh b/.github/build.sh index ed582302..5dac4d78 100755 --- a/.github/build.sh +++ b/.github/build.sh @@ -111,13 +111,13 @@ run_matrix_for_current_compiler() { # Build some tests with sanitizers if [[ ${build_sanitizers} -ne 0 ]]; then - run_single --compiler "${compiler}" --build-dir build-asan --sanitizer asan --targets test_connection test_crypto --run-tests + run_single --compiler "${compiler}" --build-dir build-asan --sanitizer asan --targets test_connection test_crypto test_pki --run-tests run_single --compiler "${compiler}" --build-dir build-asan --sanitizer asan \ --run-tests --tests test_connection:soak \ --phase test - run_single --compiler "${compiler}" --build-dir build-ubsan --sanitizer ubsan --targets test_connection test_crypto --run-tests + run_single --compiler "${compiler}" --build-dir build-ubsan --sanitizer ubsan --targets test_connection test_crypto test_pki --run-tests if [[ ${CXX} == *clang* ]]; then - run_single --compiler "${compiler}" --build-dir build-tsan --sanitizer tsan --targets test_connection test_crypto --run-tests + run_single --compiler "${compiler}" --build-dir build-tsan --sanitizer tsan --targets test_connection test_crypto test_pki --run-tests fi fi @@ -127,7 +127,7 @@ run_matrix_for_current_compiler() { run_single --compiler "${compiler}" --build-dir build-tsan \ --sanitizer tsan \ --crypto libsodium --crypto25519 libsodium \ - --targets test_connection test_crypto \ + --targets test_connection test_crypto test_pki \ --run-tests fi @@ -151,14 +151,14 @@ run_matrix_for_current_compiler() { fi # Build binaries with reference ed25519/curve25519 - run_single --compiler "${compiler}" --build-dir build-cmake-ref --build-type RelWithDebInfo --crypto25519 Reference --run-tests --tests test_crypto + run_single --compiler "${compiler}" --build-dir build-cmake-ref --build-type RelWithDebInfo --crypto25519 Reference --run-tests --tests test_crypto test_pki # Build binaries with libsodium for ed25519/curve25519 only - run_single --compiler "${compiler}" --build-dir build-cmake-sodium25519 --build-type RelWithDebInfo --crypto25519 libsodium --run-tests --tests test_crypto + run_single --compiler "${compiler}" --build-dir build-cmake-sodium25519 --build-type RelWithDebInfo --crypto25519 libsodium --run-tests --tests test_crypto test_pki # Build binaries with libsodium if [[ ${build_libsodium} -ne 0 ]]; then - run_single --compiler "${compiler}" --build-dir build-cmake-sodium --build-type RelWithDebInfo --crypto libsodium --crypto25519 libsodium --run-tests --tests test_crypto + run_single --compiler "${compiler}" --build-dir build-cmake-sodium --build-type RelWithDebInfo --crypto libsodium --crypto25519 libsodium --run-tests --tests test_crypto test_pki fi } diff --git a/.github/run-single-config.py b/.github/run-single-config.py index f212236f..7c11e301 100755 --- a/.github/run-single-config.py +++ b/.github/run-single-config.py @@ -68,7 +68,7 @@ def compiler_env(compiler: str, base_env: dict[str, str]) -> dict[str, str]: def parse_test_specs(specs: list[str] | None) -> list[list[str]]: if specs is None: - specs = ["test_crypto", "test_connection:suite-quick"] + specs = ["test_crypto", "test_pki", "test_connection:suite-quick"] commands: list[list[str]] = [] for spec in specs: diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml index ab236357..3251a8e8 100644 --- a/.github/workflows/build-ubuntu.yml +++ b/.github/workflows/build-ubuntu.yml @@ -16,35 +16,35 @@ jobs: use_webrtc: false crypto: default crypto25519: default - targets: "test_connection test_p2p trivial_signaling_server publish_test_script test_crypto" + targets: "test_connection test_p2p trivial_signaling_server publish_test_script test_crypto test_pki" - row: gcc-openssl-crypto compiler: gcc sanitizer: none use_webrtc: false crypto: default crypto25519: default - targets: "test_connection test_p2p trivial_signaling_server publish_test_script test_crypto" + targets: "test_connection test_p2p trivial_signaling_server publish_test_script test_crypto test_pki" - row: clang-libsodium compiler: clang sanitizer: none use_webrtc: false crypto: libsodium crypto25519: libsodium - targets: "test_connection test_p2p trivial_signaling_server publish_test_script test_crypto" + targets: "test_connection test_p2p trivial_signaling_server publish_test_script test_crypto test_pki" - row: clang-reference-25519 compiler: clang sanitizer: none use_webrtc: false crypto: default crypto25519: Reference - targets: "test_connection test_p2p trivial_signaling_server publish_test_script test_crypto" + targets: "test_connection test_p2p trivial_signaling_server publish_test_script test_crypto test_pki" - row: clang-openssl-webrtc compiler: clang sanitizer: none use_webrtc: true crypto: default crypto25519: default - targets: "test_connection test_p2p trivial_signaling_server publish_test_script test_crypto" + targets: "test_connection test_p2p trivial_signaling_server publish_test_script test_crypto test_pki" env: CI_BUILD: 1 IMAGE: ubuntu @@ -100,6 +100,17 @@ jobs: --run-tests \ --tests test_crypto + - name: Test pki (RelWithDebInfo) + run: | + set -euo pipefail + python3 .github/run-single-config.py \ + --compiler "${{ matrix.compiler }}" \ + --build-dir build \ + --sanitizer "${{ matrix.sanitizer }}" \ + --phase test \ + --run-tests \ + --tests test_pki + - name: Test connection (RelWithDebInfo) run: | set -euo pipefail diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index b7d7590b..4a90ffa3 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -89,6 +89,11 @@ jobs: run: test_crypto.exe shell: cmd + - name: Test pki + working-directory: '${{ github.workspace }}/build/bin' + run: test_pki.exe + shell: cmd + - name: Test connection working-directory: '${{ github.workspace }}/build/bin' run: test_connection.exe suite-quick diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 44550dc3..39e6c0f9 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -60,6 +60,11 @@ jobs: run: ./test_crypto shell: bash + - name: Test pki + working-directory: ${{github.workspace}}/build/bin + run: ./test_pki + shell: bash + - name: Test connection working-directory: ${{github.workspace}}/build/bin run: ./test_connection suite-quick diff --git a/include/steam/steamnetworkingsockets.h b/include/steam/steamnetworkingsockets.h index 62ec17e1..6353325b 100644 --- a/include/steam/steamnetworkingsockets.h +++ b/include/steam/steamnetworkingsockets.h @@ -23,6 +23,34 @@ STEAMNETWORKINGSOCKETS_INTERFACE bool GameNetworkingSockets_Init( const SteamNet // Close all connections and listen sockets and free all resources STEAMNETWORKINGSOCKETS_INTERFACE void GameNetworkingSockets_Kill(); +/// Add a certificate to the trust store used to verify peer certificates. +/// Accepts a PEM-like blob ("STEAMDATAGRAM CERT") as emitted by the certificate +/// tool, or just the raw base64 body of one. Call it once per certificate: +/// install your root CA cert, plus any intermediate certs needed to complete +/// the chain to certs that peers will present. (Certs on the wire only carry +/// the ID of the CA key that signed them, not the chain itself, so the +/// verifying side must have the whole chain installed.) The trust store is +/// global to the process and shared by all interfaces. A self-signed cert +/// installed here becomes a trusted root, in addition to the hardcoded root +/// CA key, which remains trusted. Do not install certs bound to a particular +/// identity (e.g. a server's own cert) -- only CA certs go in the trust store. +/// +/// Note that installing certs does not, by itself, *require* peers to +/// authenticate. To reject peers without a valid certificate, set +/// k_ESteamNetworkingConfig_IP_AllowWithoutAuth to 0. Also, a certificate +/// proves that the peer's identity was certified by a trusted CA, but for +/// connections initiated by IP address the library has no expected identity +/// to compare against -- check SteamNetConnectionInfo_t::m_identityRemote +/// after connecting if you need to know *which* certified peer you are +/// talking to. +STEAMNETWORKINGSOCKETS_INTERFACE bool GameNetworkingSockets_AddTrustedCert( const char *pszCert, SteamNetworkingErrMsg &errMsg ); + +/// Set the App ID used for certificate validation. Certificates are +/// authorized for particular App IDs, and both ends check certs against +/// their own App ID. If you use certificate authentication, call this +/// before SetCertificate, on clients as well as servers. +STEAMNETWORKINGSOCKETS_INTERFACE void GameNetworkingSockets_SetAppID( AppId_t nAppID ); + /// Custom memory allocation methods. If you call this, you MUST call it exactly once, /// before calling any other API function. *Most* allocations will pass through these, /// especially all allocations that are per-connection. A few allocations diff --git a/src/steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp b/src/steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp index 02918d2a..c758d5eb 100644 --- a/src/steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp +++ b/src/steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp @@ -2531,6 +2531,18 @@ STEAMNETWORKINGSOCKETS_INTERFACE void GameNetworkingSockets_Kill() } } +STEAMNETWORKINGSOCKETS_INTERFACE bool GameNetworkingSockets_AddTrustedCert( const char *pszCert, SteamNetworkingErrMsg &errMsg ) +{ + SteamNetworkingGlobalLock lock( "GameNetworkingSockets_AddTrustedCert" ); + return CertStore_AddTrustedCertFromPEM( pszCert, errMsg ); +} + +STEAMNETWORKINGSOCKETS_INTERFACE void GameNetworkingSockets_SetAppID( AppId_t nAppID ) +{ + SteamNetworkingGlobalLock lock( "GameNetworkingSockets_SetAppID" ); + ( (CSteamNetworkingUtils *)SteamNetworkingUtils() )->SetAppID( nAppID ); +} + STEAMNETWORKINGSOCKETS_INTERFACE ISteamNetworkingSockets *SteamNetworkingSockets_LibV12() { return s_pSteamNetworkingSockets; diff --git a/src/steamnetworkingsockets/steamnetworkingsockets_certstore.cpp b/src/steamnetworkingsockets/steamnetworkingsockets_certstore.cpp index eebeee4a..6ff06b72 100644 --- a/src/steamnetworkingsockets/steamnetworkingsockets_certstore.cpp +++ b/src/steamnetworkingsockets/steamnetworkingsockets_certstore.cpp @@ -148,6 +148,11 @@ struct Cert CertAuthScope m_authScope; time_t m_timeCreated; + // Was this cert explicitly installed by the app? (As opposed to being + // received over the network, etc.) Only app-installed certs may act as + // self-signed trusted roots when a hardcoded root CA key is in use. + bool m_bTrustedByApp = false; + bool Setup( const CMsgSteamDatagramCertificateSigned &msgCertSigned, CECSigningPublicKey &outPublicKey, SteamNetworkingErrMsg &errMsg ) { m_signed_data = msgCertSigned.cert(); @@ -333,13 +338,13 @@ void CertStore_AddKeyRevocation( uint64 key_id ) s_bTrustValid = false; } -bool CertStore_AddCertFromBase64( const char *pszBase64, SteamNetworkingErrMsg &errMsg ) +static bool CertStore_AddCert( const char *pszBase64, size_t cchBase64, bool bTrustedByApp, SteamNetworkingErrMsg &errMsg ) { CertStore_OneTimeInit(); // Decode CMsgSteamDatagramCertificateSigned msgSignedCert; - if ( !ParseCertFromBase64( pszBase64, V_strlen( pszBase64 ), msgSignedCert, errMsg ) ) + if ( !ParseCertFromBase64( pszBase64, cchBase64, msgSignedCert, errMsg ) ) return false; CECSigningPublicKey publicKey; @@ -348,6 +353,7 @@ bool CertStore_AddCertFromBase64( const char *pszBase64, SteamNetworkingErrMsg & Cert cert; if ( !cert.Setup( msgSignedCert, publicKey, errMsg ) ) return false; + cert.m_bTrustedByApp = bTrustedByApp; uint64 key_id = CalculatePublicKeyID( publicKey ); PublicKey *pKey = FindPublicKey( key_id ); @@ -369,13 +375,21 @@ bool CertStore_AddCertFromBase64( const char *pszBase64, SteamNetworkingErrMsg & // Check if we already have this exact cert, // using the signature as as hash/fingerprint. - for ( const Cert &c: pKey->m_vecCerts ) + for ( Cert &c: pKey->m_vecCerts ) { if ( cert.m_signature == c.m_signature ) { Assert( cert.m_signed_data == c.m_signed_data ); Assert( cert.m_ca_key_id == c.m_ca_key_id ); Assert( cert.m_timeCreated == c.m_timeCreated ); + + // Upgrade trust if the app is now vouching for a + // cert we had previously received some other way + if ( bTrustedByApp && !c.m_bTrustedByApp ) + { + c.m_bTrustedByApp = true; + s_bTrustValid = false; + } return true; } } @@ -397,6 +411,25 @@ bool CertStore_AddCertFromBase64( const char *pszBase64, SteamNetworkingErrMsg & return true; } +bool CertStore_AddCertFromBase64( const char *pszBase64, SteamNetworkingErrMsg &errMsg ) +{ + return CertStore_AddCert( pszBase64, V_strlen( pszBase64 ), false, errMsg ); +} + +bool CertStore_AddTrustedCertFromPEM( const char *pszCert, SteamNetworkingErrMsg &errMsg ) +{ + // Locate the base64 body if this is a PEM-like blob. If it isn't, + // assume we were given the raw base64 body directly. + uint32 cchBody = (uint32)V_strlen( pszCert ); + const char *pszBody = CCrypto::LocatePEMBody( pszCert, &cchBody, "STEAMDATAGRAM CERT" ); + if ( !pszBody ) + { + pszBody = pszCert; + cchBody = (uint32)V_strlen( pszCert ); + } + return CertStore_AddCert( pszBody, cchBody, true, errMsg ); +} + template< int kMaxSize = 1024 > std::string V_sprintf_stdstring( const char *pszFmt, ... ) { @@ -457,12 +490,16 @@ static void RecursiveEvaluateKeyTrust( PublicKey *pKey ) if ( pSignerKey == pKey ) { #ifdef STEAMNETWORKINGSOCKETS_HARDCODED_ROOT_CA_KEY - // If hardcoded root cert is in use, only trust the - // one hardcoded root key. (We've already tagged it - // as trusted by hardcoded, so we don't get this far - // for those keys). - cert.m_status_msg = "Trusted root is hardcoded, cannot add more self-signed certs"; - continue; + // If hardcoded root cert is in use, only trust self-signed + // certs that the app has explicitly installed. (We've + // already tagged the hardcoded key as trusted by hardcoded, + // so we don't get this far for that key.) + if ( !cert.m_bTrustedByApp ) + { + cert.m_status_msg = "Trusted root is hardcoded, cannot add more self-signed certs"; + continue; + } + cert.m_status_msg = "(Self-signed root, installed by app)"; #else // Self signed is OK. cert.m_status_msg = "(Self-signed root)"; diff --git a/src/steamnetworkingsockets/steamnetworkingsockets_certstore.h b/src/steamnetworkingsockets/steamnetworkingsockets_certstore.h index 33c49ae7..eedd155b 100644 --- a/src/steamnetworkingsockets/steamnetworkingsockets_certstore.h +++ b/src/steamnetworkingsockets/steamnetworkingsockets_certstore.h @@ -111,6 +111,13 @@ extern void CertStore_Reset(); /// only if there was a parse error. DOES NOT check for expiry or validate any signatures, etc. extern bool CertStore_AddCertFromBase64( const char *pszBase64, SteamNetworkingErrMsg &errMsg ); +/// Add a cert that the application has explicitly asked us to trust. Accepts either a +/// PEM-like blob or the raw base-64 body of one. Unlike certs added by +/// CertStore_AddCertFromBase64, if such a cert is self-signed, it is accepted as a trusted +/// root, even when a hardcoded root CA key is in use. (The hardcoded key remains trusted.) +/// Returns false only if there was a parse error. +extern bool CertStore_AddTrustedCertFromPEM( const char *pszCert, SteamNetworkingErrMsg &errMsg ); + /// Adds a key revocation entry. extern void CertStore_AddKeyRevocation( uint64 key_id ); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9746282e..da7a73e3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,6 +30,16 @@ add_test(NAME crypto COMMAND test_crypto WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPU # Test data for the crypto test when the project is built file(COPY aesgcmtestvectors DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) +add_executable( + test_pki + test_pki.cpp + ) +set_target_common_gns_properties( test_pki ) +target_include_directories(test_pki PRIVATE ../src ../src/public ../src/common ../include "${CMAKE_BINARY_DIR}/src") +target_link_libraries(test_pki GameNetworkingSockets::static) +add_sanitizers(test_pki) +add_test(NAME pki COMMAND test_pki WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + # P2P test if(ENABLE_ICE) add_executable( diff --git a/tests/test_pki.cpp b/tests/test_pki.cpp index d62872ca..3590909a 100644 --- a/tests/test_pki.cpp +++ b/tests/test_pki.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -9,34 +10,89 @@ using namespace SteamNetworkingSocketsLib; -// Time as I write this. So that the tests will still work even after these generated keys expire. -time_t k_timeTestNow = 1555374048; +// Assert() compiles out of release builds, so track failures the same +// way test_crypto does, and make sure the process exits nonzero. +static bool g_failed = false; +#define CHECK( x ) \ + do { \ + if ( !(x) ) \ + { \ + fprintf( stderr, "CHECK failed %s:%d: %s\n", __FILE__, __LINE__, #x ); \ + g_failed = true; \ + } \ + } while ( false ) + +// All certs are generated at runtime, so we can just use the real clock. +static const time_t k_timeNow = time( nullptr ); + +// A test CA: a keypair, plus a cert for it that we can install in the cert store. +struct TestCA +{ + CECSigningPrivateKey m_keyPrivate; + CECSigningPublicKey m_keyPublic; + uint64 m_nKeyID; + std::string m_sCertBase64; // CMsgSteamDatagramCertificateSigned, base64-encoded +}; + +// Finish a cert for the given public key, sign it, and return the +// base64-encoded CMsgSteamDatagramCertificateSigned +static std::string SignCert( CMsgSteamDatagramCertificate &msgCert, const CECSigningPublicKey &keyCertPublic, const CECSigningPrivateKey &keySignerPrivate, uint64 nSignerKeyID ) +{ + msgCert.set_time_expiry( k_timeNow + 3600*8 ); + CHECK( keyCertPublic.GetRawDataAsStdString( msgCert.mutable_key_data() ) ); + msgCert.set_key_type( CMsgSteamDatagramCertificate_EKeyType_ED25519 ); + + CMsgSteamDatagramCertificateSigned msgSigned; + CHECK( msgCert.SerializeToString( msgSigned.mutable_cert() ) ); + CryptoSignature_t sig; + keySignerPrivate.GenerateSignature( msgSigned.cert().c_str(), msgSigned.cert().length(), &sig ); + msgSigned.set_ca_key_id( nSignerKeyID ); + msgSigned.set_ca_signature( &sig, sizeof(sig) ); + + std::string sSerialized; + CHECK( msgSigned.SerializeToString( &sSerialized ) ); + + uint32 cchEncoded = CCrypto::Base64EncodeMaxOutput( (uint32)sSerialized.length(), nullptr ); + std::string sBase64; + sBase64.resize( cchEncoded ); + CHECK( CCrypto::Base64Encode( sSerialized.c_str(), sSerialized.length(), &sBase64[0], &cchEncoded, nullptr ) ); + sBase64.resize( strlen( sBase64.c_str() ) ); + return sBase64; +} -void GenerateCert( CMsgSteamDatagramCertificateSigned &msgOut, const char *certData, const CECSigningPrivateKey &keyCAPrivateKey, uint64 nCAKeyID ) +// Create a CA keypair and a cert for it. pszFields is protobuf TextFormat +// for any restrictions (app_ids, gameserver_datacenter_ids); empty string +// means unrestricted. If pSigner is nullptr, the cert is self-signed. +static void MakeCA( TestCA &ca, const char *pszFields, const TestCA *pSigner ) { - msgOut.Clear(); + CCrypto::GenerateSigningKeyPair( &ca.m_keyPublic, &ca.m_keyPrivate ); + ca.m_nKeyID = CalculatePublicKeyID( ca.m_keyPublic ); + if ( !pSigner ) + pSigner = &ca; - // Generate a dummy cert with the requested fields and give it a keypair - { - CMsgSteamDatagramCertificate msgCert; - DbgVerify( google::protobuf::TextFormat::ParseFromString( std::string( certData ), &msgCert ) ); + CMsgSteamDatagramCertificate msgCert; + CHECK( google::protobuf::TextFormat::ParseFromString( std::string( pszFields ), &msgCert ) ); + ca.m_sCertBase64 = SignCert( msgCert, ca.m_keyPublic, pSigner->m_keyPrivate, pSigner->m_nKeyID ); +} - msgCert.set_time_expiry( k_timeTestNow + 3600*8 ); +// Generate an end-entity cert, with its own throwaway keypair and the +// requested fields, signed by the given CA +static void GenerateCert( CMsgSteamDatagramCertificateSigned &msgOut, const char *certData, const TestCA &ca ) +{ + msgOut.Clear(); - CECSigningPrivateKey tempIdentityPrivateKey; - CECSigningPublicKey tempIdentityPublicKey; - CCrypto::GenerateSigningKeyPair( &tempIdentityPublicKey, &tempIdentityPrivateKey ); - DbgVerify( tempIdentityPublicKey.GetRawDataAsStdString( msgCert.mutable_key_data() ) ); - msgCert.set_key_type( CMsgSteamDatagramCertificate_EKeyType_ED25519 ); + CMsgSteamDatagramCertificate msgCert; + CHECK( google::protobuf::TextFormat::ParseFromString( std::string( certData ), &msgCert ) ); - DbgVerify( msgCert.SerializeToString( msgOut.mutable_cert() ) ); - } + CECSigningPrivateKey tempIdentityPrivateKey; + CECSigningPublicKey tempIdentityPublicKey; + CCrypto::GenerateSigningKeyPair( &tempIdentityPublicKey, &tempIdentityPrivateKey ); - // Sign it - CryptoSignature_t sig; - keyCAPrivateKey.GenerateSignature( msgOut.cert().c_str(), msgOut.cert().length(), &sig ); - msgOut.set_ca_key_id( nCAKeyID ); - msgOut.set_ca_signature( &sig, sizeof(sig) ); + std::string sBase64 = SignCert( msgCert, tempIdentityPublicKey, ca.m_keyPrivate, ca.m_nKeyID ); + + // Hand back the signed message, decoded + SteamNetworkingErrMsg errMsg; + CHECK( ParseCertFromBase64( sBase64.c_str(), sBase64.length(), msgOut, errMsg ) ); } int main() @@ -44,140 +100,165 @@ int main() SteamNetworkingErrMsg errMsg; // - // Populate our cert store with some certs. - // See make_test_certs.py - // - - // Dynamic (not-hardcoded) self-signed cert - // KeyID . . . .: 8112647883641536425 - // Public key . : ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBkU/enzJscDJp0N1RbYkL0E9wXVO5krNr8rm4JDrNBE - CECSigningPrivateKey privkey_dynamic_root; - DbgVerify( privkey_dynamic_root.ParsePEM( "-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQy NTUxOQAAACAZFP3p8ybHAyadDdUW2JC9BPcF1TuZKza/K5uCQ6zQRAAAAH8SNFZ4EjRWeAAA AAtzc2gtZWQyNTUxOQAAACAZFP3p8ybHAyadDdUW2JC9BPcF1TuZKza/K5uCQ6zQRAAAAEDq vSVEpg9EZkMej6Fw1EFCuiAnNtMCTKmf8ZRXSwzrXRkU/enzJscDJp0N1RbYkL0E9wXVO5kr Nr8rm4JDrNBE -----END OPENSSH PRIVATE KEY----- ", 375 ) ); - // CA KeyID . . : 8112647883641536425 - //const uint64 k_key_dynamic_root = 8112647883641536425ull; - DbgVerify( CertStore_AddCertFromBase64( "Ii4IARIgGRT96fMmxwMmnQ3VFtiQvQT3BdU7mSs2vyubgkOs0ERFmSm1XE2ZkHdgKak/R3xE6pVwMkBlDV+UgOQHEwEg5GnlKLxK5aqKAWl8J0Eo2pl6+grtk5fitu9U15EXtkHhw1o7q8+sZFvRJw8/zXuohkzVB1AC", errMsg ) ); - - // Intermediate cert for app (CSGO), signed by hardcoded key - // KeyID . . . .: 1790264268120135407 - // Public key . : ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILumwWENaKq+n5xzAvfLgOOaeLvQqky4LzU0HI0qBnU/ - CECSigningPrivateKey privkey_csgo; - DbgVerify( privkey_csgo.ParsePEM( "-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQy NTUxOQAAACC7psFhDWiqvp+ccwL3y4Djmni70KpMuC81NByNKgZ1PwAAAH8SNFZ4EjRWeAAA AAtzc2gtZWQyNTUxOQAAACC7psFhDWiqvp+ccwL3y4Djmni70KpMuC81NByNKgZ1PwAAAEAs mu57b1o/lDSwUKD4LvIM/kQMwFIbzEbFIoyuyDEf3bumwWENaKq+n5xzAvfLgOOaeLvQqky4 LzU0HI0qBnU/ -----END OPENSSH PRIVATE KEY----- ", 375 ) ); - // CA KeyID . . : 9417917822780561193 - // Apps . . . . : [730] - const uint64 k_key_csgo = 1790264268120135407ull; - DbgVerify( CertStore_AddCertFromBase64( "IjEIARIgu6bBYQ1oqr6fnHMC98uA45p4u9CqTLgvNTQcjSoGdT9FmSm1XE2ZkHdgUNoFKSnXp45cKrOCMkCPs0eTzHWsN0oDNrxnAvvi3MiDv6Tv4CudquT4D/nss3usW6xUPD3YIbbISWxL8YE1HGYVRILCYWDCqxoBOK4M", errMsg ) ); - - // Cert for particular data center. Not specifically scoped to app, but signed by CSGO cert, so should effectively be scoped. - // KeyID . . . .: 10851291850214533835 - // Public key . : ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKd+8wfN8OYAQ+P4fdiC+7xwakeqOlDSqKY5/9wtkUim - CECSigningPrivateKey privkey_csgo_eatmwh; - DbgVerify( privkey_csgo_eatmwh.ParsePEM( "-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQy NTUxOQAAACCnfvMHzfDmAEPj+H3Ygvu8cGpHqjpQ0qimOf/cLZFIpgAAAH8SNFZ4EjRWeAAA AAtzc2gtZWQyNTUxOQAAACCnfvMHzfDmAEPj+H3Ygvu8cGpHqjpQ0qimOf/cLZFIpgAAAEA0 pWdXwJgrvazaE/69qtE0zsjQJfzshriDJxfC467ktqd+8wfN8OYAQ+P4fdiC+7xwakeqOlDS qKY5/9wtkUim -----END OPENSSH PRIVATE KEY----- ", 375 ) ); - // CA KeyID . . : 1790264268120135407 - // POPs . . . . : [u'eat', u'mwh'] - const uint64 k_key_csgo_eatmwh = 10851291850214533835ull; - DbgVerify( CertStore_AddCertFromBase64( "IjgIARIgp37zB83w5gBD4/h92IL7vHBqR6o6UNKopjn/3C2RSKYtdGFlAC1od20ARZkptVxNmZB3YCnvFmDb3UvYGDJAYXfYn+ofbs5Fz4EYiMYNh4SFD302+S/xXsAzmk8awH7nuasCV+RUWjoOshkKMK6ONCYzmkMiD0so7tOR+7zsDQ==", errMsg ) ); - - // Intermediate cert for app (TF2), signed by self-signed cert - // KeyID . . . .: 12206663272037732248 - // Public key . : ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC/nkdg+La27cA2ptQj1t0buCYoo2OAQI+lf2P/QaRq4 - CECSigningPrivateKey privkey_tf2; - DbgVerify( privkey_tf2.ParsePEM( "-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQy NTUxOQAAACAv55HYPi2tu3ANqbUI9bdG7gmKKNjgECPpX9j/0GkauAAAAH8SNFZ4EjRWeAAA AAtzc2gtZWQyNTUxOQAAACAv55HYPi2tu3ANqbUI9bdG7gmKKNjgECPpX9j/0GkauAAAAEDf 8k3ME+Xapo2rNSUTO7SLog3hNCGP4cWcvM4bnEBkwC/nkdg+La27cA2ptQj1t0buCYoo2OAQ I+lf2P/QaRq4 -----END OPENSSH PRIVATE KEY----- ", 375 ) ); - // CA KeyID . . : 8112647883641536425 - // Apps . . . . : [440] - const uint64 k_key_tf2 = 12206663272037732248ull; - DbgVerify( CertStore_AddCertFromBase64( "IjEIARIgL+eR2D4trbtwDam1CPW3Ru4JiijY4BAj6V/Y/9BpGrhFmSm1XE2ZkHdgULgDKak/R3xE6pVwMkBCdDdDrAn6IkpuRwksFtXHUTgJNtColLLNPdoEhfyg/Fb5EDnTcOmaNzfoJbv2aFGmjPv2CUzYg+G8qKJv09wN", errMsg ) ); - - // Another intermediate cert signed by hardcoded root - // KeyID . . . .: 15210429824691730624 - // Public key . : ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO+dnkgm1SI2UAMbGkrotrHeTe30Mu4mhne9s7kb+knI - CECSigningPrivateKey privkey_dota_revoked; - DbgVerify( privkey_dota_revoked.ParsePEM( "-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQy NTUxOQAAACDvnZ5IJtUiNlADGxpK6Lax3k3t9DLuJoZ3vbO5G/pJyAAAAH8SNFZ4EjRWeAAA AAtzc2gtZWQyNTUxOQAAACDvnZ5IJtUiNlADGxpK6Lax3k3t9DLuJoZ3vbO5G/pJyAAAAEB8 CNRDPePmSmm66c7QyeOSiQyMHtrcouvxqzNq6GnRz++dnkgm1SI2UAMbGkrotrHeTe30Mu4m hne9s7kb+knI -----END OPENSSH PRIVATE KEY----- ", 375 ) ); - // CA KeyID . . : 9417917822780561193 - // Apps . . . . : [570] - const uint64 k_key_dota_revoked = 15210429824691730624ull; - DbgVerify( CertStore_AddCertFromBase64( "IjEIARIg752eSCbVIjZQAxsaSui2sd5N7fQy7iaGd72zuRv6SchFmSm1XE2ZkHdgULoEKSnXp45cKrOCMkCzt988yidn25C8fBC47EyW35w6SA9GbhPx6CUVeI5h8c/GGHrE4d/Mwvm5t3gv37xUg/uSquFhqWuERmUO4xAP", errMsg ) ); + // Build a test PKI at runtime and install it in the cert store. + // + // root_app . . . . : self-signed, explicitly installed by the "app" + // root_network . . : self-signed, added WITHOUT app trust. Must not become + // trusted, because a hardcoded root CA key is in use. + // root_absent . . . : self-signed, never installed + // ca_csgo . . . . . : intermediate for app 730, signed by root_app + // ca_csgo_eatmwh . : signed by ca_csgo, restricted to the eat/mwh datacenters + // ca_tf2 . . . . . : intermediate for app 440, signed by root_network + // ca_dota_revoked . : intermediate for app 570, signed by root_app, then revoked + // + const SteamNetworkingPOPID iad = CalculateSteamNetworkingPOPIDFromString( "iad" ); + const SteamNetworkingPOPID sto = CalculateSteamNetworkingPOPIDFromString( "sto" ); + const SteamNetworkingPOPID mwh = CalculateSteamNetworkingPOPIDFromString( "mwh" ); + const SteamNetworkingPOPID eat = CalculateSteamNetworkingPOPIDFromString( "eat" ); + char fields[ 256 ]; + + TestCA root_app; MakeCA( root_app, "", nullptr ); + TestCA root_network; MakeCA( root_network, "", nullptr ); + TestCA root_absent; MakeCA( root_absent, "", nullptr ); + TestCA ca_csgo; MakeCA( ca_csgo, "app_ids: 730", &root_app ); + V_sprintf_safe( fields, "gameserver_datacenter_ids: %u gameserver_datacenter_ids: %u", eat, mwh ); + TestCA ca_csgo_eatmwh; MakeCA( ca_csgo_eatmwh, fields, &ca_csgo ); + TestCA ca_tf2; MakeCA( ca_tf2, "app_ids: 440", &root_network ); + TestCA ca_dota_revoked; MakeCA( ca_dota_revoked, "app_ids: 570", &root_app ); + + // The app installs its root of trust through the app-trusted entry point. + // Wrap this one in a PEM-style block, to exercise both accepted input forms. + std::string sRootAppPEM = "-----BEGIN STEAMDATAGRAM CERT-----\n" + root_app.m_sCertBase64 + "\n-----END STEAMDATAGRAM CERT-----\n"; + CHECK( CertStore_AddTrustedCertFromPEM( sRootAppPEM.c_str(), errMsg ) ); + + // Intermediates can arrive through the ordinary path; their trust + // flows from the chain, not from who installed them. + CHECK( CertStore_AddCertFromBase64( ca_csgo.m_sCertBase64.c_str(), errMsg ) ); + CHECK( CertStore_AddCertFromBase64( ca_csgo_eatmwh.m_sCertBase64.c_str(), errMsg ) ); + CHECK( CertStore_AddCertFromBase64( ca_dota_revoked.m_sCertBase64.c_str(), errMsg ) ); + + // A self-signed root added without app trust parses fine... + CHECK( CertStore_AddCertFromBase64( root_network.m_sCertBase64.c_str(), errMsg ) ); + CHECK( CertStore_AddCertFromBase64( ca_tf2.m_sCertBase64.c_str(), errMsg ) ); // Revoke a key - CertStore_AddKeyRevocation( k_key_dota_revoked ); + CertStore_AddKeyRevocation( ca_dota_revoked.m_nKeyID ); CMsgSteamDatagramCertificateSigned msgCertSigned; CMsgSteamDatagramCertificate msgCert; const CertAuthScope *pCertScope; - const SteamNetworkingPOPID iad = CalculateSteamNetworkingPOPIDFromString( "iad" ); - const SteamNetworkingPOPID sto = CalculateSteamNetworkingPOPIDFromString( "sto" ); // 7566447 - const SteamNetworkingPOPID mwh = CalculateSteamNetworkingPOPIDFromString( "mwh" ); - const SteamNetworkingPOPID eat = CalculateSteamNetworkingPOPIDFromString( "eat" ); + + // + // The app-installed self-signed root is trusted, and coexists with + // the hardcoded root CA key. The one installed without app trust + // is not. + // + CHECK( CertStore_CheckPublicKey( root_app.m_nKeyID, k_timeNow, errMsg ) != nullptr ); + CHECK( CertStore_CheckPublicKey( root_network.m_nKeyID, k_timeNow, errMsg ) == nullptr ); // // Basic check for an identity cert issued by an intermediary. // - GenerateCert( msgCertSigned, "app_ids: 730 identity: { generic_string: \"Hercule Poirot\" }", privkey_csgo, k_key_csgo ); - pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeTestNow, errMsg ); - Assert( pCertScope ); - DbgVerify( CheckCertAppID( msgCert, pCertScope, 730, errMsg ) ); + GenerateCert( msgCertSigned, "app_ids: 730 identity_string: \"str:Hercule Poirot\"", ca_csgo ); + pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeNow, errMsg ); + CHECK( pCertScope ); + CHECK( CheckCertAppID( msgCert, pCertScope, 730, errMsg ) ); // Shouldn't work for wrong app - DbgVerify( !CheckCertAppID( msgCert, pCertScope, 570, errMsg ) ); + CHECK( !CheckCertAppID( msgCert, pCertScope, 570, errMsg ) ); // Should work for any POPID - DbgVerify( CheckCertPOPID( msgCert, pCertScope, iad, errMsg ) ); - DbgVerify( CheckCertPOPID( msgCert, pCertScope, sto, errMsg ) ); + CHECK( CheckCertPOPID( msgCert, pCertScope, iad, errMsg ) ); + CHECK( CheckCertPOPID( msgCert, pCertScope, sto, errMsg ) ); // // Try to use CSGO CA cert to authorize for Dota // - GenerateCert( msgCertSigned, "app_ids: 570 identity: { generic_string: \"Hercule Poirot\" }", privkey_csgo, k_key_csgo ); + GenerateCert( msgCertSigned, "app_ids: 570 identity_string: \"str:Hercule Poirot\"", ca_csgo ); // Signature should check out here. - pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeTestNow, errMsg ); - Assert( pCertScope ); + pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeNow, errMsg ); + CHECK( pCertScope ); // But app check should fail - DbgVerify( !CheckCertAppID( msgCert, pCertScope, 570, errMsg ) ); + CHECK( !CheckCertAppID( msgCert, pCertScope, 570, errMsg ) ); // // Cert for data center, signed directly by global app intermediary, // with the POP restriction in the issued cert // - Assert( iad == 6906212 ); - GenerateCert( msgCertSigned, "app_ids: 730 gameserver_datacenter_ids: 6906212", privkey_csgo, k_key_csgo ); - pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeTestNow, errMsg ); - Assert( pCertScope ); - DbgVerify( CheckCertAppID( msgCert, pCertScope, 730, errMsg ) ); + V_sprintf_safe( fields, "app_ids: 730 gameserver_datacenter_ids: %u", iad ); + GenerateCert( msgCertSigned, fields, ca_csgo ); + pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeNow, errMsg ); + CHECK( pCertScope ); + CHECK( CheckCertAppID( msgCert, pCertScope, 730, errMsg ) ); // Should only work for the authorized POP - DbgVerify( CheckCertPOPID( msgCert, pCertScope, iad, errMsg ) ); - DbgVerify( !CheckCertPOPID( msgCert, pCertScope, sto, errMsg ) ); + CHECK( CheckCertPOPID( msgCert, pCertScope, iad, errMsg ) ); + CHECK( !CheckCertPOPID( msgCert, pCertScope, sto, errMsg ) ); // // Cert for data center, signed by app that is further restricted by POPID // - Assert( iad == 6906212 ); - Assert( mwh == 7173992 ); - GenerateCert( msgCertSigned, "app_ids: 730 gameserver_datacenter_ids: 6906212 gameserver_datacenter_ids: 7173992", privkey_csgo_eatmwh, k_key_csgo_eatmwh ); - pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeTestNow, errMsg ); - Assert( pCertScope ); - DbgVerify( CheckCertAppID( msgCert, pCertScope, 730, errMsg ) ); - DbgVerify( !CheckCertPOPID( msgCert, pCertScope, iad, errMsg ) ); // Not in CA chain - DbgVerify( CheckCertPOPID( msgCert, pCertScope, mwh, errMsg ) ); // In both CA chain and cert - DbgVerify( !CheckCertPOPID( msgCert, pCertScope, eat, errMsg ) ); // In CA chain but not cert + V_sprintf_safe( fields, "app_ids: 730 gameserver_datacenter_ids: %u gameserver_datacenter_ids: %u", iad, mwh ); + GenerateCert( msgCertSigned, fields, ca_csgo_eatmwh ); + pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeNow, errMsg ); + CHECK( pCertScope ); + CHECK( CheckCertAppID( msgCert, pCertScope, 730, errMsg ) ); + CHECK( !CheckCertPOPID( msgCert, pCertScope, iad, errMsg ) ); // Not in CA chain + CHECK( CheckCertPOPID( msgCert, pCertScope, mwh, errMsg ) ); // In both CA chain and cert + CHECK( !CheckCertPOPID( msgCert, pCertScope, eat, errMsg ) ); // In CA chain but not cert // - // Try to use a cert where only cert is from root that isn't hardcoded + // Cert with no POP restriction of its own, signed by a POP-restricted + // chain: the chain restriction must apply // - GenerateCert( msgCertSigned, "app_ids: 440 identity: { generic_string: \"Hercule Poirot\" }", privkey_tf2, k_key_tf2 ); - pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeTestNow, errMsg ); - Assert( !pCertScope ); + GenerateCert( msgCertSigned, "app_ids: 730", ca_csgo_eatmwh ); + pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeNow, errMsg ); + CHECK( pCertScope ); + CHECK( CheckCertPOPID( msgCert, pCertScope, eat, errMsg ) ); // Granted by chain + CHECK( !CheckCertPOPID( msgCert, pCertScope, iad, errMsg ) ); // Not granted by chain // - // Try to use a cert signed by a revoked key + // Try to use a cert that chains to a root that was never installed // - GenerateCert( msgCertSigned, "app_ids: 570 identity: { generic_string: \"Hercule Poirot\" }", privkey_dota_revoked, k_key_dota_revoked ); + GenerateCert( msgCertSigned, "app_ids: 440 identity_string: \"str:Hercule Poirot\"", root_absent ); + pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeNow, errMsg ); + CHECK( !pCertScope ); - // Should fail - pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeTestNow, errMsg ); - Assert( !pCertScope ); + // + // Try to use a cert that chains to the self-signed root that was + // added without app trust + // + GenerateCert( msgCertSigned, "app_ids: 440 identity_string: \"str:Hercule Poirot\"", ca_tf2 ); + pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeNow, errMsg ); + CHECK( !pCertScope ); + + // + // Try to use a cert signed by a revoked key + // + GenerateCert( msgCertSigned, "app_ids: 570 identity_string: \"str:Hercule Poirot\"", ca_dota_revoked ); + pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeNow, errMsg ); + CHECK( !pCertScope ); + // + // Re-adding the same self-signed root through the app-trusted path + // upgrades it, and its chain starts to verify. (This also exercises + // the raw-base64 input form.) + // + CHECK( CertStore_AddTrustedCertFromPEM( root_network.m_sCertBase64.c_str(), errMsg ) ); + CHECK( CertStore_CheckPublicKey( root_network.m_nKeyID, k_timeNow, errMsg ) != nullptr ); + GenerateCert( msgCertSigned, "app_ids: 440 identity_string: \"str:Hercule Poirot\"", ca_tf2 ); + pCertScope = CertStore_CheckCert( msgCertSigned, msgCert, k_timeNow, errMsg ); + CHECK( pCertScope ); + CHECK( CheckCertAppID( msgCert, pCertScope, 440, errMsg ) ); + + if ( g_failed ) + { + fprintf( stderr, "test_pki FAILED\n" ); + return 1; + } + printf( "test_pki passed\n" ); return 0; }