1 #include "config.h"
2 
3 #include "image_verify.hpp"
4 #include "utils.hpp"
5 #include "version.hpp"
6 
7 #include <openssl/evp.h>
8 #include <stdlib.h>
9 
10 #include <filesystem>
11 #include <fstream>
12 #include <iostream>
13 #include <sstream>
14 #include <string>
15 #include <vector>
16 
17 #include <gtest/gtest.h>
18 
19 using namespace phosphor::software::manager;
20 using namespace phosphor::software::image;
21 
22 class VersionTest : public testing::Test
23 {
24   protected:
25     virtual void SetUp()
26     {
27         char versionDir[] = "./versionXXXXXX";
28         _directory = mkdtemp(versionDir);
29 
30         if (_directory.empty())
31         {
32             throw std::bad_alloc();
33         }
34     }
35 
36     virtual void TearDown()
37     {
38         fs::remove_all(_directory);
39     }
40 
41     std::string _directory;
42 };
43 
44 /** @brief Make sure we correctly get the version and purpose from getValue()*/
45 TEST_F(VersionTest, TestGetValue)
46 {
47     auto manifestFilePath = _directory + "/" + "MANIFEST";
48     auto version = "test-version";
49     auto purpose = "BMC";
50 
51     std::ofstream file;
52     file.open(manifestFilePath, std::ofstream::out);
53     ASSERT_TRUE(file.is_open());
54 
55     file << "version=" << version << "\n";
56     file << "purpose=" << purpose << "\n";
57     file.close();
58 
59     EXPECT_EQ(Version::getValue(manifestFilePath, "version"), version);
60     EXPECT_EQ(Version::getValue(manifestFilePath, "purpose"), purpose);
61 }
62 
63 TEST_F(VersionTest, TestGetRepeatedValue)
64 {
65     auto manifestFilePath = _directory + "/" + "MANIFEST";
66     const std::vector<std::string> names = {"foo.bar", "baz.bim"};
67 
68     std::ofstream file;
69     file.open(manifestFilePath, std::ofstream::out);
70     ASSERT_TRUE(file.is_open());
71 
72     for (const auto& name : names)
73     {
74         file << "CompatibleName=" << name << "\n";
75     }
76     file.close();
77 
78     EXPECT_EQ(Version::getRepeatedValues(manifestFilePath, "CompatibleName"),
79               names);
80 }
81 
82 TEST_F(VersionTest, TestGetValueWithCRLF)
83 {
84     auto manifestFilePath = _directory + "/" + "MANIFEST";
85     auto version = "test-version";
86     auto purpose = "BMC";
87 
88     std::ofstream file;
89     file.open(manifestFilePath, std::ofstream::out);
90     ASSERT_TRUE(file.is_open());
91 
92     file << "version=" << version << "\r\n";
93     file << "purpose=" << purpose << "\r\n";
94     file.close();
95 
96     EXPECT_EQ(Version::getValue(manifestFilePath, "version"), version);
97     EXPECT_EQ(Version::getValue(manifestFilePath, "purpose"), purpose);
98 }
99 
100 TEST_F(VersionTest, TestGetVersionWithQuotes)
101 {
102     auto releasePath = _directory + "/" + "os-release";
103     auto version = "1.2.3-test-version";
104 
105     std::ofstream file;
106     file.open(releasePath, std::ofstream::out);
107     ASSERT_TRUE(file.is_open());
108 
109     file << "VERSION_ID=\"" << version << "\"\n";
110     file.close();
111 
112     EXPECT_EQ(Version::getBMCVersion(releasePath), version);
113 }
114 
115 TEST_F(VersionTest, TestGetVersionWithoutQuotes)
116 {
117     auto releasePath = _directory + "/" + "os-release";
118     auto version = "9.88.1-test-version";
119 
120     std::ofstream file;
121     file.open(releasePath, std::ofstream::out);
122     ASSERT_TRUE(file.is_open());
123 
124     file << "VERSION_ID=" << version << "\n";
125     file.close();
126 
127     EXPECT_EQ(Version::getBMCVersion(releasePath), version);
128 }
129 
130 /** @brief Make sure we correctly get the Id from getId()*/
131 TEST_F(VersionTest, TestGetId)
132 {
133     auto version = "test-id";
134     unsigned char digest[EVP_MAX_MD_SIZE];
135     unsigned int digest_count = 0;
136 
137     EVP_MD_CTX_Ptr ctx(EVP_MD_CTX_new(), &::EVP_MD_CTX_free);
138 
139     EVP_DigestInit(ctx.get(), EVP_sha512());
140     EVP_DigestUpdate(ctx.get(), version, strlen(version));
141     EVP_DigestFinal(ctx.get(), digest, &digest_count);
142 
143     char mdString[EVP_MAX_MD_SIZE * 2 + 1];
144     for (decltype(digest_count) i = 0; i < digest_count; i++)
145     {
146         snprintf(&mdString[static_cast<size_t>(i) * 2], 3, "%02x",
147                  (unsigned int)digest[i]);
148     }
149     std::string hexId = std::string(mdString);
150     hexId = hexId.substr(0, 8);
151     EXPECT_EQ(Version::getId(version), hexId);
152 }
153 
154 TEST_F(VersionTest, TestGetExtendedVersion)
155 {
156     auto releasePath = _directory + "/" + "os-release";
157     auto ExtendedVersion = "9.88.1-test-ExtendedVersion";
158 
159     std::ofstream file;
160     file.open(releasePath, std::ofstream::out);
161     ASSERT_TRUE(file.is_open());
162 
163     file << "EXTENDED_VERSION=" << ExtendedVersion << "\n";
164     file.close();
165 
166     EXPECT_EQ(Version::getBMCExtendedVersion(releasePath), ExtendedVersion);
167 }
168 
169 class SignatureTest : public testing::Test
170 {
171   public:
172     static constexpr auto opensslCmd = "openssl dgst -sha256 -sign ";
173     static constexpr auto testPath = "/tmp/_testSig";
174 
175   protected:
176     void command(const std::string& cmd)
177     {
178         auto val = std::system(cmd.c_str());
179         if (val)
180         {
181             std::cout << "COMMAND Error: " << val << std::endl;
182         }
183     }
184     virtual void SetUp()
185     {
186         // Create test base directory.
187         fs::create_directories(testPath);
188 
189         // Create unique temporary path for images
190         std::string tmpDir(testPath);
191         tmpDir += "/extractXXXXXX";
192         std::string imageDir = mkdtemp(const_cast<char*>(tmpDir.c_str()));
193 
194         // Create unique temporary configuration path
195         std::string tmpConfDir(testPath);
196         tmpConfDir += "/confXXXXXX";
197         std::string confDir = mkdtemp(const_cast<char*>(tmpConfDir.c_str()));
198 
199         extractPath = imageDir;
200         extractPath /= "images";
201 
202         signedConfPath = confDir;
203         signedConfPath /= "conf";
204 
205         signedConfOpenBMCPath = confDir;
206         signedConfOpenBMCPath /= "conf";
207         signedConfOpenBMCPath /= "OpenBMC";
208 
209         std::cout << "SETUP " << std::endl;
210 
211         command("mkdir " + extractPath.string());
212         command("mkdir " + signedConfPath.string());
213         command("mkdir " + signedConfOpenBMCPath.string());
214 
215         std::string hashFile = signedConfOpenBMCPath.string() + "/hashfunc";
216         command("echo \"HashType=RSA-SHA256\" > " + hashFile);
217 
218         std::string manifestFile = extractPath.string() + "/" + "MANIFEST";
219         command(
220             "echo \"purpose=xyz.openbmc_project.Software.Version.VersionPurpose.BMC\" > " +
221             manifestFile);
222         command("echo \"HashType=RSA-SHA256\" >> " + manifestFile);
223         command("echo \"KeyType=OpenBMC\" >> " + manifestFile);
224 
225         std::string kernelFile = extractPath.string() + "/" + "image-kernel";
226         command("echo \"image-kernel file \" > " + kernelFile);
227 
228         std::string rofsFile = extractPath.string() + "/" + "image-rofs";
229         command("echo \"image-rofs file \" > " + rofsFile);
230 
231         std::string rwfsFile = extractPath.string() + "/" + "image-rwfs";
232         command("echo \"image-rwfs file \" > " + rwfsFile);
233 
234         std::string ubootFile = extractPath.string() + "/" + "image-u-boot";
235         command("echo \"image-u-boot file \" > " + ubootFile);
236 
237         std::string pkeyFile = extractPath.string() + "/" + "private.pem";
238         command("openssl genrsa  -out " + pkeyFile + " 2048");
239 
240         std::string pubkeyFile = extractPath.string() + "/" + "publickey";
241         command("openssl rsa -in " + pkeyFile + " -outform PEM " +
242                 "-pubout -out " + pubkeyFile);
243 
244         command("cp " + pubkeyFile + " " + signedConfOpenBMCPath.string());
245         command(opensslCmd + pkeyFile + " -out " + kernelFile + ".sig " +
246                 kernelFile);
247 
248         command(opensslCmd + pkeyFile + " -out " + manifestFile + ".sig " +
249                 manifestFile);
250         command(opensslCmd + pkeyFile + " -out " + rofsFile + ".sig " +
251                 rofsFile);
252         command(opensslCmd + pkeyFile + " -out " + rwfsFile + ".sig " +
253                 rwfsFile);
254         command(opensslCmd + pkeyFile + " -out " + ubootFile + ".sig " +
255                 ubootFile);
256         command(opensslCmd + pkeyFile + " -out " + pubkeyFile + ".sig " +
257                 pubkeyFile);
258 
259 #ifdef WANT_SIGNATURE_VERIFY
260         std::string fullFile = extractPath.string() + "/" + "image-full";
261         command("cat " + kernelFile + ".sig " + rofsFile + ".sig " + rwfsFile +
262                 ".sig " + ubootFile + ".sig " + manifestFile + ".sig " +
263                 pubkeyFile + ".sig > " + fullFile);
264         command(opensslCmd + pkeyFile + " -out " + fullFile + ".sig " +
265                 fullFile);
266 #endif
267 
268         signature = std::make_unique<Signature>(extractPath, signedConfPath);
269     }
270     virtual void TearDown()
271     {
272         command("rm -rf " + std::string(testPath));
273     }
274 
275     std::unique_ptr<Signature> signature;
276     fs::path extractPath;
277     fs::path signedConfPath;
278     fs::path signedConfOpenBMCPath;
279 };
280 
281 /** @brief Test for success scenario*/
282 TEST_F(SignatureTest, TestSignatureVerify)
283 {
284     EXPECT_TRUE(signature->verify());
285 }
286 
287 /** @brief Test failure scenario with corrupted signature file*/
288 TEST_F(SignatureTest, TestCorruptSignatureFile)
289 {
290     // corrupt the image-kernel.sig file and ensure that verification fails
291     std::string kernelFile = extractPath.string() + "/" + "image-kernel";
292     command("echo \"dummy data\" > " + kernelFile + ".sig ");
293     EXPECT_FALSE(signature->verify());
294 }
295 
296 /** @brief Test failure scenario with no public key in the image*/
297 TEST_F(SignatureTest, TestNoPublicKeyInImage)
298 {
299     // Remove publickey file from the image and ensure that verify fails
300     std::string pubkeyFile = extractPath.string() + "/" + "publickey";
301     command("rm " + pubkeyFile);
302     EXPECT_FALSE(signature->verify());
303 }
304 
305 /** @brief Test failure scenario with invalid hash function value*/
306 TEST_F(SignatureTest, TestInvalidHashValue)
307 {
308     // Change the hashfunc value and ensure that verification fails
309     std::string hashFile = signedConfOpenBMCPath.string() + "/hashfunc";
310     command("echo \"HashType=md5\" > " + hashFile);
311     EXPECT_FALSE(signature->verify());
312 }
313 
314 /** @brief Test for failure scenario with no config file in system*/
315 TEST_F(SignatureTest, TestNoConfigFileInSystem)
316 {
317     // Remove the conf folder in the system and ensure that verify fails
318     command("rm -rf " + signedConfOpenBMCPath.string());
319     EXPECT_FALSE(signature->verify());
320 }
321 
322 #ifdef WANT_SIGNATURE_VERIFY
323 /** @brief Test for failure scenario without full verification */
324 TEST_F(SignatureTest, TestNoFullSignature)
325 {
326     // Remove the full signature and ensure that verify fails
327     std::string fullFile = extractPath.string() + "/" + "image-full.sig";
328     command("rm " + fullFile);
329     EXPECT_FALSE(signature->verify());
330 }
331 
332 /** @brief Test for failure scenario without full verification */
333 TEST_F(SignatureTest, TestNoFullSignatureForBIOS)
334 {
335     // Remove the full signature
336     std::string fullFile = extractPath.string() + "/" + "image-full.sig";
337     command("rm " + fullFile);
338 
339     // Change the purpose to BIOS
340     std::string manifestFile = extractPath.string() + "/" + "MANIFEST";
341     std::string pkeyFile = extractPath.string() + "/" + "private.pem";
342     command("sed -i s/VersionPurpose.BMC/VersionPurpose.BIOS/ " + manifestFile);
343     command(opensslCmd + pkeyFile + " -out " + manifestFile + ".sig " +
344             manifestFile);
345 
346     // Re-create signature object and make sure verify succeed.
347     signature = std::make_unique<Signature>(extractPath, signedConfPath);
348     EXPECT_TRUE(signature->verify());
349 }
350 #endif
351 
352 class FileTest : public testing::Test
353 {
354   protected:
355     std::string readFile(fs::path path)
356     {
357         std::ifstream f(path, std::ios::in);
358         const auto sz = fs::file_size(path);
359         std::string result(sz, '\0');
360         f.read(result.data(), sz);
361 
362         return result;
363     }
364 
365     void command(const std::string& cmd)
366     {
367         auto val = std::system(cmd.c_str());
368         if (val)
369         {
370             std::cout << "COMMAND Error: " << val << std::endl;
371         }
372     }
373 
374     virtual void SetUp()
375     {
376         // Create test base directory.
377         tmpDir = fs::temp_directory_path() / "testFileXXXXXX";
378         if (!mkdtemp(tmpDir.data()))
379         {
380             throw "Failed to create tmp dir";
381         }
382 
383         std::string file1 = tmpDir + "/file1";
384         std::string file2 = tmpDir + "/file2";
385         command("echo \"File Test1\n\n\" > " + file1);
386         command("echo \"FileTe st2\n\nte st2\" > " + file2);
387 
388         srcFiles.push_back(file1);
389         srcFiles.push_back(file2);
390     }
391 
392     virtual void TearDown()
393     {
394         fs::remove_all(tmpDir);
395     }
396 
397     std::vector<std::string> srcFiles;
398     std::string tmpDir;
399 };
400 
401 TEST_F(FileTest, TestMergeFiles)
402 {
403     std::string retFile = tmpDir + "/retFile";
404     for (auto file : srcFiles)
405     {
406         command("cat " + file + " >> " + retFile);
407     }
408 
409     std::string dstFile = tmpDir + "/dstFile";
410     utils::mergeFiles(srcFiles, dstFile);
411 
412     ASSERT_NE(fs::file_size(retFile), static_cast<uintmax_t>(-1));
413     ASSERT_NE(fs::file_size(dstFile), static_cast<uintmax_t>(-1));
414     ASSERT_EQ(fs::file_size(retFile), fs::file_size(dstFile));
415 
416     std::string ssRetFile = readFile(fs::path(retFile));
417     std::string ssDstFile = readFile(fs::path(dstFile));
418     ASSERT_EQ(ssRetFile, ssDstFile);
419 }
420 
421 TEST(ExecTest, TestConstructArgv)
422 {
423     auto name = "/bin/ls";
424     auto arg1 = "-a";
425     auto arg2 = "-l";
426     auto arg3 = "-t";
427     auto arg4 = "-rS";
428     auto argV = utils::internal::constructArgv(name, arg1, arg2, arg3, arg4);
429     char** charArray = argV.data();
430     EXPECT_EQ(argV.size(), 6);
431     EXPECT_EQ(charArray[0], name);
432     EXPECT_EQ(charArray[1], arg1);
433     EXPECT_EQ(charArray[2], arg2);
434     EXPECT_EQ(charArray[3], arg3);
435     EXPECT_EQ(charArray[4], arg4);
436     EXPECT_EQ(charArray[5], nullptr);
437 
438     name = "/usr/bin/du";
439     argV = utils::internal::constructArgv(name);
440     charArray = argV.data();
441     EXPECT_EQ(argV.size(), 2);
442     EXPECT_EQ(charArray[0], name);
443     EXPECT_EQ(charArray[1], nullptr);
444 
445     name = "/usr/bin/hexdump";
446     arg1 = "-C";
447     arg2 = "/path/to/filename";
448     argV = utils::internal::constructArgv(name, arg1, arg2);
449     charArray = argV.data();
450     EXPECT_EQ(argV.size(), 4);
451     EXPECT_EQ(charArray[0], name);
452     EXPECT_EQ(charArray[1], arg1);
453     EXPECT_EQ(charArray[2], arg2);
454     EXPECT_EQ(charArray[3], nullptr);
455 }
456