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; }