[PATCH v5 19/23] lib: rspdm: Support SPDM get_certificate

From: alistair23

Date: Tue Oct 27 2026 - 01:32:31 EST


From: Alistair Francis <alistair.francis@xxxxxxx>

Support the GET_CERTIFICATE SPDM command.

The kernel will send a GET_CERTIFICATE request to the the responder and
then iterate over all of the certificates returned.

Certificate validation happens in the next commit.

Signed-off-by: Alistair Francis <alistair.francis@xxxxxxx>
---
lib/rspdm/consts.rs | 2 +
lib/rspdm/lib.rs | 15 ++++
lib/rspdm/state.rs | 100 ++++++++++++++++++++++++-
lib/rspdm/validator.rs | 162 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 278 insertions(+), 1 deletion(-)

diff --git a/lib/rspdm/consts.rs b/lib/rspdm/consts.rs
index 68f347a046859..964ae2a21c6da 100644
--- a/lib/rspdm/consts.rs
+++ b/lib/rspdm/consts.rs
@@ -75,6 +75,8 @@

pub(crate) const SPDM_GET_DIGESTS: u8 = 0x81;

+pub(crate) const SPDM_GET_CERTIFICATE: u8 = 0x82;
+
// If the crypto support isn't enabled don't offer the algorithms
// to the responder
#[cfg(CONFIG_CRYPTO_RSA)]
diff --git a/lib/rspdm/lib.rs b/lib/rspdm/lib.rs
index ee7e9be5b41c1..6b0be0a0303fa 100644
--- a/lib/rspdm/lib.rs
+++ b/lib/rspdm/lib.rs
@@ -274,6 +274,21 @@ pub extern "C" fn spdm_authenticate(state_ptr: *mut spdm_state) -> c_int {
return e.to_errno() as c_int;
}

+ if state.provisioned_slots == 0 {
+ return -(bindings::EIO as c_int);
+ }
+
+ let mut provisioned_slots = state.provisioned_slots;
+ while (provisioned_slots as usize) > 0 {
+ let slot = provisioned_slots.trailing_zeros() as u8;
+
+ if let Err(e) = state.get_certificate(slot) {
+ return e.to_errno() as c_int;
+ }
+
+ provisioned_slots &= !(1 << slot);
+ }
+
-(EPROTONOSUPPORT as i32)
}

diff --git a/lib/rspdm/state.rs b/lib/rspdm/state.rs
index 73c39847d7335..7799d4ac70c3f 100644
--- a/lib/rspdm/state.rs
+++ b/lib/rspdm/state.rs
@@ -50,16 +50,21 @@
use crate::validator::{
GetCapabilitiesReq,
GetCapabilitiesRsp,
+ GetCertificateReq,
+ GetCertificateRsp,
GetDigestsReq,
GetDigestsRsp,
GetVersionReq,
GetVersionRsp,
NegotiateAlgsReq,
NegotiateAlgsRsp,
+ SpdmCertChain,
SpdmErrorRsp,
SpdmHeader,
GET_CAPABILITIES_RSP_SZ,
- NEGOTIATE_ALGS_RSP_SZ, //
+ GET_CERTIFICATE_RSP_HDR_SZ,
+ NEGOTIATE_ALGS_RSP_SZ,
+ SPDM_CERT_CHAIN_HDR_SZ, //
};

/// Transport for exchanging SPDM messages with a responder device.
@@ -453,4 +458,97 @@ pub(crate) fn get_digests(&mut self) -> Result<(), Error> {

Ok(())
}
+
+ pub(crate) fn get_certificate(&mut self, slot: u8) -> Result<(), Error> {
+ let mut request = GetCertificateReq::default();
+ request.header.version = self.version;
+ request.header.param1 = slot;
+
+ let rsp_sz = (GET_CERTIFICATE_RSP_HDR_SZ as u32 + u32::from(u16::MAX))
+ .min(self.transport_sz) as usize;
+
+ request.offset = 0;
+ request.length = (rsp_sz - GET_CERTIFICATE_RSP_HDR_SZ) as u16;
+
+ let response_vec: KVec<u8> = KVec::from_elem(0u8, rsp_sz, GFP_KERNEL)?;
+
+ let mut request_buf = request.to_bytes()?;
+ let response: GetCertificateRsp = self
+ .spdm_exchange(request_buf.as_mut_slice(), response_vec)?
+ .validate(&*self)?;
+
+ if (response.header.param1 & 0xF) != slot {
+ pr_err!("Invalid slot response\n");
+ return Err(EPROTO);
+ }
+
+ let portion_length = response.portion_length;
+ let rem_length = response.remainder_length;
+
+ let total_cert_len = portion_length as usize + rem_length as usize;
+
+ let mut certs_buf: KVec<u8> = KVec::new();
+
+ certs_buf.extend_from_slice(&response.cert_chain, GFP_KERNEL)?;
+
+ let mut offset: u16 = portion_length;
+ let mut remainder_length = rem_length as usize;
+
+ while remainder_length > 0 {
+ request.offset = offset;
+ request.length = (remainder_length.min(rsp_sz - GET_CERTIFICATE_RSP_HDR_SZ)) as u16;
+
+ let mut request_buf = request.to_bytes()?;
+
+ let response_vec: KVec<u8> = KVec::from_elem(
+ 0u8,
+ request.length as usize + GET_CERTIFICATE_RSP_HDR_SZ,
+ GFP_KERNEL,
+ )?;
+
+ let response: GetCertificateRsp = self
+ .spdm_exchange(request_buf.as_mut_slice(), response_vec)?
+ .validate(&*self)?;
+
+ let portion_length = response.portion_length;
+ let rem_length = response.remainder_length;
+
+ if portion_length == 0
+ || (response.header.param1 & 0xF) != slot
+ || offset as usize + portion_length as usize + rem_length as usize != total_cert_len
+ {
+ pr_err!("Malformed certificate response\n");
+ return Err(EPROTO);
+ }
+
+ certs_buf.extend_from_slice(&response.cert_chain, GFP_KERNEL)?;
+ let (val, overflow) = offset.overflowing_add(portion_length);
+ if overflow {
+ pr_err!("portion_length response overflowed\n");
+ return Err(EPROTO);
+ }
+ offset = val;
+ remainder_length = rem_length as usize;
+ }
+
+ let header_length = SPDM_CERT_CHAIN_HDR_SZ + self.hash_len();
+
+ if total_cert_len < header_length || total_cert_len != certs_buf.len() {
+ pr_err!("Malformed certificate chain in slot {slot}\n");
+ return Err(EPROTO);
+ }
+
+ let certs: SpdmCertChain = Untrusted::new(certs_buf.as_slice()).validate(&*self)?;
+ let cert_chain_length = certs.length as usize;
+
+ if total_cert_len != cert_chain_length {
+ pr_err!("Malformed certificate chain in slot {slot}\n");
+ return Err(EPROTO);
+ }
+
+ self.certs[slot as usize].clear();
+ self.certs[slot as usize].extend_from_slice(&certs_buf, GFP_KERNEL)?;
+
+ Ok(())
+ }
}
diff --git a/lib/rspdm/validator.rs b/lib/rspdm/validator.rs
index f7ad46aef1b88..469a5722368f2 100644
--- a/lib/rspdm/validator.rs
+++ b/lib/rspdm/validator.rs
@@ -12,6 +12,7 @@
SPDM_CAP_SUPPORTED_ALGORITHMS,
SPDM_CTEXPONENT,
SPDM_GET_CAPABILITIES,
+ SPDM_GET_CERTIFICATE,
SPDM_GET_DIGESTS,
SPDM_GET_VERSION,
SPDM_HASH_ALGOS,
@@ -819,3 +820,164 @@ fn validate(unvalidated: KVec<u8>, context: &'c SpdmState<'c, T>) -> Result<Self
})
}
}
+
+pub(crate) struct GetCertificateReq {
+ pub(crate) header: SpdmHeader,
+
+ pub(crate) offset: u16,
+ pub(crate) length: u16,
+}
+
+impl GetCertificateReq {
+ pub(crate) fn to_bytes(&self) -> Result<KVec<u8>> {
+ let mut out = self.header.to_bytes()?;
+
+ out.extend_from_slice(&self.offset.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&self.length.to_le_bytes(), GFP_KERNEL)?;
+
+ Ok(out)
+ }
+}
+
+impl Default for GetCertificateReq {
+ fn default() -> Self {
+ GetCertificateReq {
+ header: SpdmHeader::new(SPDM_GET_CERTIFICATE),
+
+ offset: 0,
+ length: 0,
+ }
+ }
+}
+
+pub(crate) const GET_CERTIFICATE_RSP_HDR_SZ: usize = mem::size_of::<SpdmHeader>() + 4;
+
+pub(crate) const SPDM_CERT_CHAIN_HDR_SZ: usize = mem::size_of::<u16>() + 2;
+
+/// A parsed SPDM certificate chain
+#[expect(dead_code)]
+pub(crate) struct SpdmCertChain {
+ // `length` is a u16 (with 2 bytes reserved) for SPDM versions 1.3
+ // and lower and u32 for 1.4. We don't currently support `LargeOffset`
+ // and `LargeLength`, so let's pretend this is always a u16
+ pub(crate) length: u16,
+
+ pub(crate) root_hash: KVec<u8>,
+
+ pub(crate) certificates: KVec<u8>,
+}
+
+impl<'a, 'c, T: Transport> Validate<'c, Untrusted<&'a [u8]>, &'c SpdmState<'c, T>>
+ for SpdmCertChain
+{
+ type Err = Error;
+
+ fn validate(unvalidated: &[u8], context: &'c SpdmState<'c, T>) -> Result<Self, Self::Err> {
+ let length = u16::from_le_bytes(
+ unvalidated
+ .get(0..2)
+ .ok_or(EPROTO)?
+ .try_into()
+ .map_err(|_| EPROTO)?,
+ );
+
+ let root_hash_end = SPDM_CERT_CHAIN_HDR_SZ + context.hash_len();
+ let mut root_hash = KVec::new();
+ root_hash.extend_from_slice(
+ unvalidated
+ .get(SPDM_CERT_CHAIN_HDR_SZ..root_hash_end)
+ .ok_or(EPROTO)?,
+ GFP_KERNEL,
+ )?;
+
+ let cert_chain_end = length as usize;
+ let mut certificates = KVec::new();
+ certificates.extend_from_slice(
+ unvalidated
+ .get(root_hash_end..cert_chain_end)
+ .ok_or(EPROTO)?,
+ GFP_KERNEL,
+ )?;
+
+ Ok(SpdmCertChain {
+ length,
+ root_hash,
+ certificates,
+ })
+ }
+}
+
+pub(crate) struct GetCertificateRsp {
+ pub(crate) header: SpdmHeader,
+
+ pub(crate) portion_length: u16,
+ pub(crate) remainder_length: u16,
+
+ pub(crate) cert_chain: KVec<u8>,
+
+ /// The actual length of the SPDM message. `raw_bytes` might be longer
+ /// as it includes transport specific padding.
+ length: usize,
+
+ /// Entire response, used for the transcript hash.
+ raw_bytes: KVec<u8>,
+}
+
+impl GetCertificateRsp {
+ #[expect(dead_code)]
+ pub(crate) fn as_bytes(&self) -> Result<&[u8], Error> {
+ self.raw_bytes.get(..self.length).ok_or(EPROTO)
+ }
+}
+
+impl<'c, T: Transport> Validate<'c, Untrusted<KVec<u8>>, &'c SpdmState<'c, T>>
+ for GetCertificateRsp
+{
+ type Err = Error;
+
+ fn validate(unvalidated: KVec<u8>, context: &'c SpdmState<'c, T>) -> Result<Self, Self::Err> {
+ let header: SpdmHeader =
+ Untrusted::new(unvalidated.get(0..4).ok_or(EPROTO)?).validate(context)?;
+
+ if header.code != SPDM_GET_CERTIFICATE - 0x80 {
+ return Err(EPROTO);
+ }
+
+ if header.version != context.version {
+ pr_err!("Invalid version response\n");
+ return Err(EPROTO);
+ }
+
+ let portion_length = u16::from_le_bytes(
+ unvalidated
+ .get(4..6)
+ .ok_or(EPROTO)?
+ .try_into()
+ .map_err(|_| EPROTO)?,
+ );
+ let remainder_length = u16::from_le_bytes(
+ unvalidated
+ .get(6..8)
+ .ok_or(EPROTO)?
+ .try_into()
+ .map_err(|_| EPROTO)?,
+ );
+
+ let cert_chain_end = 8 + portion_length as usize;
+
+ let mut cert_chain = KVec::new();
+ cert_chain.extend_from_slice(
+ unvalidated.get(8..cert_chain_end).ok_or(EPROTO)?,
+ GFP_KERNEL,
+ )?;
+
+ Ok(GetCertificateRsp {
+ header,
+ portion_length,
+ remainder_length,
+ cert_chain,
+ length: cert_chain_end,
+ raw_bytes: unvalidated,
+ })
+ }
+}
--
2.56.0