1pub mod config;
25pub mod docker;
26pub mod error;
27pub mod initramfs;
28
29use std::fs;
30use std::path::{Path, PathBuf};
31
32use sha3::{Digest, Sha3_256};
33
34use rvf_types::kernel::{KernelArch, KernelHeader, KernelType, KERNEL_MAGIC};
35use rvf_types::kernel_binding::KernelBinding;
36
37use crate::docker::DockerBuildContext;
38use crate::error::KernelError;
39
40#[derive(Clone, Debug)]
42pub struct KernelConfig {
43 pub prebuilt_path: Option<PathBuf>,
45 pub docker_context: Option<PathBuf>,
47 pub cmdline: String,
49 pub arch: KernelArch,
51 pub with_initramfs: bool,
53 pub services: Vec<String>,
55 pub kernel_version: Option<String>,
57}
58
59impl Default for KernelConfig {
60 fn default() -> Self {
61 Self {
62 prebuilt_path: None,
63 docker_context: None,
64 cmdline: "console=ttyS0 quiet".to_string(),
65 arch: KernelArch::X86_64,
66 with_initramfs: false,
67 services: Vec::new(),
68 kernel_version: None,
69 }
70 }
71}
72
73#[derive(Clone, Debug)]
75pub struct BuiltKernel {
76 pub bzimage: Vec<u8>,
78 pub initramfs: Option<Vec<u8>>,
80 pub config: KernelConfig,
82 pub image_hash: [u8; 32],
84 pub compressed_size: u64,
86}
87
88pub struct KernelBuilder {
90 arch: KernelArch,
91 kernel_type: KernelType,
92 config: KernelConfig,
93}
94
95impl KernelBuilder {
96 pub fn new(arch: KernelArch) -> Self {
98 Self {
99 arch,
100 kernel_type: KernelType::MicroLinux,
101 config: KernelConfig {
102 arch,
103 ..Default::default()
104 },
105 }
106 }
107
108 pub fn kernel_type(mut self, kt: KernelType) -> Self {
110 self.kernel_type = kt;
111 self
112 }
113
114 pub fn cmdline(mut self, cmdline: &str) -> Self {
116 self.config.cmdline = cmdline.to_string();
117 self
118 }
119
120 pub fn with_initramfs(mut self, services: &[&str]) -> Self {
122 self.config.with_initramfs = true;
123 self.config.services = services.iter().map(|s| s.to_string()).collect();
124 self
125 }
126
127 pub fn kernel_version(mut self, version: &str) -> Self {
129 self.config.kernel_version = Some(version.to_string());
130 self
131 }
132
133 pub fn from_prebuilt(path: &Path) -> Result<BuiltKernel, KernelError> {
142 if !path.exists() {
143 return Err(KernelError::Io(std::io::Error::new(
144 std::io::ErrorKind::NotFound,
145 format!("kernel image not found: {}", path.display()),
146 )));
147 }
148
149 let metadata = fs::metadata(path)?;
150 if metadata.len() < 512 {
151 return Err(KernelError::ImageTooSmall {
152 size: metadata.len(),
153 min_size: 512,
154 });
155 }
156
157 let bzimage = fs::read(path)?;
158
159 let is_elf = bzimage.len() >= 4 && &bzimage[..4] == b"\x7FELF";
161 let is_bzimage = bzimage.len() >= 514
162 && bzimage[510] == 0x55
163 && bzimage[511] == 0xAA;
164 let is_pe = bzimage.len() >= 2 && &bzimage[..2] == b"MZ";
165
166 if !is_elf && !is_bzimage && !is_pe && metadata.len() < 4096 {
167 return Err(KernelError::InvalidImage {
168 path: path.to_path_buf(),
169 reason: "not a recognized kernel format (ELF, bzImage, or PE)".into(),
170 });
171 }
172
173 let image_hash = sha3_256(&bzimage);
174 let compressed_size = bzimage.len() as u64;
175
176 Ok(BuiltKernel {
177 bzimage,
178 initramfs: None,
179 config: KernelConfig {
180 prebuilt_path: Some(path.to_path_buf()),
181 ..Default::default()
182 },
183 image_hash,
184 compressed_size,
185 })
186 }
187
188 pub fn build_docker(
197 &self,
198 context_dir: &Path,
199 ) -> Result<BuiltKernel, KernelError> {
200 let version = self
201 .config
202 .kernel_version
203 .as_deref()
204 .unwrap_or(docker::DEFAULT_KERNEL_VERSION);
205
206 let ctx = DockerBuildContext::prepare(context_dir, Some(version))?;
207 let bzimage = ctx.build()?;
208
209 let image_hash = sha3_256(&bzimage);
210 let compressed_size = bzimage.len() as u64;
211
212 let initramfs = if self.config.with_initramfs {
213 let services: Vec<&str> = self.config.services.iter().map(|s| s.as_str()).collect();
214 Some(initramfs::build_initramfs(&services, &[])?)
215 } else {
216 None
217 };
218
219 Ok(BuiltKernel {
220 bzimage,
221 initramfs,
222 config: self.config.clone(),
223 image_hash,
224 compressed_size,
225 })
226 }
227
228 pub fn build_initramfs(
236 &self,
237 services: &[&str],
238 extra_binaries: &[(&str, &[u8])],
239 ) -> Result<Vec<u8>, KernelError> {
240 initramfs::build_initramfs(services, extra_binaries)
241 }
242
243 pub fn kernel_flags(&self) -> u32 {
245 use rvf_types::kernel::*;
246
247 let mut flags = KERNEL_FLAG_COMPRESSED;
248
249 flags |= KERNEL_FLAG_HAS_VIRTIO_NET;
251 flags |= KERNEL_FLAG_HAS_VIRTIO_BLK;
252 flags |= KERNEL_FLAG_HAS_VSOCK;
253 flags |= KERNEL_FLAG_HAS_NETWORKING;
254
255 for svc in &self.config.services {
257 match svc.as_str() {
258 "rvf-server" => {
259 flags |= KERNEL_FLAG_HAS_QUERY_API;
260 }
261 "sshd" | "dropbear" => {
262 flags |= KERNEL_FLAG_HAS_ADMIN_API;
263 }
264 _ => {}
265 }
266 }
267
268 flags
269 }
270
271 pub fn arch_byte(&self) -> u8 {
273 self.arch as u8
274 }
275
276 pub fn kernel_type_byte(&self) -> u8 {
278 self.kernel_type as u8
279 }
280}
281
282pub struct KernelVerifier;
284
285impl KernelVerifier {
286 pub fn verify(
292 header_bytes: &[u8; 128],
293 image_bytes: &[u8],
294 ) -> Result<VerifiedKernel, KernelError> {
295 let header = KernelHeader::from_bytes(header_bytes).map_err(|e| {
296 KernelError::InvalidImage {
297 path: PathBuf::from("<embedded>"),
298 reason: format!("invalid kernel header: {e}"),
299 }
300 })?;
301
302 let actual_hash = sha3_256(image_bytes);
303
304 if actual_hash != header.image_hash {
305 return Err(KernelError::HashMismatch {
306 expected: header.image_hash,
307 actual: actual_hash,
308 });
309 }
310
311 let arch = KernelArch::try_from(header.arch).unwrap_or(KernelArch::Unknown);
312 let kernel_type = KernelType::try_from(header.kernel_type).unwrap_or(KernelType::Custom);
313
314 Ok(VerifiedKernel {
315 header,
316 arch,
317 kernel_type,
318 image_size: image_bytes.len() as u64,
319 })
320 }
321
322 pub fn verify_with_binding(
325 header_bytes: &[u8; 128],
326 binding_bytes: &[u8; 128],
327 image_bytes: &[u8],
328 ) -> Result<(VerifiedKernel, KernelBinding), KernelError> {
329 let verified = Self::verify(header_bytes, image_bytes)?;
330 let binding = KernelBinding::from_bytes_validated(binding_bytes).map_err(|e| {
331 KernelError::InvalidImage {
332 path: PathBuf::from("<embedded>"),
333 reason: format!("invalid kernel binding: {e}"),
334 }
335 })?;
336 Ok((verified, binding))
337 }
338}
339
340#[derive(Debug)]
342pub struct VerifiedKernel {
343 pub header: KernelHeader,
345 pub arch: KernelArch,
347 pub kernel_type: KernelType,
349 pub image_size: u64,
351}
352
353pub fn build_kernel_header(
358 kernel: &BuiltKernel,
359 builder: &KernelBuilder,
360 api_port: u16,
361) -> KernelHeader {
362 let cmdline_offset = 128u64; let cmdline_length = kernel.config.cmdline.len() as u32;
364
365 KernelHeader {
366 kernel_magic: KERNEL_MAGIC,
367 header_version: 1,
368 arch: builder.arch_byte(),
369 kernel_type: builder.kernel_type_byte(),
370 kernel_flags: builder.kernel_flags(),
371 min_memory_mb: 64, entry_point: 0x0020_0000, image_size: kernel.bzimage.len() as u64,
374 compressed_size: kernel.compressed_size,
375 compression: 0, api_transport: rvf_types::kernel::ApiTransport::TcpHttp as u8,
377 api_port,
378 api_version: 1,
379 image_hash: kernel.image_hash,
380 build_id: [0u8; 16], build_timestamp: 0, vcpu_count: 1,
383 reserved_0: 0,
384 cmdline_offset,
385 cmdline_length,
386 reserved_1: 0,
387 }
388}
389
390pub fn sha3_256(data: &[u8]) -> [u8; 32] {
392 let mut hasher = Sha3_256::new();
393 hasher.update(data);
394 let result = hasher.finalize();
395 let mut hash = [0u8; 32];
396 hash.copy_from_slice(&result);
397 hash
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn sha3_256_produces_32_bytes() {
406 let hash = sha3_256(b"test data");
407 assert_eq!(hash.len(), 32);
408 assert_eq!(hash, sha3_256(b"test data"));
410 assert_ne!(hash, sha3_256(b"other data"));
412 }
413
414 #[test]
415 fn kernel_builder_defaults() {
416 let builder = KernelBuilder::new(KernelArch::X86_64);
417 assert_eq!(builder.arch, KernelArch::X86_64);
418 assert_eq!(builder.kernel_type, KernelType::MicroLinux);
419 assert_eq!(builder.arch_byte(), 0x00);
420 assert_eq!(builder.kernel_type_byte(), 0x01);
421 }
422
423 #[test]
424 fn kernel_builder_chaining() {
425 let builder = KernelBuilder::new(KernelArch::Aarch64)
426 .kernel_type(KernelType::Custom)
427 .cmdline("console=ttyAMA0 root=/dev/vda")
428 .with_initramfs(&["sshd", "rvf-server"])
429 .kernel_version("6.6.30");
430
431 assert_eq!(builder.arch, KernelArch::Aarch64);
432 assert_eq!(builder.kernel_type, KernelType::Custom);
433 assert_eq!(builder.config.cmdline, "console=ttyAMA0 root=/dev/vda");
434 assert!(builder.config.with_initramfs);
435 assert_eq!(builder.config.services, vec!["sshd", "rvf-server"]);
436 assert_eq!(builder.config.kernel_version, Some("6.6.30".to_string()));
437 }
438
439 #[test]
440 fn kernel_flags_include_virtio() {
441 let builder = KernelBuilder::new(KernelArch::X86_64);
442 let flags = builder.kernel_flags();
443 assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_VIRTIO_NET != 0);
444 assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_VIRTIO_BLK != 0);
445 assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_VSOCK != 0);
446 assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_NETWORKING != 0);
447 assert!(flags & rvf_types::kernel::KERNEL_FLAG_COMPRESSED != 0);
448 }
449
450 #[test]
451 fn kernel_flags_service_detection() {
452 let builder = KernelBuilder::new(KernelArch::X86_64)
453 .with_initramfs(&["sshd", "rvf-server"]);
454 let flags = builder.kernel_flags();
455 assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_QUERY_API != 0);
456 assert!(flags & rvf_types::kernel::KERNEL_FLAG_HAS_ADMIN_API != 0);
457 }
458
459 #[test]
460 fn from_prebuilt_rejects_nonexistent() {
461 let result = KernelBuilder::from_prebuilt(Path::new("/nonexistent/bzImage"));
462 assert!(result.is_err());
463 match result.unwrap_err() {
464 KernelError::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::NotFound),
465 other => panic!("expected Io error, got: {other}"),
466 }
467 }
468
469 #[test]
470 fn from_prebuilt_rejects_too_small() {
471 let dir = tempfile::TempDir::new().unwrap();
472 let path = dir.path().join("tiny.img");
473 std::fs::write(&path, &[0u8; 100]).unwrap();
474
475 let result = KernelBuilder::from_prebuilt(&path);
476 assert!(result.is_err());
477 match result.unwrap_err() {
478 KernelError::ImageTooSmall { size, min_size } => {
479 assert_eq!(size, 100);
480 assert_eq!(min_size, 512);
481 }
482 other => panic!("expected ImageTooSmall, got: {other}"),
483 }
484 }
485
486 #[test]
487 fn from_prebuilt_reads_elf() {
488 let dir = tempfile::TempDir::new().unwrap();
489 let path = dir.path().join("kernel.elf");
490
491 let mut data = vec![0u8; 4096];
493 data[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
494 data[4] = 2; data[5] = 1; std::fs::write(&path, &data).unwrap();
497
498 let kernel = KernelBuilder::from_prebuilt(&path).unwrap();
499 assert_eq!(kernel.bzimage.len(), 4096);
500 assert_eq!(kernel.image_hash, sha3_256(&data));
501 assert!(kernel.initramfs.is_none());
502 }
503
504 #[test]
505 fn from_prebuilt_reads_bzimage() {
506 let dir = tempfile::TempDir::new().unwrap();
507 let path = dir.path().join("bzImage");
508
509 let mut data = vec![0u8; 8192];
511 data[510] = 0x55;
512 data[511] = 0xAA;
513 std::fs::write(&path, &data).unwrap();
514
515 let kernel = KernelBuilder::from_prebuilt(&path).unwrap();
516 assert_eq!(kernel.bzimage.len(), 8192);
517 assert_eq!(kernel.compressed_size, 8192);
518 }
519
520 #[test]
521 fn verifier_accepts_correct_hash() {
522 let image = b"this is a fake kernel image for testing hash verification";
524 let hash = sha3_256(image);
525
526 let header = KernelHeader {
527 kernel_magic: KERNEL_MAGIC,
528 header_version: 1,
529 arch: KernelArch::X86_64 as u8,
530 kernel_type: KernelType::MicroLinux as u8,
531 kernel_flags: 0,
532 min_memory_mb: 64,
533 entry_point: 0x0020_0000,
534 image_size: image.len() as u64,
535 compressed_size: image.len() as u64,
536 compression: 0,
537 api_transport: 0,
538 api_port: 8080,
539 api_version: 1,
540 image_hash: hash,
541 build_id: [0; 16],
542 build_timestamp: 0,
543 vcpu_count: 1,
544 reserved_0: 0,
545 cmdline_offset: 128,
546 cmdline_length: 0,
547 reserved_1: 0,
548 };
549 let header_bytes = header.to_bytes();
550
551 let verified = KernelVerifier::verify(&header_bytes, image).unwrap();
552 assert_eq!(verified.arch, KernelArch::X86_64);
553 assert_eq!(verified.kernel_type, KernelType::MicroLinux);
554 assert_eq!(verified.image_size, image.len() as u64);
555 }
556
557 #[test]
558 fn verifier_rejects_wrong_hash() {
559 let image = b"kernel image data";
560 let wrong_hash = [0xAA; 32];
561
562 let header = KernelHeader {
563 kernel_magic: KERNEL_MAGIC,
564 header_version: 1,
565 arch: KernelArch::X86_64 as u8,
566 kernel_type: KernelType::MicroLinux as u8,
567 kernel_flags: 0,
568 min_memory_mb: 64,
569 entry_point: 0,
570 image_size: image.len() as u64,
571 compressed_size: image.len() as u64,
572 compression: 0,
573 api_transport: 0,
574 api_port: 0,
575 api_version: 1,
576 image_hash: wrong_hash,
577 build_id: [0; 16],
578 build_timestamp: 0,
579 vcpu_count: 0,
580 reserved_0: 0,
581 cmdline_offset: 128,
582 cmdline_length: 0,
583 reserved_1: 0,
584 };
585 let header_bytes = header.to_bytes();
586
587 let result = KernelVerifier::verify(&header_bytes, image);
588 assert!(result.is_err());
589 match result.unwrap_err() {
590 KernelError::HashMismatch { expected, actual } => {
591 assert_eq!(expected, wrong_hash);
592 assert_eq!(actual, sha3_256(image));
593 }
594 other => panic!("expected HashMismatch, got: {other}"),
595 }
596 }
597
598 #[test]
599 fn verifier_with_binding() {
600 let image = b"kernel with binding test";
601 let hash = sha3_256(image);
602
603 let header = KernelHeader {
604 kernel_magic: KERNEL_MAGIC,
605 header_version: 1,
606 arch: KernelArch::X86_64 as u8,
607 kernel_type: KernelType::MicroLinux as u8,
608 kernel_flags: 0,
609 min_memory_mb: 64,
610 entry_point: 0,
611 image_size: image.len() as u64,
612 compressed_size: image.len() as u64,
613 compression: 0,
614 api_transport: 0,
615 api_port: 0,
616 api_version: 1,
617 image_hash: hash,
618 build_id: [0; 16],
619 build_timestamp: 0,
620 vcpu_count: 0,
621 reserved_0: 0,
622 cmdline_offset: 256,
623 cmdline_length: 0,
624 reserved_1: 0,
625 };
626 let header_bytes = header.to_bytes();
627
628 let binding = KernelBinding {
629 manifest_root_hash: [0x11; 32],
630 policy_hash: [0x22; 32],
631 binding_version: 1,
632 min_runtime_version: 0,
633 _pad0: 0,
634 allowed_segment_mask: 0,
635 _reserved: [0; 48],
636 };
637 let binding_bytes = binding.to_bytes();
638
639 let (verified, decoded_binding) =
640 KernelVerifier::verify_with_binding(&header_bytes, &binding_bytes, image).unwrap();
641 assert_eq!(verified.arch, KernelArch::X86_64);
642 assert_eq!(decoded_binding.binding_version, 1);
643 assert_eq!(decoded_binding.manifest_root_hash, [0x11; 32]);
644 }
645
646 #[test]
647 fn build_kernel_header_fills_fields() {
648 let image_data = b"test kernel data for header building";
649 let hash = sha3_256(image_data);
650
651 let kernel = BuiltKernel {
652 bzimage: image_data.to_vec(),
653 initramfs: None,
654 config: KernelConfig {
655 cmdline: "console=ttyS0 root=/dev/vda".to_string(),
656 ..Default::default()
657 },
658 image_hash: hash,
659 compressed_size: image_data.len() as u64,
660 };
661
662 let builder = KernelBuilder::new(KernelArch::X86_64)
663 .with_initramfs(&["sshd"]);
664
665 let header = build_kernel_header(&kernel, &builder, 8080);
666
667 assert_eq!(header.kernel_magic, KERNEL_MAGIC);
668 assert_eq!(header.header_version, 1);
669 assert_eq!(header.arch, KernelArch::X86_64 as u8);
670 assert_eq!(header.kernel_type, KernelType::MicroLinux as u8);
671 assert_eq!(header.image_size, image_data.len() as u64);
672 assert_eq!(header.image_hash, hash);
673 assert_eq!(header.api_port, 8080);
674 assert_eq!(header.min_memory_mb, 64);
675 assert_eq!(header.entry_point, 0x0020_0000);
676 assert_eq!(header.cmdline_length, 27); assert!(header.kernel_flags & rvf_types::kernel::KERNEL_FLAG_HAS_ADMIN_API != 0);
680 }
681
682 #[test]
683 fn build_initramfs_via_builder() {
684 let builder = KernelBuilder::new(KernelArch::X86_64);
685 let result = builder.build_initramfs(&["sshd"], &[]).unwrap();
686
687 assert_eq!(result[0], 0x1F);
689 assert_eq!(result[1], 0x8B);
690 assert!(result.len() > 100);
691 }
692
693 #[test]
694 fn error_display_formatting() {
695 let err = KernelError::ImageTooSmall {
696 size: 100,
697 min_size: 512,
698 };
699 let msg = format!("{err}");
700 assert!(msg.contains("100"));
701 assert!(msg.contains("512"));
702
703 let err2 = KernelError::HashMismatch {
704 expected: [0xAA; 32],
705 actual: [0xBB; 32],
706 };
707 let msg2 = format!("{err2}");
708 assert!(msg2.contains("aaaa"));
709 assert!(msg2.contains("bbbb"));
710 }
711
712 #[test]
713 fn kernel_config_default() {
714 let cfg = KernelConfig::default();
715 assert!(cfg.prebuilt_path.is_none());
716 assert!(cfg.docker_context.is_none());
717 assert_eq!(cfg.cmdline, "console=ttyS0 quiet");
718 assert_eq!(cfg.arch, KernelArch::X86_64);
719 assert!(!cfg.with_initramfs);
720 assert!(cfg.services.is_empty());
721 }
722}