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[i * 2], 3, "%02x", (unsigned int)digest[i]);
147     }
148     std::string hexId = std::string(mdString);
149     hexId = hexId.substr(0, 8);
150     EXPECT_EQ(Version::getId(version), hexId);
151 }
152 
153 TEST_F(VersionTest, TestGetExtendedVersion)
154 {
155     auto releasePath = _directory + "/" + "os-release";
156     auto ExtendedVersion = "9.88.1-test-ExtendedVersion";
157 
158     std::ofstream file;
159     file.open(releasePath, std::ofstream::out);
160     ASSERT_TRUE(file.is_open());
161 
162     file << "EXTENDED_VERSION=" << ExtendedVersion << "\n";
163     file.close();
164 
165     EXPECT_EQ(Version::getBMCExtendedVersion(releasePath), ExtendedVersion);
166 }
167 
168 class SignatureTest : public testing::Test
169 {
170   public:
171     static constexpr auto opensslCmd = "openssl dgst -sha256 -sign ";
172     static constexpr auto testPath = "/tmp/_testSig";
173 
174   protected:
175     void command(const std::string& cmd)
176     {
177         auto val = std::system(cmd.c_str());
178         if (val)
179         {
180             std::cout << "COMMAND Error: " << val << std::endl;
181         }
182     }
183     virtual void SetUp()
184     {
185         // Create test base directory.
186         fs::create_directories(testPath);
187 
188         // Create unique temporary path for images
189         std::string tmpDir(testPath);
190         tmpDir += "/extractXXXXXX";
191         std::string imageDir = mkdtemp(const_cast<char*>(tmpDir.c_str()));
192 
193         // Create unique temporary configuration path
194         std::string tmpConfDir(testPath);
195         tmpConfDir += "/confXXXXXX";
196         std::string confDir = mkdtemp(const_cast<char*>(tmpConfDir.c_str()));
197 
198         extractPath = imageDir;
199         extractPath /= "images";
200 
201         signedConfPath = confDir;
202         signedConfPath /= "conf";
203 
204         signedConfOpenBMCPath = confDir;
205         signedConfOpenBMCPath /= "conf";
206         signedConfOpenBMCPath /= "OpenBMC";
207 
208         std::cout << "SETUP " << std::endl;
209 
210         command("mkdir " + extractPath.string());
211         command("mkdir " + signedConfPath.string());
212         command("mkdir " + signedConfOpenBMCPath.string());
213 
214         std::string hashFile = signedConfOpenBMCPath.string() + "/hashfunc";
215         command("echo \"HashType=RSA-SHA256\" > " + hashFile);
216 
217         std::string manifestFile = extractPath.string() + "/" + "MANIFEST";
218         command(
219             "echo \"purpose=xyz.openbmc_project.Software.Version.VersionPurpose.BMC\" > " +
220             manifestFile);
221         command("echo \"HashType=RSA-SHA256\" >> " + manifestFile);
222         command("echo \"KeyType=OpenBMC\" >> " + manifestFile);
223 
224         std::string kernelFile = extractPath.string() + "/" + "image-kernel";
225         command("echo \"image-kernel file \" > " + kernelFile);
226 
227         std::string rofsFile = extractPath.string() + "/" + "image-rofs";
228         command("echo \"image-rofs file \" > " + rofsFile);
229 
230         std::string rwfsFile = extractPath.string() + "/" + "image-rwfs";
231         command("echo \"image-rwfs file \" > " + rwfsFile);
232 
233         std::string ubootFile = extractPath.string() + "/" + "image-u-boot";
234         command("echo \"image-u-boot file \" > " + ubootFile);
235 
236         std::string pkeyFile = extractPath.string() + "/" + "private.pem";
237         command("openssl genrsa  -out " + pkeyFile + " 2048");
238 
239         std::string pubkeyFile = extractPath.string() + "/" + "publickey";
240         command("openssl rsa -in " + pkeyFile + " -outform PEM " +
241                 "-pubout -out " + pubkeyFile);
242 
243         command("cp " + pubkeyFile + " " + signedConfOpenBMCPath.string());
244         command(opensslCmd + pkeyFile + " -out " + kernelFile + ".sig " +
245                 kernelFile);
246 
247         command(opensslCmd + pkeyFile + " -out " + manifestFile + ".sig " +
248                 manifestFile);
249         command(opensslCmd + pkeyFile + " -out " + rofsFile + ".sig " +
250                 rofsFile);
251         command(opensslCmd + pkeyFile + " -out " + rwfsFile + ".sig " +
252                 rwfsFile);
253         command(opensslCmd + pkeyFile + " -out " + ubootFile + ".sig " +
254                 ubootFile);
255         command(opensslCmd + pkeyFile + " -out " + pubkeyFile + ".sig " +
256                 pubkeyFile);
257 
258 #ifdef WANT_SIGNATURE_VERIFY
259         std::string fullFile = extractPath.string() + "/" + "image-full";
260         command("cat " + kernelFile + ".sig " + rofsFile + ".sig " + rwfsFile +
261                 ".sig " + ubootFile + ".sig " + manifestFile + ".sig " +
262                 pubkeyFile + ".sig > " + fullFile);
263         command(opensslCmd + pkeyFile + " -out " + fullFile + ".sig " +
264                 fullFile);
265 #endif
266 
267         signature = std::make_unique<Signature>(extractPath, signedConfPath);
268     }
269     virtual void TearDown()
270     {
271         command("rm -rf " + std::string(testPath));
272     }
273 
274     std::unique_ptr<Signature> signature;
275     fs::path extractPath;
276     fs::path signedConfPath;
277     fs::path signedConfOpenBMCPath;
278 };
279 
280 /** @brief Test for success scenario*/
281 TEST_F(SignatureTest, TestSignatureVerify)
282 {
283     EXPECT_TRUE(signature->verify());
284 }
285 
286 /** @brief Test failure scenario with corrupted signature file*/
287 TEST_F(SignatureTest, TestCorruptSignatureFile)
288 {
289     // corrupt the image-kernel.sig file and ensure that verification fails
290     std::string kernelFile = extractPath.string() + "/" + "image-kernel";
291     command("echo \"dummy data\" > " + kernelFile + ".sig ");
292     EXPECT_FALSE(signature->verify());
293 }
294 
295 /** @brief Test failure scenario with no public key in the image*/
296 TEST_F(SignatureTest, TestNoPublicKeyInImage)
297 {
298     // Remove publickey file from the image and ensure that verify fails
299     std::string pubkeyFile = extractPath.string() + "/" + "publickey";
300     command("rm " + pubkeyFile);
301     EXPECT_FALSE(signature->verify());
302 }
303 
304 /** @brief Test failure scenario with invalid hash function value*/
305 TEST_F(SignatureTest, TestInvalidHashValue)
306 {
307     // Change the hashfunc value and ensure that verification fails
308     std::string hashFile = signedConfOpenBMCPath.string() + "/hashfunc";
309     command("echo \"HashType=md5\" > " + hashFile);
310     EXPECT_FALSE(signature->verify());
311 }
312 
313 /** @brief Test for failure scenario with no config file in system*/
314 TEST_F(SignatureTest, TestNoConfigFileInSystem)
315 {
316     // Remove the conf folder in the system and ensure that verify fails
317     command("rm -rf " + signedConfOpenBMCPath.string());
318     EXPECT_FALSE(signature->verify());
319 }
320 
321 #ifdef WANT_SIGNATURE_VERIFY
322 /** @brief Test for failure scenario without full verification */
323 TEST_F(SignatureTest, TestNoFullSignature)
324 {
325     // Remove the full signature and ensure that verify fails
326     std::string fullFile = extractPath.string() + "/" + "image-full.sig";
327     command("rm " + fullFile);
328     EXPECT_FALSE(signature->verify());
329 }
330 
331 /** @brief Test for failure scenario without full verification */
332 TEST_F(SignatureTest, TestNoFullSignatureForBIOS)
333 {
334     // Remove the full signature
335     std::string fullFile = extractPath.string() + "/" + "image-full.sig";
336     command("rm " + fullFile);
337 
338     // Change the purpose to BIOS
339     std::string manifestFile = extractPath.string() + "/" + "MANIFEST";
340     std::string pkeyFile = extractPath.string() + "/" + "private.pem";
341     command("sed -i s/VersionPurpose.BMC/VersionPurpose.BIOS/ " + manifestFile);
342     command(opensslCmd + pkeyFile + " -out " + manifestFile + ".sig " +
343             manifestFile);
344 
345     // Re-create signature object and make sure verify succeed.
346     signature = std::make_unique<Signature>(extractPath, signedConfPath);
347     EXPECT_TRUE(signature->verify());
348 }
349 #endif
350 
351 class FileTest : public testing::Test
352 {
353   protected:
354     std::string readFile(fs::path path)
355     {
356         std::ifstream f(path, std::ios::in);
357         const auto sz = fs::file_size(path);
358         std::string result(sz, '\0');
359         f.read(result.data(), sz);
360 
361         return result;
362     }
363 
364     void command(const std::string& cmd)
365     {
366         auto val = std::system(cmd.c_str());
367         if (val)
368         {
369             std::cout << "COMMAND Error: " << val << std::endl;
370         }
371     }
372 
373     virtual void SetUp()
374     {
375         // Create test base directory.
376         tmpDir = fs::temp_directory_path() / "testFileXXXXXX";
377         if (!mkdtemp(tmpDir.data()))
378         {
379             throw "Failed to create tmp dir";
380         }
381 
382         std::string file1 = tmpDir + "/file1";
383         std::string file2 = tmpDir + "/file2";
384         command("echo \"File Test1\n\n\" > " + file1);
385         command("echo \"FileTe st2\n\nte st2\" > " + file2);
386 
387         srcFiles.push_back(file1);
388         srcFiles.push_back(file2);
389     }
390 
391     virtual void TearDown()
392     {
393         fs::remove_all(tmpDir);
394     }
395 
396     std::vector<std::string> srcFiles;
397     std::string tmpDir;
398 };
399 
400 TEST_F(FileTest, TestMergeFiles)
401 {
402     std::string retFile = tmpDir + "/retFile";
403     for (auto file : srcFiles)
404     {
405         command("cat " + file + " >> " + retFile);
406     }
407 
408     std::string dstFile = tmpDir + "/dstFile";
409     utils::mergeFiles(srcFiles, dstFile);
410 
411     ASSERT_NE(fs::file_size(retFile), static_cast<uintmax_t>(-1));
412     ASSERT_NE(fs::file_size(dstFile), static_cast<uintmax_t>(-1));
413     ASSERT_EQ(fs::file_size(retFile), fs::file_size(dstFile));
414 
415     std::string ssRetFile = readFile(fs::path(retFile));
416     std::string ssDstFile = readFile(fs::path(dstFile));
417     ASSERT_EQ(ssRetFile, ssDstFile);
418 }
419 
420 TEST(ExecTest, TestConstructArgv)
421 {
422     auto name = "/bin/ls";
423     auto arg1 = "-a";
424     auto arg2 = "-l";
425     auto arg3 = "-t";
426     auto arg4 = "-rS";
427     auto argV = utils::internal::constructArgv(name, arg1, arg2, arg3, arg4);
428     char** charArray = argV.data();
429     EXPECT_EQ(argV.size(), 6);
430     EXPECT_EQ(charArray[0], name);
431     EXPECT_EQ(charArray[1], arg1);
432     EXPECT_EQ(charArray[2], arg2);
433     EXPECT_EQ(charArray[3], arg3);
434     EXPECT_EQ(charArray[4], arg4);
435     EXPECT_EQ(charArray[5], nullptr);
436 
437     name = "/usr/bin/du";
438     argV = utils::internal::constructArgv(name);
439     charArray = argV.data();
440     EXPECT_EQ(argV.size(), 2);
441     EXPECT_EQ(charArray[0], name);
442     EXPECT_EQ(charArray[1], nullptr);
443 
444     name = "/usr/bin/hexdump";
445     arg1 = "-C";
446     arg2 = "/path/to/filename";
447     argV = utils::internal::constructArgv(name, arg1, arg2);
448     charArray = argV.data();
449     EXPECT_EQ(argV.size(), 4);
450     EXPECT_EQ(charArray[0], name);
451     EXPECT_EQ(charArray[1], arg1);
452     EXPECT_EQ(charArray[2], arg2);
453     EXPECT_EQ(charArray[3], nullptr);
454 }
455