|
| 1 | +/* |
| 2 | + * Copyright (C) 2025 EPAM Systems, Inc. |
| 3 | + * |
| 4 | + * SPDX-License-Identifier: Apache-2.0 |
| 5 | + */ |
| 6 | + |
| 7 | +#include <gmock/gmock.h> |
| 8 | + |
| 9 | +#include <map> |
| 10 | +#include <string> |
| 11 | +#include <vector> |
| 12 | + |
| 13 | +#include <core/common/cloudprotocol/servicediscovery.hpp> |
| 14 | +#include <core/common/crypto/cryptohelper.hpp> |
| 15 | +#include <core/common/tests/crypto/providers/cryptofactory.hpp> |
| 16 | +#include <core/common/tests/crypto/softhsmenv.hpp> |
| 17 | +#include <core/common/tests/utils/log.hpp> |
| 18 | +#include <core/common/tools/utils.hpp> |
| 19 | + |
| 20 | +using namespace testing; |
| 21 | + |
| 22 | +namespace aos::crypto { |
| 23 | + |
| 24 | +/*********************************************************************************************************************** |
| 25 | + * Test stubs |
| 26 | + **********************************************************************************************************************/ |
| 27 | + |
| 28 | +/** |
| 29 | + * Stub implementation of CertProviderItf. |
| 30 | + */ |
| 31 | +class CertProviderStub : public iam::certhandler::CertProviderItf { |
| 32 | +public: |
| 33 | + Error GetCert(const String& certType, const Array<uint8_t>& issuer, const Array<uint8_t>& serial, |
| 34 | + iam::certhandler::CertInfo& resCert) const override |
| 35 | + { |
| 36 | + (void)issuer; |
| 37 | + (void)serial; |
| 38 | + |
| 39 | + if (mCerts.count(certType.CStr()) == 0) { |
| 40 | + return ErrorEnum::eNotFound; |
| 41 | + } |
| 42 | + |
| 43 | + resCert = mCerts.find(certType.CStr())->second; |
| 44 | + |
| 45 | + return ErrorEnum::eNone; |
| 46 | + } |
| 47 | + |
| 48 | + Error SubscribeCertChanged(const String& certType, iam::certhandler::CertReceiverItf& certReceiver) override |
| 49 | + { |
| 50 | + (void)certType; |
| 51 | + (void)certReceiver; |
| 52 | + |
| 53 | + return ErrorEnum::eNone; |
| 54 | + } |
| 55 | + |
| 56 | + Error UnsubscribeCertChanged(iam::certhandler::CertReceiverItf& certReceiver) override |
| 57 | + { |
| 58 | + (void)certReceiver; |
| 59 | + |
| 60 | + return ErrorEnum::eNone; |
| 61 | + } |
| 62 | + |
| 63 | + void AddCert(const std::string& certType, const std::string& certName) |
| 64 | + { |
| 65 | + iam::certhandler::CertInfo certInfo; |
| 66 | + |
| 67 | + certInfo.mCertURL = ("file://" + FullCertPath(certName)).c_str(); |
| 68 | + certInfo.mKeyURL = ("file://" + FullKeyPath(certName)).c_str(); |
| 69 | + |
| 70 | + mCerts[certType] = certInfo; |
| 71 | + } |
| 72 | + |
| 73 | +private: |
| 74 | + static std::string FullCertPath(const std::string& name) |
| 75 | + { |
| 76 | + return std::string(CRYPTOHELPER_CERTS_DIR) + "/" + name + ".pem"; |
| 77 | + } |
| 78 | + |
| 79 | + static std::string FullKeyPath(const std::string& name) |
| 80 | + { |
| 81 | + return std::string(CRYPTOHELPER_CERTS_DIR) + "/" + name + ".key"; |
| 82 | + } |
| 83 | + |
| 84 | + std::map<std::string, iam::certhandler::CertInfo> mCerts; |
| 85 | +}; |
| 86 | + |
| 87 | +void AssertOK(const Error& err) |
| 88 | +{ |
| 89 | + if (!err.IsNone()) { |
| 90 | + throw err; |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +std::vector<uint8_t> ReadFileFromAESDir(const String& fileName) |
| 95 | +{ |
| 96 | + StaticArray<uint8_t, 2048> content; |
| 97 | + |
| 98 | + auto fullPath = CRYPTOHELPER_AES_DIR "/" + std::string(fileName.CStr()); |
| 99 | + |
| 100 | + AssertOK(fs::ReadFile(fullPath.c_str(), content)); |
| 101 | + |
| 102 | + return {content.begin(), content.end()}; |
| 103 | +} |
| 104 | + |
| 105 | +std::vector<uint8_t> ReadFileFromCrtDir(const String& fileName) |
| 106 | +{ |
| 107 | + StaticArray<uint8_t, 2048> content; |
| 108 | + |
| 109 | + auto fullPath = CRYPTOHELPER_CERTS_DIR "/" + std::string(fileName.CStr()); |
| 110 | + |
| 111 | + AssertOK(fs::ReadFile(fullPath.c_str(), content)); |
| 112 | + |
| 113 | + return {content.begin(), content.end()}; |
| 114 | +} |
| 115 | + |
| 116 | +cloudprotocol::DecryptInfo CreateDecryptionInfo( |
| 117 | + const char* blockAlg, const std::vector<uint8_t>& blockIV, const std::vector<uint8_t>& blockKey) |
| 118 | +{ |
| 119 | + cloudprotocol::DecryptInfo decryptInfo; |
| 120 | + |
| 121 | + decryptInfo.mBlockAlg = blockAlg; |
| 122 | + decryptInfo.mBlockIV = Array<uint8_t>(blockIV.data(), blockIV.size()); |
| 123 | + decryptInfo.mBlockIV.Resize(16); |
| 124 | + |
| 125 | + decryptInfo.mBlockKey = Array<uint8_t>(blockKey.data(), blockKey.size()); |
| 126 | + |
| 127 | + return decryptInfo; |
| 128 | +} |
| 129 | + |
| 130 | +cloudprotocol::CertificateInfo CreateCert(CryptoProviderItf& provider, const char* name) |
| 131 | +{ |
| 132 | + auto fullPath = CRYPTOHELPER_CERTS_DIR "/" + std::string(name) + ".pem"; |
| 133 | + |
| 134 | + StaticString<cCertPEMLen> pem; |
| 135 | + x509::CertificateChain chain; |
| 136 | + |
| 137 | + AssertOK(fs::ReadFileToString(fullPath.c_str(), pem)); |
| 138 | + AssertOK(provider.PEMToX509Certs(pem, chain)); |
| 139 | + |
| 140 | + cloudprotocol::CertificateInfo cert; |
| 141 | + |
| 142 | + cert.mFingerprint = name; |
| 143 | + cert.mCertificate = chain[0].mRaw; |
| 144 | + |
| 145 | + return cert; |
| 146 | +} |
| 147 | + |
| 148 | +cloudprotocol::CertificateChainInfo CreateCertChain(const char* name, const std::vector<std::string> fingerprints) |
| 149 | +{ |
| 150 | + cloudprotocol::CertificateChainInfo chain; |
| 151 | + |
| 152 | + chain.mName = name; |
| 153 | + |
| 154 | + for (const auto& item : fingerprints) { |
| 155 | + AssertOK(chain.mFingerprints.PushBack(item.c_str())); |
| 156 | + } |
| 157 | + |
| 158 | + return chain; |
| 159 | +} |
| 160 | + |
| 161 | +cloudprotocol::SignInfo CreateSigns(const char* chainName, const char* algName) |
| 162 | +{ |
| 163 | + cloudprotocol::SignInfo signs; |
| 164 | + |
| 165 | + signs.mChainName = chainName; |
| 166 | + signs.mAlg = algName; |
| 167 | + |
| 168 | + auto testFilePath = std::string(CRYPTOHELPER_CERTS_DIR "/hello-world.txt.") + chainName + ".sig"; |
| 169 | + AssertOK(fs::ReadFile(testFilePath.c_str(), signs.mValue)); |
| 170 | + signs.mTrustedTimestamp = Time::Now(); |
| 171 | + |
| 172 | + return signs; |
| 173 | +} |
| 174 | + |
| 175 | +/*********************************************************************************************************************** |
| 176 | + * Suite |
| 177 | + **********************************************************************************************************************/ |
| 178 | + |
| 179 | +class CryptoHelperTest : public Test { |
| 180 | +public: |
| 181 | + void SetUp() override |
| 182 | + { |
| 183 | + tests::utils::InitLog(); |
| 184 | + |
| 185 | + ASSERT_TRUE(mCryptoFactory.Init().IsNone()); |
| 186 | + |
| 187 | + mCryptoProvider = &mCryptoFactory.GetCryptoProvider(); |
| 188 | + |
| 189 | + ASSERT_TRUE(mSoftHSMEnv.Init(cPIN, cLabel).IsNone()); |
| 190 | + ASSERT_TRUE(mCertLoader.Init(*mCryptoProvider, mSoftHSMEnv.GetManager()).IsNone()); |
| 191 | + |
| 192 | + ASSERT_TRUE( |
| 193 | + mCryptoHelper.Init(mCertProvider, *mCryptoProvider, mCertLoader, cDefaultServiceDiscoveryURL, cCACert) |
| 194 | + .IsNone()); |
| 195 | + } |
| 196 | + |
| 197 | +protected: |
| 198 | + static constexpr auto cDefaultServiceDiscoveryURL = "http://service-discovery-url.html"; |
| 199 | + static constexpr auto cCACert = CRYPTOHELPER_CERTS_DIR "/rootCA.pem"; |
| 200 | + |
| 201 | + static constexpr auto cLabel = "iam pkcs11 test slot"; |
| 202 | + static constexpr auto cPIN = "admin"; |
| 203 | + |
| 204 | + DefaultCryptoFactory mCryptoFactory; |
| 205 | + |
| 206 | + CryptoProviderItf* mCryptoProvider; |
| 207 | + |
| 208 | + CertProviderStub mCertProvider; |
| 209 | + CertLoader mCertLoader; |
| 210 | + CryptoHelper mCryptoHelper; |
| 211 | + test::SoftHSMEnv mSoftHSMEnv; |
| 212 | +}; |
| 213 | + |
| 214 | +TEST_F(CryptoHelperTest, ServiceDiscoveryURLs) |
| 215 | +{ |
| 216 | + struct TestData { |
| 217 | + std::string mCert; |
| 218 | + std::string mServiceDiscoveryURL; |
| 219 | + }; |
| 220 | + |
| 221 | + TestData testData[] = {{"online", "https://www.mytest.com"}, {"onlineTest1", "https://Test1:9000"}, |
| 222 | + {"onlineTest2", cDefaultServiceDiscoveryURL}}; |
| 223 | + |
| 224 | + for (const auto& [certName, url] : testData) { |
| 225 | + mCertProvider.AddCert("online", certName); |
| 226 | + |
| 227 | + StaticArray<StaticString<cURLLen>, cloudprotocol::cURLsCount> discoveryURLs; |
| 228 | + |
| 229 | + ASSERT_TRUE(mCryptoHelper.GetServiceDiscoveryURLs(discoveryURLs).IsNone()); |
| 230 | + ASSERT_EQ(discoveryURLs.Size(), 1); |
| 231 | + EXPECT_EQ(url, discoveryURLs[0].CStr()); |
| 232 | + } |
| 233 | +} |
| 234 | + |
| 235 | +TEST_F(CryptoHelperTest, Decrypt) |
| 236 | +{ |
| 237 | + struct TestData { |
| 238 | + const char* mEncryptedFile; |
| 239 | + cloudprotocol::DecryptInfo mDecryptInfo; |
| 240 | + std::vector<uint8_t> mDecryptedContent; |
| 241 | + }; |
| 242 | + |
| 243 | + std::vector<TestData> testData = { |
| 244 | + |
| 245 | + {CRYPTOHELPER_AES_DIR "/hello-world.txt.enc", |
| 246 | + CreateDecryptionInfo("AES256/CBC/PKCS7PADDING", {1, 2, 3, 4, 5}, ReadFileFromAESDir("aes.key")), |
| 247 | + ReadFileFromAESDir("hello-world.txt")}, |
| 248 | + }; |
| 249 | + |
| 250 | + mCertProvider.AddCert("offline", "offline1"); |
| 251 | + |
| 252 | + for (const auto& test : testData) { |
| 253 | + LOG_INF() << "Decode encrypted file: " << test.mEncryptedFile; |
| 254 | + |
| 255 | + constexpr auto cDecryptedFile = CRYPTOHELPER_AES_DIR "/decrypted.raw"; |
| 256 | + |
| 257 | + ASSERT_TRUE(mCryptoHelper.Decrypt(test.mEncryptedFile, cDecryptedFile, test.mDecryptInfo).IsNone()); |
| 258 | + EXPECT_EQ(test.mDecryptedContent, ReadFileFromAESDir("decrypted.raw")); |
| 259 | + } |
| 260 | +} |
| 261 | + |
| 262 | +TEST_F(CryptoHelperTest, ValidateSigns) |
| 263 | +{ |
| 264 | + struct TestData { |
| 265 | + std::vector<cloudprotocol::CertificateInfo> mCerts; |
| 266 | + cloudprotocol::CertificateChainInfo mChain; |
| 267 | + cloudprotocol::SignInfo mSigns; |
| 268 | + }; |
| 269 | + |
| 270 | + constexpr auto cDecryptedFile = CRYPTOHELPER_CERTS_DIR "/hello-world.txt"; |
| 271 | + |
| 272 | + auto rootCA = CreateCert(*mCryptoProvider, "rootCA"); |
| 273 | + auto secondaryCA = CreateCert(*mCryptoProvider, "secondaryCA"); |
| 274 | + auto intermediateCA = CreateCert(*mCryptoProvider, "intermediateCA"); |
| 275 | + |
| 276 | + auto online = CreateCert(*mCryptoProvider, "online"); |
| 277 | + auto offline1 = CreateCert(*mCryptoProvider, "offline1"); |
| 278 | + auto offline2 = CreateCert(*mCryptoProvider, "offline2"); |
| 279 | + auto onlineTest1 = CreateCert(*mCryptoProvider, "onlineTest1"); |
| 280 | + auto onlineTest2 = CreateCert(*mCryptoProvider, "onlineTest2"); |
| 281 | + |
| 282 | + std::vector<TestData> testData = { |
| 283 | + {{online, intermediateCA, secondaryCA}, CreateCertChain("online", {"online", "intermediateCA", "secondaryCA"}), |
| 284 | + CreateSigns("online", "RSA/SHA256/PKCS1v1_5")}, |
| 285 | + |
| 286 | + {{offline1, intermediateCA, secondaryCA}, |
| 287 | + CreateCertChain("offline1", {"offline1", "intermediateCA", "secondaryCA"}), |
| 288 | + CreateSigns("offline1", "RSA/SHA256/PKCS1v1_5")}, |
| 289 | + {{offline2, intermediateCA, secondaryCA}, |
| 290 | + CreateCertChain("offline2", {"offline2", "intermediateCA", "secondaryCA"}), |
| 291 | + CreateSigns("offline2", "RSA/SHA256/PKCS1v1_5")}, |
| 292 | + {{onlineTest1, intermediateCA, secondaryCA}, |
| 293 | + CreateCertChain("onlineTest1", {"onlineTest1", "intermediateCA", "secondaryCA"}), |
| 294 | + CreateSigns("onlineTest1", "RSA/SHA256/PKCS1v1_5")}, |
| 295 | + {{onlineTest2, intermediateCA, secondaryCA}, |
| 296 | + CreateCertChain("onlineTest2", {"onlineTest2", "intermediateCA", "secondaryCA"}), |
| 297 | + CreateSigns("onlineTest2", "RSA/SHA256/PKCS1v1_5")}}; |
| 298 | + |
| 299 | + for (const auto& item : testData) { |
| 300 | + StaticArray<cloudprotocol::CertificateInfo, 10> certs; |
| 301 | + certs = Array<cloudprotocol::CertificateInfo>(item.mCerts.data(), item.mCerts.size()); |
| 302 | + |
| 303 | + StaticArray<cloudprotocol::CertificateChainInfo, 1> chains; |
| 304 | + chains.PushBack(item.mChain); |
| 305 | + |
| 306 | + ASSERT_TRUE(mCryptoHelper.ValidateSigns(cDecryptedFile, item.mSigns, chains, certs).IsNone()); |
| 307 | + } |
| 308 | +} |
| 309 | + |
| 310 | +TEST_F(CryptoHelperTest, DecryptMetadata) |
| 311 | +{ |
| 312 | + StaticArray<uint8_t, cCloudMetadataSize> output; |
| 313 | + |
| 314 | + auto inputData = ReadFileFromCrtDir("hello-world-cms.txt.offline1.cms"); |
| 315 | + auto input = Array<uint8_t>(inputData.data(), inputData.size()); |
| 316 | + |
| 317 | + mCertProvider.AddCert("offline", "offline1"); |
| 318 | + |
| 319 | + ASSERT_TRUE(mCryptoHelper.DecryptMetadata(input, output).IsNone()); |
| 320 | + |
| 321 | + auto expected = ReadFileFromCrtDir("hello-world-cms.txt"); |
| 322 | + EXPECT_EQ(std::vector<uint8_t>(output.begin(), output.end()), expected); |
| 323 | +} |
| 324 | + |
| 325 | +} // namespace aos::crypto |
0 commit comments