Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(245)

Side by Side Diff: delta_performer_unittest.cc

Issue 6332002: AU: Add more signing unit tests. (Closed) Base URL: ssh://git@gitrw.chromium.org:9222/update_engine.git@master
Patch Set: review comments Created 9 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2009 The Chromium OS Authors. All rights reserved. 1 // Copyright (c) 2009 The Chromium OS Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include <sys/mount.h> 5 #include <sys/mount.h>
6 #include <inttypes.h> 6 #include <inttypes.h>
7 7
8 #include <algorithm> 8 #include <algorithm>
9 #include <string> 9 #include <string>
10 #include <vector> 10 #include <vector>
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after
88 ScopedFdCloser fd_closer(&fd); 88 ScopedFdCloser fd_closer(&fd);
89 off_t rc = lseek(fd, size + 1, SEEK_SET); 89 off_t rc = lseek(fd, size + 1, SEEK_SET);
90 TEST_AND_RETURN_FALSE_ERRNO(rc != static_cast<off_t>(-1)); 90 TEST_AND_RETURN_FALSE_ERRNO(rc != static_cast<off_t>(-1));
91 int return_code = ftruncate(fd, size); 91 int return_code = ftruncate(fd, size);
92 TEST_AND_RETURN_FALSE_ERRNO(return_code == 0); 92 TEST_AND_RETURN_FALSE_ERRNO(return_code == 0);
93 return true; 93 return true;
94 } 94 }
95 } // namespace {} 95 } // namespace {}
96 96
97 namespace { 97 namespace {
98 enum SignatureTest {
99 kSignatureNone, // No payload signing.
100 kSignatureGenerator, // Sign the payload at generation time.
101 kSignatureGenerated, // Sign the payload after it's generated.
102 kSignatureGeneratedShell, // Sign the generated payload through shell cmds.
103 };
104
105 size_t GetSignatureSize() {
106 const vector<char> data(1, 'x');
107 vector<char> hash;
108 EXPECT_TRUE(OmahaHashCalculator::RawHashOfData(data, &hash));
109 vector<char> signature;
110 EXPECT_TRUE(PayloadSigner::SignHash(hash,
111 kUnittestPrivateKeyPath,
112 &signature));
113 return signature.size();
114 }
115
116 void SignGeneratedPayload(const string& payload_path) {
117 int signature_size = GetSignatureSize();
118 vector<char> hash;
119 ASSERT_TRUE(PayloadSigner::HashPayloadForSigning(payload_path,
120 signature_size,
121 &hash));
122 vector<char> signature;
123 ASSERT_TRUE(PayloadSigner::SignHash(hash,
124 kUnittestPrivateKeyPath,
125 &signature));
126 ASSERT_TRUE(PayloadSigner::AddSignatureToPayload(payload_path,
127 signature,
128 payload_path));
129 EXPECT_TRUE(PayloadSigner::VerifySignedPayload(payload_path,
130 kUnittestPublicKeyPath));
131 }
132
133 void SignGeneratedShellPayload(const string& payload_path) {
134 int signature_size = GetSignatureSize();
135 string hash_file;
136 ASSERT_TRUE(utils::MakeTempFile("/tmp/hash.XXXXXX", &hash_file, NULL));
137 ScopedPathUnlinker hash_unlinker(hash_file);
138
139 ASSERT_EQ(0,
140 System(StringPrintf(
141 "./delta_generator -in_file %s -signature_size %d "
142 "-out_hash_file %s",
143 payload_path.c_str(),
144 signature_size,
145 hash_file.c_str())));
146
147 string sig_file;
148 ASSERT_TRUE(utils::MakeTempFile("/tmp/signature.XXXXXX", &sig_file, NULL));
149 ScopedPathUnlinker sig_unlinker(sig_file);
150 ASSERT_EQ(0,
151 System(StringPrintf(
152 "/usr/bin/openssl rsautl -pkcs -sign -inkey %s -in %s -out %s",
153 kUnittestPrivateKeyPath,
154 hash_file.c_str(),
155 sig_file.c_str())));
156 ASSERT_EQ(0,
157 System(StringPrintf(
158 "./delta_generator -in_file %s -signature_file %s "
159 "-out_file %s",
160 payload_path.c_str(),
161 sig_file.c_str(),
162 payload_path.c_str())));
163 ASSERT_EQ(0,
164 System(StringPrintf(
165 "./delta_generator -in_file %s -public_key %s",
166 payload_path.c_str(),
167 kUnittestPublicKeyPath)));
168 }
169
98 void DoSmallImageTest(bool full_kernel, bool full_rootfs, bool noop, 170 void DoSmallImageTest(bool full_kernel, bool full_rootfs, bool noop,
99 bool post_sign) { 171 SignatureTest signature_test) {
100 string a_img, b_img; 172 string a_img, b_img;
101 EXPECT_TRUE(utils::MakeTempFile("/tmp/a_img.XXXXXX", &a_img, NULL)); 173 EXPECT_TRUE(utils::MakeTempFile("/tmp/a_img.XXXXXX", &a_img, NULL));
102 ScopedPathUnlinker a_img_unlinker(a_img); 174 ScopedPathUnlinker a_img_unlinker(a_img);
103 EXPECT_TRUE(utils::MakeTempFile("/tmp/b_img.XXXXXX", &b_img, NULL)); 175 EXPECT_TRUE(utils::MakeTempFile("/tmp/b_img.XXXXXX", &b_img, NULL));
104 ScopedPathUnlinker b_img_unlinker(b_img); 176 ScopedPathUnlinker b_img_unlinker(b_img);
105 177
106 CreateExtImageAtPath(a_img, NULL); 178 CreateExtImageAtPath(a_img, NULL);
107 179
108 int image_size = static_cast<int>(utils::FileSize(a_img)); 180 int image_size = static_cast<int>(utils::FileSize(a_img));
109 181
(...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after
194 new_kernel.c_str(), &new_kernel_data[0], new_kernel_data.size())); 266 new_kernel.c_str(), &new_kernel_data[0], new_kernel_data.size()));
195 267
196 string delta_path; 268 string delta_path;
197 EXPECT_TRUE(utils::MakeTempFile("/tmp/delta.XXXXXX", &delta_path, NULL)); 269 EXPECT_TRUE(utils::MakeTempFile("/tmp/delta.XXXXXX", &delta_path, NULL));
198 LOG(INFO) << "delta path: " << delta_path; 270 LOG(INFO) << "delta path: " << delta_path;
199 ScopedPathUnlinker delta_path_unlinker(delta_path); 271 ScopedPathUnlinker delta_path_unlinker(delta_path);
200 { 272 {
201 string a_mnt, b_mnt; 273 string a_mnt, b_mnt;
202 ScopedLoopMounter a_mounter(a_img, &a_mnt, MS_RDONLY); 274 ScopedLoopMounter a_mounter(a_img, &a_mnt, MS_RDONLY);
203 ScopedLoopMounter b_mounter(b_img, &b_mnt, MS_RDONLY); 275 ScopedLoopMounter b_mounter(b_img, &b_mnt, MS_RDONLY);
204 276 const string private_key =
277 signature_test == kSignatureGenerator ? kUnittestPrivateKeyPath : "";
205 EXPECT_TRUE( 278 EXPECT_TRUE(
206 DeltaDiffGenerator::GenerateDeltaUpdateFile( 279 DeltaDiffGenerator::GenerateDeltaUpdateFile(
207 full_rootfs ? "" : a_mnt, 280 full_rootfs ? "" : a_mnt,
208 full_rootfs ? "" : a_img, 281 full_rootfs ? "" : a_img,
209 b_mnt, 282 b_mnt,
210 b_img, 283 b_img,
211 full_kernel ? "" : old_kernel, 284 full_kernel ? "" : old_kernel,
212 new_kernel, 285 new_kernel,
213 delta_path, 286 delta_path,
214 post_sign ? "" : kUnittestPrivateKeyPath)); 287 private_key));
215 } 288 }
216 289
217 if (post_sign) { 290 if (signature_test == kSignatureGenerated) {
218 int signature_size; 291 SignGeneratedPayload(delta_path);
219 { 292 } else if (signature_test == kSignatureGeneratedShell) {
220 const vector<char> data(1, 'x'); 293 SignGeneratedShellPayload(delta_path);
221 vector<char> hash;
222 ASSERT_TRUE(OmahaHashCalculator::RawHashOfData(data, &hash));
223 vector<char> signature;
224 ASSERT_TRUE(PayloadSigner::SignHash(hash,
225 kUnittestPrivateKeyPath,
226 &signature));
227 signature_size = signature.size();
228 }
229
230 vector<char> hash;
231 ASSERT_TRUE(PayloadSigner::HashPayloadForSigning(delta_path,
232 signature_size,
233 &hash));
234 vector<char> signature;
235 ASSERT_TRUE(PayloadSigner::SignHash(hash,
236 kUnittestPrivateKeyPath,
237 &signature));
238 ASSERT_TRUE(PayloadSigner::AddSignatureToPayload(delta_path,
239 signature,
240 delta_path));
241 EXPECT_TRUE(PayloadSigner::VerifySignedPayload(delta_path,
242 kUnittestPublicKeyPath));
243 } 294 }
244 295
245 // Read delta into memory. 296 // Read delta into memory.
246 vector<char> delta; 297 vector<char> delta;
247 EXPECT_TRUE(utils::ReadFile(delta_path, &delta)); 298 EXPECT_TRUE(utils::ReadFile(delta_path, &delta));
248 299
249 uint64_t manifest_metadata_size; 300 uint64_t manifest_metadata_size;
250 301
251 // Check the metadata. 302 // Check the metadata.
252 { 303 {
253 LOG(INFO) << "delta size: " << delta.size(); 304 LOG(INFO) << "delta size: " << delta.size();
254 DeltaArchiveManifest manifest; 305 DeltaArchiveManifest manifest;
255 const int kManifestSizeOffset = 12; 306 const int kManifestSizeOffset = 12;
256 const int kManifestOffset = 20; 307 const int kManifestOffset = 20;
257 uint64_t manifest_size = 0; 308 uint64_t manifest_size = 0;
258 memcpy(&manifest_size, &delta[kManifestSizeOffset], sizeof(manifest_size)); 309 memcpy(&manifest_size, &delta[kManifestSizeOffset], sizeof(manifest_size));
259 manifest_size = be64toh(manifest_size); 310 manifest_size = be64toh(manifest_size);
260 LOG(INFO) << "manifest size: " << manifest_size; 311 LOG(INFO) << "manifest size: " << manifest_size;
261 EXPECT_TRUE(manifest.ParseFromArray(&delta[kManifestOffset], 312 EXPECT_TRUE(manifest.ParseFromArray(&delta[kManifestOffset],
262 manifest_size)); 313 manifest_size));
263 EXPECT_TRUE(manifest.has_signatures_offset());
264 manifest_metadata_size = kManifestOffset + manifest_size; 314 manifest_metadata_size = kManifestOffset + manifest_size;
265 315
266 Signatures sigs_message; 316 if (signature_test == kSignatureNone) {
267 EXPECT_TRUE(sigs_message.ParseFromArray( 317 EXPECT_FALSE(manifest.has_signatures_offset());
268 &delta[manifest_metadata_size + manifest.signatures_offset()], 318 EXPECT_FALSE(manifest.has_signatures_size());
269 manifest.signatures_size())); 319 } else {
270 EXPECT_EQ(1, sigs_message.signatures_size()); 320 EXPECT_TRUE(manifest.has_signatures_offset());
271 const Signatures_Signature& signature = sigs_message.signatures(0); 321 EXPECT_TRUE(manifest.has_signatures_size());
272 EXPECT_EQ(1, signature.version()); 322 Signatures sigs_message;
323 EXPECT_TRUE(sigs_message.ParseFromArray(
324 &delta[manifest_metadata_size + manifest.signatures_offset()],
325 manifest.signatures_size()));
326 EXPECT_EQ(1, sigs_message.signatures_size());
327 const Signatures_Signature& signature = sigs_message.signatures(0);
328 EXPECT_EQ(1, signature.version());
273 329
274 uint64_t expected_sig_data_length = 0; 330 uint64_t expected_sig_data_length = 0;
275 EXPECT_TRUE(PayloadSigner::SignatureBlobLength(kUnittestPrivateKeyPath, 331 EXPECT_TRUE(PayloadSigner::SignatureBlobLength(
276 &expected_sig_data_length)); 332 kUnittestPrivateKeyPath, &expected_sig_data_length));
277 EXPECT_EQ(expected_sig_data_length, manifest.signatures_size()); 333 EXPECT_EQ(expected_sig_data_length, manifest.signatures_size());
278 EXPECT_FALSE(signature.data().empty()); 334 EXPECT_FALSE(signature.data().empty());
335 }
279 336
280 if (noop) { 337 if (noop) {
281 EXPECT_EQ(1, manifest.install_operations_size()); 338 EXPECT_EQ(1, manifest.install_operations_size());
282 EXPECT_EQ(1, manifest.kernel_install_operations_size()); 339 EXPECT_EQ(1, manifest.kernel_install_operations_size());
283 } 340 }
284 341
285 if (full_kernel) { 342 if (full_kernel) {
286 EXPECT_FALSE(manifest.has_old_kernel_info()); 343 EXPECT_FALSE(manifest.has_old_kernel_info());
287 } else { 344 } else {
288 EXPECT_EQ(old_kernel_data.size(), manifest.old_kernel_info().size()); 345 EXPECT_EQ(old_kernel_data.size(), manifest.old_kernel_info().size());
(...skipping 18 matching lines...) Expand all
307 EXPECT_CALL(prefs, SetInt64(kPrefsManifestMetadataSize, 364 EXPECT_CALL(prefs, SetInt64(kPrefsManifestMetadataSize,
308 manifest_metadata_size)).WillOnce(Return(true)); 365 manifest_metadata_size)).WillOnce(Return(true));
309 EXPECT_CALL(prefs, SetInt64(kPrefsUpdateStateNextOperation, _)) 366 EXPECT_CALL(prefs, SetInt64(kPrefsUpdateStateNextOperation, _))
310 .WillRepeatedly(Return(true)); 367 .WillRepeatedly(Return(true));
311 EXPECT_CALL(prefs, GetInt64(kPrefsUpdateStateNextOperation, _)) 368 EXPECT_CALL(prefs, GetInt64(kPrefsUpdateStateNextOperation, _))
312 .WillOnce(Return(false)); 369 .WillOnce(Return(false));
313 EXPECT_CALL(prefs, SetInt64(kPrefsUpdateStateNextDataOffset, _)) 370 EXPECT_CALL(prefs, SetInt64(kPrefsUpdateStateNextDataOffset, _))
314 .WillRepeatedly(Return(true)); 371 .WillRepeatedly(Return(true));
315 EXPECT_CALL(prefs, SetString(kPrefsUpdateStateSHA256Context, _)) 372 EXPECT_CALL(prefs, SetString(kPrefsUpdateStateSHA256Context, _))
316 .WillRepeatedly(Return(true)); 373 .WillRepeatedly(Return(true));
317 EXPECT_CALL(prefs, SetString(kPrefsUpdateStateSignedSHA256Context, _)) 374 if (signature_test != kSignatureNone) {
318 .WillOnce(Return(true)); 375 EXPECT_CALL(prefs, SetString(kPrefsUpdateStateSignedSHA256Context, _))
376 .WillOnce(Return(true));
377 }
319 378
320 // Update the A image in place. 379 // Update the A image in place.
321 DeltaPerformer performer(&prefs); 380 DeltaPerformer performer(&prefs);
322 381
323 vector<char> rootfs_hash; 382 vector<char> rootfs_hash;
324 EXPECT_EQ(image_size, 383 EXPECT_EQ(image_size,
325 OmahaHashCalculator::RawHashOfFile(a_img, 384 OmahaHashCalculator::RawHashOfFile(a_img,
326 image_size, 385 image_size,
327 &rootfs_hash)); 386 &rootfs_hash));
328 performer.set_current_rootfs_hash(rootfs_hash); 387 performer.set_current_rootfs_hash(rootfs_hash);
(...skipping 17 matching lines...) Expand all
346 405
347 CompareFilesByBlock(old_kernel, new_kernel); 406 CompareFilesByBlock(old_kernel, new_kernel);
348 CompareFilesByBlock(a_img, b_img); 407 CompareFilesByBlock(a_img, b_img);
349 408
350 vector<char> updated_kernel_partition; 409 vector<char> updated_kernel_partition;
351 EXPECT_TRUE(utils::ReadFile(old_kernel, &updated_kernel_partition)); 410 EXPECT_TRUE(utils::ReadFile(old_kernel, &updated_kernel_partition));
352 EXPECT_EQ(0, strncmp(&updated_kernel_partition[0], new_data_string, 411 EXPECT_EQ(0, strncmp(&updated_kernel_partition[0], new_data_string,
353 strlen(new_data_string))); 412 strlen(new_data_string)));
354 413
355 EXPECT_TRUE(utils::FileExists(kUnittestPublicKeyPath)); 414 EXPECT_TRUE(utils::FileExists(kUnittestPublicKeyPath));
356 EXPECT_TRUE(performer.VerifyPayload( 415 EXPECT_EQ(signature_test != kSignatureNone,
357 kUnittestPublicKeyPath, 416 performer.VerifyPayload(
358 OmahaHashCalculator::OmahaHashOfData(delta), 417 kUnittestPublicKeyPath,
359 delta.size())); 418 OmahaHashCalculator::OmahaHashOfData(delta),
419 delta.size()));
360 420
361 uint64_t new_kernel_size; 421 uint64_t new_kernel_size;
362 vector<char> new_kernel_hash; 422 vector<char> new_kernel_hash;
363 uint64_t new_rootfs_size; 423 uint64_t new_rootfs_size;
364 vector<char> new_rootfs_hash; 424 vector<char> new_rootfs_hash;
365 EXPECT_TRUE(performer.GetNewPartitionInfo(&new_kernel_size, 425 EXPECT_TRUE(performer.GetNewPartitionInfo(&new_kernel_size,
366 &new_kernel_hash, 426 &new_kernel_hash,
367 &new_rootfs_size, 427 &new_rootfs_size,
368 &new_rootfs_hash)); 428 &new_rootfs_hash));
369 EXPECT_EQ(4096, new_kernel_size); 429 EXPECT_EQ(4096, new_kernel_size);
370 vector<char> expected_new_kernel_hash; 430 vector<char> expected_new_kernel_hash;
371 EXPECT_TRUE(OmahaHashCalculator::RawHashOfData(new_kernel_data, 431 EXPECT_TRUE(OmahaHashCalculator::RawHashOfData(new_kernel_data,
372 &expected_new_kernel_hash)); 432 &expected_new_kernel_hash));
373 EXPECT_TRUE(expected_new_kernel_hash == new_kernel_hash); 433 EXPECT_TRUE(expected_new_kernel_hash == new_kernel_hash);
374 EXPECT_EQ(image_size, new_rootfs_size); 434 EXPECT_EQ(image_size, new_rootfs_size);
375 vector<char> expected_new_rootfs_hash; 435 vector<char> expected_new_rootfs_hash;
376 EXPECT_EQ(image_size, 436 EXPECT_EQ(image_size,
377 OmahaHashCalculator::RawHashOfFile(b_img, 437 OmahaHashCalculator::RawHashOfFile(b_img,
378 image_size, 438 image_size,
379 &expected_new_rootfs_hash)); 439 &expected_new_rootfs_hash));
380 EXPECT_TRUE(expected_new_rootfs_hash == new_rootfs_hash); 440 EXPECT_TRUE(expected_new_rootfs_hash == new_rootfs_hash);
381 } 441 }
382 } 442 }
383 443
384 TEST(DeltaPerformerTest, RunAsRootSmallImageTest) { 444 TEST(DeltaPerformerTest, RunAsRootSmallImageTest) {
385 DoSmallImageTest(false, false, false, false); 445 DoSmallImageTest(false, false, false, kSignatureGenerator);
386 } 446 }
387 447
388 TEST(DeltaPerformerTest, RunAsRootFullKernelSmallImageTest) { 448 TEST(DeltaPerformerTest, RunAsRootFullKernelSmallImageTest) {
389 DoSmallImageTest(true, false, false, false); 449 DoSmallImageTest(true, false, false, kSignatureGenerator);
390 } 450 }
391 451
392 TEST(DeltaPerformerTest, RunAsRootFullSmallImageTest) { 452 TEST(DeltaPerformerTest, RunAsRootFullSmallImageTest) {
393 DoSmallImageTest(true, true, false, false); 453 DoSmallImageTest(true, true, false, kSignatureGenerator);
394 } 454 }
395 455
396 TEST(DeltaPerformerTest, RunAsRootNoopSmallImageTest) { 456 TEST(DeltaPerformerTest, RunAsRootNoopSmallImageTest) {
397 DoSmallImageTest(false, false, true, false); 457 DoSmallImageTest(false, false, true, kSignatureGenerator);
398 } 458 }
399 459
400 TEST(DeltaPerformerTest, RunAsRootSmallImagePostSignTest) { 460 TEST(DeltaPerformerTest, RunAsRootSmallImageSignNoneTest) {
401 DoSmallImageTest(false, false, false, true); 461 DoSmallImageTest(false, false, false, kSignatureNone);
462 }
463
464 TEST(DeltaPerformerTest, RunAsRootSmallImageSignGeneratedTest) {
465 DoSmallImageTest(false, false, false, kSignatureGenerated);
466 }
467
468 TEST(DeltaPerformerTest, RunAsRootSmallImageSignGeneratedShellTest) {
469 DoSmallImageTest(false, false, false, kSignatureGeneratedShell);
402 } 470 }
403 471
404 TEST(DeltaPerformerTest, BadDeltaMagicTest) { 472 TEST(DeltaPerformerTest, BadDeltaMagicTest) {
405 PrefsMock prefs; 473 PrefsMock prefs;
406 DeltaPerformer performer(&prefs); 474 DeltaPerformer performer(&prefs);
407 EXPECT_EQ(0, performer.Open("/dev/null", 0, 0)); 475 EXPECT_EQ(0, performer.Open("/dev/null", 0, 0));
408 EXPECT_TRUE(performer.OpenKernel("/dev/null")); 476 EXPECT_TRUE(performer.OpenKernel("/dev/null"));
409 EXPECT_EQ(4, performer.Write("junk", 4)); 477 EXPECT_EQ(4, performer.Write("junk", 4));
410 EXPECT_EQ(8, performer.Write("morejunk", 8)); 478 EXPECT_EQ(8, performer.Write("morejunk", 8));
411 EXPECT_LT(performer.Write("morejunk", 8), 0); 479 EXPECT_LT(performer.Write("morejunk", 8), 0);
(...skipping 10 matching lines...) Expand all
422 op.clear_src_extents(); 490 op.clear_src_extents();
423 *(op.add_src_extents()) = ExtentForRange(5, 3); 491 *(op.add_src_extents()) = ExtentForRange(5, 3);
424 EXPECT_TRUE(DeltaPerformer::IsIdempotentOperation(op)); 492 EXPECT_TRUE(DeltaPerformer::IsIdempotentOperation(op));
425 *(op.add_dst_extents()) = ExtentForRange(20, 6); 493 *(op.add_dst_extents()) = ExtentForRange(20, 6);
426 EXPECT_TRUE(DeltaPerformer::IsIdempotentOperation(op)); 494 EXPECT_TRUE(DeltaPerformer::IsIdempotentOperation(op));
427 *(op.add_src_extents()) = ExtentForRange(19, 2); 495 *(op.add_src_extents()) = ExtentForRange(19, 2);
428 EXPECT_FALSE(DeltaPerformer::IsIdempotentOperation(op)); 496 EXPECT_FALSE(DeltaPerformer::IsIdempotentOperation(op));
429 } 497 }
430 498
431 } // namespace chromeos_update_engine 499 } // namespace chromeos_update_engine
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698