Skip to content

Commit 198e38a

Browse files
committed
attest: add unit tests for attestation protocol
Add unit tests for the attestation protocol request handler (kernel/src/protocols/attest.rs). The SVSM attestation protocol exposes a fixed wire ABI defined by Table 11 and Table 13 of "Secure VM Service Module for SEV-SNP Guests, Rev 1.00". Any accidental field reordering or padding change would silently break the guest interface, so the layout of AttestServicesOp and AttestSingleServiceOp is now checked with compile-time const assertions (following the pattern in greq/msg.rs). Runtime tests cover: - Reserved-field validation: every reserved region in both structs must be rejected when non-zero, guarding forward-compatibility with future spec revisions. - Certificate region boundary: size==0 is valid (no extended report); size<=MAX_CERTIFICATE_SIZE is accepted; size>MAX_CERTIFICATE_SIZE is rejected. - GUID serialization: single-entry and multi-entry GuidTable wire format, including offset arithmetic between header and payload. - Protocol routing: unknown request codes must return UNSUPPORTED_PROTOCOL. Signed-off-by: Nihal <nihalxkumar@protonmail.com>
1 parent 6376e36 commit 198e38a

1 file changed

Lines changed: 229 additions & 0 deletions

File tree

kernel/src/protocols/attest.rs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,24 @@ impl AttestServicesOp {
183183
}
184184
}
185185

186+
// Compile-time ABI layout guard: any change to field order or padding that
187+
// breaks the wire format with the guest will be caught here.
188+
const _: () = assert!(
189+
core::mem::offset_of!(AttestServicesOp, report_gpa) == 0x00
190+
&& core::mem::offset_of!(AttestServicesOp, report_size) == 0x08
191+
&& core::mem::offset_of!(AttestServicesOp, reserved_1) == 0x0c
192+
&& core::mem::offset_of!(AttestServicesOp, nonce_gpa) == 0x10
193+
&& core::mem::offset_of!(AttestServicesOp, nonce_size) == 0x18
194+
&& core::mem::offset_of!(AttestServicesOp, reserved_2) == 0x1a
195+
&& core::mem::offset_of!(AttestServicesOp, manifest_gpa) == 0x20
196+
&& core::mem::offset_of!(AttestServicesOp, manifest_size) == 0x28
197+
&& core::mem::offset_of!(AttestServicesOp, reserved_3) == 0x2c
198+
&& core::mem::offset_of!(AttestServicesOp, certificate_gpa) == 0x30
199+
&& core::mem::offset_of!(AttestServicesOp, certificate_size) == 0x38
200+
&& core::mem::offset_of!(AttestServicesOp, reserved_4) == 0x3c
201+
&& core::mem::size_of::<AttestServicesOp>() == 0x40
202+
);
203+
186204
#[derive(Clone)]
187205
struct GuidTableEntry {
188206
guid: uuid::Uuid,
@@ -314,6 +332,16 @@ impl AttestSingleServiceOp {
314332
}
315333
}
316334

335+
// Compile-time ABI layout guard for the extended single-service structure
336+
// that wraps AttestServicesOp.
337+
const _: () = assert!(
338+
core::mem::offset_of!(AttestSingleServiceOp, op) == 0x00
339+
&& core::mem::offset_of!(AttestSingleServiceOp, guid) == 0x40
340+
&& core::mem::offset_of!(AttestSingleServiceOp, manifest_ver) == 0x50
341+
&& core::mem::offset_of!(AttestSingleServiceOp, reserved_5) == 0x54
342+
&& core::mem::size_of::<AttestSingleServiceOp>() == 0x58
343+
);
344+
317345
fn get_attestation_report_standard(nonce: &[u8]) -> Result<Box<SnpReportResponse>, SvsmReqError> {
318346
let mut resp = SnpReportResponse::new_box_zeroed()
319347
.map_err(|_| SvsmReqError::FatalError(SvsmError::Mem))?;
@@ -584,3 +612,204 @@ pub fn attest_protocol_request(
584612
_ => Err(SvsmReqError::unsupported_protocol()),
585613
}
586614
}
615+
616+
#[cfg(test)]
617+
mod tests {
618+
use super::*;
619+
use crate::protocols::errors::SvsmResultCode;
620+
use alloc::vec;
621+
use core::mem::offset_of;
622+
use zerocopy::FromZeros;
623+
624+
fn is_invalid_parameter(err: &SvsmReqError) -> bool {
625+
matches!(
626+
err,
627+
SvsmReqError::RequestError(SvsmResultCode::INVALID_PARAMETER)
628+
)
629+
}
630+
631+
fn is_unsupported_protocol(err: &SvsmReqError) -> bool {
632+
matches!(
633+
err,
634+
SvsmReqError::RequestError(SvsmResultCode::UNSUPPORTED_PROTOCOL)
635+
)
636+
}
637+
638+
mod attest_services {
639+
use super::*;
640+
641+
fn base_op() -> AttestServicesOp {
642+
AttestServicesOp::new_zeroed()
643+
}
644+
645+
/// The struct has four separate reserved regions. Iterate over
646+
/// all of them to ensure is_reserved_clear() inspects every one,
647+
/// not just the first.
648+
#[test]
649+
fn reserved_clear_rejects_each_field() {
650+
for offset in [
651+
offset_of!(AttestServicesOp, reserved_1),
652+
offset_of!(AttestServicesOp, reserved_2),
653+
offset_of!(AttestServicesOp, reserved_3),
654+
offset_of!(AttestServicesOp, reserved_4),
655+
] {
656+
let mut bytes = base_op().as_bytes().to_vec();
657+
bytes[offset] = 0xff;
658+
let op = AttestServicesOp::ref_from_bytes(&bytes).unwrap();
659+
assert!(
660+
!op.is_reserved_clear(),
661+
"reserved check should fail for byte at offset {offset:#x}"
662+
);
663+
}
664+
}
665+
666+
/// Parsing must reject requests with non-zero reserved fields
667+
/// to enforce forward-compatibility with future spec revisions.
668+
#[test]
669+
fn try_from_as_ref_rejects_nonzero_reserved() {
670+
for offset in [
671+
offset_of!(AttestServicesOp, reserved_1),
672+
offset_of!(AttestServicesOp, reserved_2),
673+
offset_of!(AttestServicesOp, reserved_3),
674+
offset_of!(AttestServicesOp, reserved_4),
675+
] {
676+
let mut bytes = base_op().as_bytes().to_vec();
677+
bytes[offset] = 1;
678+
let err = AttestServicesOp::try_from_as_ref(&bytes).unwrap_err();
679+
assert!(
680+
is_invalid_parameter(&err),
681+
"should reject nonzero reserved at offset {offset:#x}"
682+
);
683+
}
684+
}
685+
686+
/// The certificate region is optional (size == 0 means absent).
687+
/// When present, it must enforce MAX_CERTIFICATE_SIZE as an upper
688+
/// bound to prevent guests from requesting unbounded allocations.
689+
#[test]
690+
fn certificate_region_boundary() {
691+
let mut op = base_op();
692+
assert!(op.get_certificate_region().unwrap().is_none());
693+
694+
op.certificate_gpa = 0x4000;
695+
op.certificate_size = MAX_CERTIFICATE_SIZE as u32;
696+
assert!(op.get_certificate_region().unwrap().is_some());
697+
698+
op.certificate_size = (MAX_CERTIFICATE_SIZE + 1) as u32;
699+
let err = op.get_certificate_region().unwrap_err();
700+
assert!(is_invalid_parameter(&err));
701+
}
702+
}
703+
704+
mod attest_single_service {
705+
use super::*;
706+
707+
fn base_op() -> AttestSingleServiceOp {
708+
AttestSingleServiceOp::new_zeroed()
709+
}
710+
711+
/// Only manifest version 0 is currently defined by the protocol.
712+
/// Parsing must reject any other version to prevent silent
713+
/// misinterpretation of the manifest payload.
714+
#[test]
715+
fn try_from_as_ref_rejects_manifest_version() {
716+
let mut bytes = base_op().as_bytes().to_vec();
717+
bytes[offset_of!(AttestSingleServiceOp, manifest_ver)] = 1;
718+
let err = AttestSingleServiceOp::try_from_as_ref(&bytes).unwrap_err();
719+
assert!(is_invalid_parameter(&err));
720+
}
721+
722+
/// AttestSingleServiceOp embeds an AttestServicesOp. Parsing must
723+
/// validate the reserved fields of the inner struct too, not just
724+
/// its own reserved_5.
725+
#[test]
726+
fn try_from_as_ref_rejects_inner_reserved() {
727+
let mut bytes = base_op().as_bytes().to_vec();
728+
bytes[offset_of!(AttestServicesOp, reserved_1)] = 1;
729+
let err = AttestSingleServiceOp::try_from_as_ref(&bytes).unwrap_err();
730+
assert!(is_invalid_parameter(&err));
731+
}
732+
733+
/// GUID bytes are stored in little-endian in the wire format;
734+
/// verify get_guid() reconstructs the original UUID correctly.
735+
#[test]
736+
fn get_guid_round_trip() {
737+
let expected = uuid!("c476f1eb-0123-45a5-9641-b4e7dde5bfe3");
738+
let mut op = base_op();
739+
op.guid = expected.to_bytes_le();
740+
assert_eq!(op.get_guid(), expected);
741+
}
742+
}
743+
744+
mod guid_table {
745+
use super::*;
746+
747+
/// Verify the serialized wire format for a single-entry GuidTable:
748+
/// header GUID, total length, entry count, per-entry GUID, offset,
749+
/// size, and trailing payload must all appear at the correct byte
750+
/// positions.
751+
#[test]
752+
fn single_entry_wire_format() {
753+
let guid = uuid!("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
754+
let payload = vec![0x01, 0x02, 0x03, 0x04];
755+
756+
let mut table = GuidTable::new();
757+
table.push(guid, payload.clone());
758+
759+
let data = table.to_vec().unwrap();
760+
assert_eq!(data.len(), 48 + payload.len());
761+
762+
let entry_count = u32::from_le_bytes(data[20..24].try_into().unwrap());
763+
assert_eq!(entry_count, 1);
764+
765+
assert_eq!(&data[24..40], &guid.to_bytes_le());
766+
let entry_offset = u32::from_le_bytes(data[40..44].try_into().unwrap());
767+
assert_eq!(entry_offset, 48);
768+
let entry_size = u32::from_le_bytes(data[44..48].try_into().unwrap());
769+
assert_eq!(entry_size as usize, payload.len());
770+
771+
assert_eq!(&data[48..], &payload);
772+
}
773+
774+
/// With two entries the payload offsets must account for the larger
775+
/// header. This catches off-by-one errors in the offset arithmetic.
776+
#[test]
777+
fn multiple_entries_offset_arithmetic() {
778+
let guid1 = uuid!("11111111-1111-1111-1111-111111111111");
779+
let guid2 = uuid!("22222222-2222-2222-2222-222222222222");
780+
let payload1 = vec![0xaa; 10];
781+
let payload2 = vec![0xbb; 20];
782+
783+
let mut table = GuidTable::new();
784+
table.push(guid1, payload1.clone());
785+
table.push(guid2, payload2.clone());
786+
787+
let data = table.to_vec().unwrap();
788+
assert_eq!(data.len(), 72 + 30);
789+
790+
let entry_count = u32::from_le_bytes(data[20..24].try_into().unwrap());
791+
assert_eq!(entry_count, 2);
792+
793+
let offset1 = u32::from_le_bytes(data[40..44].try_into().unwrap());
794+
assert_eq!(offset1, 72);
795+
let offset2 = u32::from_le_bytes(data[64..68].try_into().unwrap());
796+
assert_eq!(offset2, 82);
797+
798+
assert_eq!(&data[72..82], &payload1);
799+
assert_eq!(&data[82..102], &payload2);
800+
}
801+
}
802+
803+
mod protocol_routing {
804+
use super::*;
805+
806+
/// The protocol handler must reject unknown request codes with
807+
/// UNSUPPORTED_PROTOCOL to prevent silent misrouting.
808+
#[test]
809+
fn rejects_unknown_request() {
810+
let mut params = RequestParams::default();
811+
let err = attest_protocol_request(99, &mut params).unwrap_err();
812+
assert!(is_unsupported_protocol(&err));
813+
}
814+
}
815+
}

0 commit comments

Comments
 (0)