* [PATCH v5 01/23] virt: coco: change tsm_register() to use a const struct
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:25 ` [PATCH v5 02/23] rust: create basic untrusted data API alistair23
` (21 subsequent siblings)
22 siblings, 0 replies; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Alistair Francis <alistair.francis@wdc.com>
The tsm_register() function takes a struct pci_tsm_ops *ops argument.
This eventually is stored in struct tsm_dev as a const, so we should
just change the tsm_register() register function argument to be a const.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
drivers/crypto/ccp/sev-dev-tsm.c | 2 +-
drivers/virt/coco/tsm-core.c | 4 ++--
include/linux/tsm.h | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/drivers/crypto/ccp/sev-dev-tsm.c b/drivers/crypto/ccp/sev-dev-tsm.c
index 46f2539d2d5ab..48df8c579c74d 100644
--- a/drivers/crypto/ccp/sev-dev-tsm.c
+++ b/drivers/crypto/ccp/sev-dev-tsm.c
@@ -339,7 +339,7 @@ static void dsm_disconnect(struct pci_dev *pdev)
streams_teardown(dev_data->ide);
}
-static struct pci_tsm_ops sev_tsm_ops = {
+static const struct pci_tsm_ops sev_tsm_ops = {
.probe = dsm_probe,
.remove = dsm_remove,
.connect = dsm_connect,
diff --git a/drivers/virt/coco/tsm-core.c b/drivers/virt/coco/tsm-core.c
index e784993353d8f..6b735ac2a814e 100644
--- a/drivers/virt/coco/tsm-core.c
+++ b/drivers/virt/coco/tsm-core.c
@@ -57,7 +57,7 @@ static struct tsm_dev *alloc_tsm_dev(struct device *parent)
}
static struct tsm_dev *tsm_register_pci_or_reset(struct tsm_dev *tsm_dev,
- struct pci_tsm_ops *pci_ops)
+ const struct pci_tsm_ops *pci_ops)
{
int rc;
@@ -78,7 +78,7 @@ static struct tsm_dev *tsm_register_pci_or_reset(struct tsm_dev *tsm_dev,
return tsm_dev;
}
-struct tsm_dev *tsm_register(struct device *parent, struct pci_tsm_ops *pci_ops)
+struct tsm_dev *tsm_register(struct device *parent, const struct pci_tsm_ops *pci_ops)
{
struct tsm_dev *tsm_dev __free(put_tsm_dev) = alloc_tsm_dev(parent);
struct device *dev;
diff --git a/include/linux/tsm.h b/include/linux/tsm.h
index 381c53244c835..6ac12de8c4d36 100644
--- a/include/linux/tsm.h
+++ b/include/linux/tsm.h
@@ -120,7 +120,7 @@ DEFINE_FREE(put_tsm_dev, struct tsm_dev *,
int tsm_report_register(const struct tsm_report_ops *ops, void *priv);
int tsm_report_unregister(const struct tsm_report_ops *ops);
-struct tsm_dev *tsm_register(struct device *parent, struct pci_tsm_ops *ops);
+struct tsm_dev *tsm_register(struct device *parent, const struct pci_tsm_ops *ops);
void tsm_unregister(struct tsm_dev *tsm_dev);
struct tsm_dev *find_tsm_dev(int id);
#endif /* __TSM_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* [PATCH v5 02/23] rust: create basic untrusted data API
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
2026-10-27 5:25 ` [PATCH v5 01/23] virt: coco: change tsm_register() to use a const struct alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:34 ` sashiko-bot
2026-10-27 5:25 ` [PATCH v5 03/23] rust: validate: add `Validate` trait alistair23
` (20 subsequent siblings)
22 siblings, 1 reply; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Benno Lossin <benno.lossin@proton.me>
When the kernel receives external data (e.g. from userspace), it usually
is a very bad idea to directly use the data for logic decision in the
kernel. For this reason, such data should be explicitly marked and
validated before making decision based on its value.
The `Untrusted<T>` wrapper type marks a value of type `T` as untrusted.
The particular meaning of "untrusted" highly depends on the type `T`.
For example `T = u8` ensures that the value of the byte cannot be
retrieved. However, `T = [u8]` still allows to access the length of the
slice. Similarly, `T = KVec<U>` allows modifications.
Signed-off-by: Benno Lossin <benno.lossin@proton.me>
Message-ID: <20250814124424.516191-3-lossin@kernel.org>
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
rust/kernel/lib.rs | 1 +
rust/kernel/validate.rs | 192 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 193 insertions(+)
create mode 100644 rust/kernel/validate.rs
diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
index 1ab41129d557a..a3c54971e9dea 100644
--- a/rust/kernel/lib.rs
+++ b/rust/kernel/lib.rs
@@ -147,6 +147,7 @@
pub mod uaccess;
#[cfg(CONFIG_USB = "y")]
pub mod usb;
+pub mod validate;
pub mod workqueue;
pub mod xarray;
diff --git a/rust/kernel/validate.rs b/rust/kernel/validate.rs
new file mode 100644
index 0000000000000..3c1fe37624573
--- /dev/null
+++ b/rust/kernel/validate.rs
@@ -0,0 +1,192 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Untrusted data API.
+//!
+//! # Overview
+//!
+//! Untrusted data is marked using the [`Untrusted<T>`] type. See [Rationale](#rationale) for the
+//! reasons to mark untrusted data throughout the kernel. It is a totally opaque wrapper, it is not
+//! possible to read the data inside.
+//!
+//! APIs that write back into userspace usually allow writing untrusted bytes directly, allowing
+//! direct copying of untrusted user data back into userspace without validation.
+//!
+//! # Rationale
+//!
+//! When reading data from an untrusted source, it must be validated before it can be used for
+//! **logic**. For example, this is a very bad idea:
+//!
+//! ```
+//! # fn read_bytes_from_network() -> KBox<[u8]> {
+//! # Box::new([1, 0], kernel::alloc::flags::GFP_KERNEL).unwrap()
+//! # }
+//! let bytes: KBox<[u8]> = read_bytes_from_network();
+//! let data_index = bytes[0];
+//! let data = bytes[usize::from(data_index)];
+//! ```
+//!
+//! While this will not lead to a memory violation (because the array index checks the bounds), it
+//! might result in a kernel panic. For this reason, all untrusted data must be wrapped in
+//! [`Untrusted<T>`]. This type only allows validating the data or passing it along, since copying
+//! data from userspace back into userspace is allowed for untrusted data.
+
+use core::{
+ ops::{
+ Deref,
+ DerefMut, //
+ },
+ ptr::{
+ from_mut,
+ from_ref, //
+ },
+ slice, //
+};
+
+use crate::alloc::{
+ Allocator,
+ Vec, //
+};
+
+/// Untrusted data of type `T`.
+///
+/// Data coming from userspace is considered untrusted and should be marked by this type.
+///
+/// The particular meaning of [`Untrusted<T>`] depends heavily on the type `T`. For example,
+/// `&Untrusted<[u8]>` is a reference to an untrusted slice. But the length is not considered
+/// untrusted, as it would otherwise violate normal Rust rules. For this reason, one can easily
+/// convert that reference to `&[Untrusted<u8>]`. Another such example is `Untrusted<KVec<T>>`, it
+/// derefs to `[Untrusted<T>]`. Raw bytes however do not behave in this way, `Untrusted<u8>` is
+/// totally opaque.
+///
+/// # Usage in API Design
+///
+/// The exact location where to put [`Untrusted`] depends on the kind of API. When asking for an
+/// untrusted input value, or buffer to write to, always move the [`Untrusted`] wrapper as far
+/// inwards as possible:
+///
+/// ```ignore
+/// // use this
+/// pub fn read_from_userspace(buf: &mut [Untrusted<u8>]) { todo!() }
+///
+/// // and not this
+/// pub fn read_from_userspace(buf: &mut Untrusted<[u8]>) { todo!() }
+/// ```
+///
+/// The reason for this is that `&mut Untrusted<[u8]>` can be converted into `&mut [Untrusted<u8>]`
+/// very easily, but the converse is not possible.
+///
+/// For the same reason, when returning untrusted data by-value, one should move the [`Untrusted`]
+/// wrapper as far outward as possible:
+///
+/// ```ignore
+/// // use this
+/// pub fn read_all_from_userspace() -> Untrusted<KVec<u8>> { todo!() }
+///
+/// // and not this
+/// pub fn read_all_from_userspace() -> KVec<Untrusted<u8>> { todo!() }
+/// ```
+///
+/// Here too the reason is that `KVec<Untrusted<u8>>` is more restrictive compared to
+/// `Untrusted<KVec<u8>>`.
+#[repr(transparent)]
+pub struct Untrusted<T: ?Sized>(T);
+
+impl<T> From<T> for Untrusted<T> {
+ #[inline]
+ fn from(value: T) -> Self {
+ Self(value)
+ }
+}
+
+impl<T: ?Sized> Untrusted<T> {
+ /// Marks the given value as untrusted.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use kernel::validate::Untrusted;
+ ///
+ /// # mod bindings { pub(crate) unsafe fn read_foo_info() -> [u8; 4] { todo!() } };
+ /// fn read_foo_info() -> Untrusted<[u8; 4]> {
+ /// // SAFETY: just an FFI call without preconditions.
+ /// Untrusted::new(unsafe { bindings::read_foo_info() })
+ /// }
+ /// ```
+ #[inline]
+ pub fn new(value: T) -> Self
+ where
+ T: Sized,
+ {
+ Self::from(value)
+ }
+}
+
+impl<T> Untrusted<T> {
+ /// Marks the data behind the given reference as untrusted.
+ ///
+ /// Unlike [`Untrusted::new`], this does not require moving the value.
+ #[inline]
+ pub const fn from_ref(value: &T) -> &Self {
+ // SAFETY: `Untrusted<T>` transparently wraps `T`. The cast
+ // does not introduce interior mutability, as `Untrusted<T>`
+ // has interior mutability if and only if `T` has.
+ unsafe { &*from_ref(value).cast::<Self>() }
+ }
+
+ /// Marks the data behind the given mutable reference as untrusted.
+ ///
+ /// Unlike [`Untrusted::new`], this does not require moving the value.
+ #[inline]
+ pub const fn from_mut(value: &mut T) -> &mut Self {
+ // SAFETY: Same reasoning as `from_ref()`.
+ unsafe { &mut *from_mut(value).cast::<Self>() }
+ }
+
+ /// Marks the given slice of data as untrusted.
+ #[inline]
+ pub const fn from_slice(value: &[T]) -> &[Self] {
+ // SAFETY: `Untrusted<T>` transparently wraps `T`. The cast
+ // does not introduce interior mutability either, as `Untrusted<T>`
+ // has interior mutability if and only if `T` has.
+ unsafe { slice::from_raw_parts(value.as_ptr().cast::<Self>(), value.len()) }
+ }
+
+ /// Marks the given mutable slice of data as untrusted.
+ #[inline]
+ pub const fn from_mut_slice(value: &mut [T]) -> &mut [Self] {
+ // SAFETY: Same reasoning as `from_slice()`.
+ unsafe { slice::from_raw_parts_mut(value.as_mut_ptr().cast::<Self>(), value.len()) }
+ }
+}
+
+impl<T> Deref for Untrusted<[T]> {
+ type Target = [Untrusted<T>];
+
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ Untrusted::from_slice(&self.0)
+ }
+}
+
+impl<T> DerefMut for Untrusted<[T]> {
+ #[inline]
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ Untrusted::from_mut_slice(&mut self.0)
+ }
+}
+
+impl<T, A: Allocator> Deref for Untrusted<Vec<T, A>> {
+ type Target = [Untrusted<T>];
+
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ Untrusted::from_slice(self.0.as_slice())
+ }
+}
+
+impl<T, A: Allocator> DerefMut for Untrusted<Vec<T, A>> {
+ #[inline]
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ Untrusted::from_mut_slice(self.0.as_mut_slice())
+ }
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* Re: [PATCH v5 02/23] rust: create basic untrusted data API
2026-10-27 5:25 ` [PATCH v5 02/23] rust: create basic untrusted data API alistair23
@ 2026-10-27 5:34 ` sashiko-bot
2026-10-27 13:17 ` Benno Lossin
0 siblings, 1 reply; 37+ messages in thread
From: sashiko-bot @ 2026-10-27 5:34 UTC (permalink / raw)
To: alistair23, linux-pci, linux-cxl; +Cc: ojeda, gary
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] rust: validate: from_mut() and from_mut_slice() bypass Untrusted opacity
--
commit 46cfb0c607f570933dd68037f09805a7bea851ee
Author: Benno Lossin <lossin@kernel.org>
rust: create basic untrusted data API
This commit introduces the Untrusted<T> wrapper type in kernel::validate to
explicitly mark external data as untrusted and prevent unvalidated data from
being directly read or used for kernel logic decisions.
> diff --git a/rust/kernel/validate.rs b/rust/kernel/validate.rs
> new file mode 100644
> index 0000000000000..3c1fe37624573
> --- /dev/null
> +++ b/rust/kernel/validate.rs
> @@ -0,0 +1,192 @@
[ ... ]
> +impl<T> Untrusted<T> {
> + /// Marks the data behind the given reference as untrusted.
> + ///
> + /// Unlike [`Untrusted::new`], this does not require moving the value.
> + #[inline]
> + pub const fn from_ref(value: &T) -> &Self {
> + // SAFETY: `Untrusted<T>` transparently wraps `T`. The cast
> + // does not introduce interior mutability, as `Untrusted<T>`
> + // has interior mutability if and only if `T` has.
> + unsafe { &*from_ref(value).cast::<Self>() }
> + }
> +
> + /// Marks the data behind the given mutable reference as untrusted.
> + ///
> + /// Unlike [`Untrusted::new`], this does not require moving the value.
> + #[inline]
> + pub const fn from_mut(value: &mut T) -> &mut Self {
> + // SAFETY: Same reasoning as `from_ref()`.
> + unsafe { &mut *from_mut(value).cast::<Self>() }
> + }
> +
> + /// Marks the given slice of data as untrusted.
> + #[inline]
> + pub const fn from_slice(value: &[T]) -> &[Self] {
> + // SAFETY: `Untrusted<T>` transparently wraps `T`. The cast
> + // does not introduce interior mutability either, as `Untrusted<T>`
> + // has interior mutability if and only if `T` has.
> + unsafe { slice::from_raw_parts(value.as_ptr().cast::<Self>(), value.len()) }
> + }
> +
> + /// Marks the given mutable slice of data as untrusted.
> + #[inline]
> + pub const fn from_mut_slice(value: &mut [T]) -> &mut [Self] {
> + // SAFETY: Same reasoning as `from_slice()`.
> + unsafe { slice::from_raw_parts_mut(value.as_mut_ptr().cast::<Self>(), value.len()) }
> + }
> +}
[Severity: Medium]
Do public Untrusted::from_mut() and Untrusted::from_mut_slice() allow safe
code to bypass the opacity guarantee of Untrusted<T>?
Because mutable references are invariant in T, converting &mut T into
&mut Untrusted<T> (or &mut [T] into &mut [Untrusted<T>]) allows callers to
write unvalidated Untrusted<T> values directly into a plain, trusted T or [T]
variable without validation:
let mut trusted: u8 = 0;
*Untrusted::from_mut(&mut trusted) = untrusted_byte;
Similarly, a caller with a plain mutable slice buf: &mut [u8] can pass
Untrusted::from_mut_slice(buf) to a function expecting &mut [Untrusted<u8>]
(such as the read_from_userspace(buf: &mut [Untrusted<u8>]) pattern shown in
the documentation) and then read the unvalidated bytes directly from buf
after the call returns.
Since DerefMut on Untrusted<[T]> and Untrusted<Vec<T, A>> only converts an
already-untrusted container into &mut [Untrusted<T>], should from_mut_slice()
be private and from_mut() be removed or kept private?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027052536.1722081-1-alistair.francis@wdc.com?part=2
^ permalink raw reply [flat|nested] 37+ messages in thread* Re: [PATCH v5 02/23] rust: create basic untrusted data API
2026-10-27 5:34 ` sashiko-bot
@ 2026-10-27 13:17 ` Benno Lossin
0 siblings, 0 replies; 37+ messages in thread
From: Benno Lossin @ 2026-10-27 13:17 UTC (permalink / raw)
To: sashiko-reviews, alistair23, linux-pci, linux-cxl; +Cc: ojeda, gary
On Tue Oct 27, 2026 at 5:34 AM GMT, sashiko-bot wrote:
>> +impl<T> Untrusted<T> {
>> + /// Marks the data behind the given reference as untrusted.
>> + ///
>> + /// Unlike [`Untrusted::new`], this does not require moving the value.
>> + #[inline]
>> + pub const fn from_ref(value: &T) -> &Self {
>> + // SAFETY: `Untrusted<T>` transparently wraps `T`. The cast
>> + // does not introduce interior mutability, as `Untrusted<T>`
>> + // has interior mutability if and only if `T` has.
>> + unsafe { &*from_ref(value).cast::<Self>() }
>> + }
>> +
>> + /// Marks the data behind the given mutable reference as untrusted.
>> + ///
>> + /// Unlike [`Untrusted::new`], this does not require moving the value.
>> + #[inline]
>> + pub const fn from_mut(value: &mut T) -> &mut Self {
>> + // SAFETY: Same reasoning as `from_ref()`.
>> + unsafe { &mut *from_mut(value).cast::<Self>() }
>> + }
>> +
>> + /// Marks the given slice of data as untrusted.
>> + #[inline]
>> + pub const fn from_slice(value: &[T]) -> &[Self] {
>> + // SAFETY: `Untrusted<T>` transparently wraps `T`. The cast
>> + // does not introduce interior mutability either, as `Untrusted<T>`
>> + // has interior mutability if and only if `T` has.
>> + unsafe { slice::from_raw_parts(value.as_ptr().cast::<Self>(), value.len()) }
>> + }
>> +
>> + /// Marks the given mutable slice of data as untrusted.
>> + #[inline]
>> + pub const fn from_mut_slice(value: &mut [T]) -> &mut [Self] {
>> + // SAFETY: Same reasoning as `from_slice()`.
>> + unsafe { slice::from_raw_parts_mut(value.as_mut_ptr().cast::<Self>(), value.len()) }
>> + }
>> +}
>
> [Severity: Medium]
> Do public Untrusted::from_mut() and Untrusted::from_mut_slice() allow safe
> code to bypass the opacity guarantee of Untrusted<T>?
>
> Because mutable references are invariant in T, converting &mut T into
> &mut Untrusted<T> (or &mut [T] into &mut [Untrusted<T>]) allows callers to
> write unvalidated Untrusted<T> values directly into a plain, trusted T or [T]
> variable without validation:
>
> let mut trusted: u8 = 0;
> *Untrusted::from_mut(&mut trusted) = untrusted_byte;
>
> Similarly, a caller with a plain mutable slice buf: &mut [u8] can pass
> Untrusted::from_mut_slice(buf) to a function expecting &mut [Untrusted<u8>]
> (such as the read_from_userspace(buf: &mut [Untrusted<u8>]) pattern shown in
> the documentation) and then read the unvalidated bytes directly from buf
> after the call returns.
>
> Since DerefMut on Untrusted<[T]> and Untrusted<Vec<T, A>> only converts an
> already-untrusted container into &mut [Untrusted<T>], should from_mut_slice()
> be private and from_mut() be removed or kept private?
this is correct. we should remove `from_mut_slice`. but we can provide
`Vec::from_trusted: fn(Vec<T>) -> Vec<Untrusted<T>>`.
best,
Benno
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH v5 03/23] rust: validate: add `Validate` trait
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
2026-10-27 5:25 ` [PATCH v5 01/23] virt: coco: change tsm_register() to use a const struct alistair23
2026-10-27 5:25 ` [PATCH v5 02/23] rust: create basic untrusted data API alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:25 ` [PATCH v5 04/23] X.509: Make certificate parser public alistair23
` (19 subsequent siblings)
22 siblings, 0 replies; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Benno Lossin <benno.lossin@proton.me>
Introduce the `Validate<Input>` trait and functions to validate
`Untrusted<T>` using said trait. This allows one to access the inner
value of `Untrusted<T>` via `validate{,_ref,_mut}` functions which
subsequently delegate the validation to user-implemented `Validate`
trait.
The `Validate` trait is the only entry point for validation code, making
it easy to spot where data is being validated.
The reason for restricting the types that can be inputs to
`Validate::validate` is to be able to have the `validate...` functions
on `Untrusted`. This is also the reason for the suggestions in the
`Usage in API Design` section in the commit that introduced
`Untrusted<T>`.
The `Validate::validate()` function allows additional context to be
passed, which can be used as part of the validation process. For example
comparing data against already negotiated information.
Signed-off-by: Benno Lossin <benno.lossin@proton.me>
Message-ID: <20250814124424.516191-4-lossin@kernel.org>
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
rust/kernel/validate.rs | 85 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 84 insertions(+), 1 deletion(-)
diff --git a/rust/kernel/validate.rs b/rust/kernel/validate.rs
index 3c1fe37624573..3d1c4107fa8fe 100644
--- a/rust/kernel/validate.rs
+++ b/rust/kernel/validate.rs
@@ -11,6 +11,9 @@
//! APIs that write back into userspace usually allow writing untrusted bytes directly, allowing
//! direct copying of untrusted user data back into userspace without validation.
//!
+//! The only way to access untrusted data is to [`Validate::validate`] it. This is facilitated by
+//! the [`Validate`] trait.
+//!
//! # Rationale
//!
//! When reading data from an untrusted source, it must be validated before it can be used for
@@ -56,7 +59,7 @@
/// untrusted, as it would otherwise violate normal Rust rules. For this reason, one can easily
/// convert that reference to `&[Untrusted<u8>]`. Another such example is `Untrusted<KVec<T>>`, it
/// derefs to `[Untrusted<T>]`. Raw bytes however do not behave in this way, `Untrusted<u8>` is
-/// totally opaque.
+/// totally opaque and one can only access its value by calling [`Untrusted::validate()`].
///
/// # Usage in API Design
///
@@ -119,6 +122,42 @@ pub fn new(value: T) -> Self
{
Self::from(value)
}
+
+ /// Validate the underlying untrusted data.
+ ///
+ /// See the [`Validate`] trait for more information.
+ #[inline]
+ pub fn validate<'c, Context, V: Validate<'c, Self, Context>>(
+ self,
+ context: Context,
+ ) -> Result<V, V::Err>
+ where
+ T: Sized,
+ {
+ V::validate(self.0, context)
+ }
+
+ /// Validate the underlying untrusted data.
+ ///
+ /// See the [`Validate`] trait for more information.
+ #[inline]
+ pub fn validate_ref<'a, 'c, Context, V: Validate<'c, &'a Self, Context>>(
+ &'a self,
+ context: Context,
+ ) -> Result<V, V::Err> {
+ V::validate(&self.0, context)
+ }
+
+ /// Validate the underlying untrusted data.
+ ///
+ /// See the [`Validate`] trait for more information.
+ #[inline]
+ pub fn validate_mut<'a, 'c, Context, V: Validate<'c, &'a mut Self, Context>>(
+ &'a mut self,
+ context: Context,
+ ) -> Result<V, V::Err> {
+ V::validate(&mut self.0, context)
+ }
}
impl<T> Untrusted<T> {
@@ -190,3 +229,47 @@ fn deref_mut(&mut self) -> &mut Self::Target {
Untrusted::from_mut_slice(self.0.as_mut_slice())
}
}
+
+/// Marks valid input for the [`Validate`] trait.
+pub trait ValidateInput: private::Sealed {
+ /// Type of the inner data.
+ type Inner: ?Sized;
+}
+
+impl<T: ?Sized> ValidateInput for Untrusted<T> {
+ type Inner = T;
+}
+
+impl<'a, T: ?Sized> ValidateInput for &'a Untrusted<T> {
+ type Inner = &'a T;
+}
+
+impl<'a, T: ?Sized> ValidateInput for &'a mut Untrusted<T> {
+ type Inner = &'a mut T;
+}
+
+mod private {
+ use super::Untrusted;
+
+ pub trait Sealed {}
+
+ impl<T: ?Sized> Sealed for Untrusted<T> {}
+ impl<'a, T: ?Sized> Sealed for &'a Untrusted<T> {}
+ impl<'a, T: ?Sized> Sealed for &'a mut Untrusted<T> {}
+}
+
+/// Validate [`Untrusted`] data.
+///
+/// Care must be taken when implementing this trait, as unprotected access to unvalidated data is
+/// given to the [`Validate::validate`] function. The implementer must ensure that the data is only
+/// used for logic after successful validation.
+pub trait Validate<'c, Input: ValidateInput, Context>: Sized {
+ /// Validation error.
+ type Err;
+
+ /// Validate the raw input.
+ ///
+ /// `Context` is additional context needed for validation. The `'c` lifetime is the lifetime of
+ /// the borrowed context (`&'c Foo`).
+ fn validate(raw: Input::Inner, context: Context) -> Result<Self, Self::Err>;
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* [PATCH v5 04/23] X.509: Make certificate parser public
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (2 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 03/23] rust: validate: add `Validate` trait alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:25 ` [PATCH v5 05/23] X.509: Parse Subject Alternative Name in certificates alistair23
` (18 subsequent siblings)
22 siblings, 0 replies; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Dan Williams, Alistair Francis, Ilpo Järvinen,
Jonathan Cameron
From: Lukas Wunner <lukas@wunner.de>
The upcoming support for PCI device authentication with CMA-SPDM
(PCIe r6.1 sec 6.31) requires validating the Subject Alternative Name
in X.509 certificates.
High-level functions for X.509 parsing such as key_create_or_update()
throw away the internal, low-level struct x509_certificate after
extracting the struct public_key and public_key_signature from it.
The Subject Alternative Name is thus inaccessible when using those
functions.
Afford CMA-SPDM access to the Subject Alternative Name by making struct
x509_certificate public, together with the functions for parsing an
X.509 certificate into such a struct and freeing such a struct.
No functional change intended.
Signed-off-by: Lukas Wunner <lukas@wunner.de>
Reviewed-by: Dan Williams <dan.j.williams@intel.com>
Reviewed-by: Alistair Francis <alistair.francis@wdc.com>
Reviewed-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
Reviewed-by: Jonathan Cameron <Jonathan.Cameron@huawei.com>
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
crypto/asymmetric_keys/x509_parser.h | 42 +--------------------
include/keys/x509-parser.h | 55 ++++++++++++++++++++++++++++
2 files changed, 56 insertions(+), 41 deletions(-)
create mode 100644 include/keys/x509-parser.h
diff --git a/crypto/asymmetric_keys/x509_parser.h b/crypto/asymmetric_keys/x509_parser.h
index b7aeebdddb360..39f1521b773dd 100644
--- a/crypto/asymmetric_keys/x509_parser.h
+++ b/crypto/asymmetric_keys/x509_parser.h
@@ -5,51 +5,11 @@
* Written by David Howells (dhowells@redhat.com)
*/
-#include <linux/cleanup.h>
-#include <linux/time.h>
-#include <crypto/public_key.h>
-#include <keys/asymmetric-type.h>
-#include <crypto/sha2.h>
-
-struct x509_certificate {
- struct x509_certificate *next;
- struct x509_certificate *signer; /* Certificate that signed this one */
- struct public_key *pub; /* Public key details */
- struct public_key_signature *sig; /* Signature parameters */
- u8 sha256[SHA256_DIGEST_SIZE]; /* Hash for blacklist purposes */
- char *issuer; /* Name of certificate issuer */
- char *subject; /* Name of certificate subject */
- struct asymmetric_key_id *id; /* Issuer + Serial number */
- struct asymmetric_key_id *skid; /* Subject + subjectKeyId (optional) */
- time64_t valid_from;
- time64_t valid_to;
- const void *tbs; /* Signed data */
- unsigned tbs_size; /* Size of signed data */
- unsigned raw_sig_size; /* Size of signature */
- const void *raw_sig; /* Signature data */
- const void *raw_serial; /* Raw serial number in ASN.1 */
- unsigned raw_serial_size;
- unsigned raw_issuer_size;
- const void *raw_issuer; /* Raw issuer name in ASN.1 */
- const void *raw_subject; /* Raw subject name in ASN.1 */
- unsigned raw_subject_size;
- unsigned raw_skid_size;
- const void *raw_skid; /* Raw subjectKeyId in ASN.1 */
- unsigned index;
- bool seen; /* Infinite recursion prevention */
- bool verified;
- bool self_signed; /* T if self-signed (check unsupported_sig too) */
- bool unsupported_sig; /* T if signature uses unsupported crypto */
- bool blacklisted;
-};
+#include <keys/x509-parser.h>
/*
* x509_cert_parser.c
*/
-extern void x509_free_certificate(struct x509_certificate *cert);
-DEFINE_FREE(x509_free_certificate, struct x509_certificate *,
- if (!IS_ERR(_T)) x509_free_certificate(_T))
-extern struct x509_certificate *x509_cert_parse(const void *data, size_t datalen);
extern int x509_decode_time(time64_t *_t, size_t hdrlen,
unsigned char tag,
const unsigned char *value, size_t vlen);
diff --git a/include/keys/x509-parser.h b/include/keys/x509-parser.h
new file mode 100644
index 0000000000000..8b68e720693a6
--- /dev/null
+++ b/include/keys/x509-parser.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* X.509 certificate parser
+ *
+ * Copyright (C) 2012 Red Hat, Inc. All Rights Reserved.
+ * Written by David Howells (dhowells@redhat.com)
+ */
+
+#ifndef _KEYS_X509_PARSER_H
+#define _KEYS_X509_PARSER_H
+
+#include <linux/cleanup.h>
+#include <linux/time.h>
+#include <crypto/public_key.h>
+#include <keys/asymmetric-type.h>
+#include <crypto/sha2.h>
+
+struct x509_certificate {
+ struct x509_certificate *next;
+ struct x509_certificate *signer; /* Certificate that signed this one */
+ struct public_key *pub; /* Public key details */
+ struct public_key_signature *sig; /* Signature parameters */
+ u8 sha256[SHA256_DIGEST_SIZE]; /* Hash for blacklist purposes */
+ char *issuer; /* Name of certificate issuer */
+ char *subject; /* Name of certificate subject */
+ struct asymmetric_key_id *id; /* Issuer + Serial number */
+ struct asymmetric_key_id *skid; /* Subject + subjectKeyId (optional) */
+ time64_t valid_from;
+ time64_t valid_to;
+ const void *tbs; /* Signed data */
+ unsigned tbs_size; /* Size of signed data */
+ unsigned raw_sig_size; /* Size of signature */
+ const void *raw_sig; /* Signature data */
+ const void *raw_serial; /* Raw serial number in ASN.1 */
+ unsigned raw_serial_size;
+ unsigned raw_issuer_size;
+ const void *raw_issuer; /* Raw issuer name in ASN.1 */
+ const void *raw_subject; /* Raw subject name in ASN.1 */
+ unsigned raw_subject_size;
+ unsigned raw_skid_size;
+ const void *raw_skid; /* Raw subjectKeyId in ASN.1 */
+ unsigned index;
+ bool seen; /* Infinite recursion prevention */
+ bool verified;
+ bool self_signed; /* T if self-signed (check unsupported_sig too) */
+ bool unsupported_sig; /* T if signature uses unsupported crypto */
+ bool blacklisted;
+};
+
+struct x509_certificate *x509_cert_parse(const void *data, size_t datalen);
+void x509_free_certificate(struct x509_certificate *cert);
+
+DEFINE_FREE(x509_free_certificate, struct x509_certificate *,
+ if (!IS_ERR(_T)) x509_free_certificate(_T))
+
+#endif /* _KEYS_X509_PARSER_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* [PATCH v5 05/23] X.509: Parse Subject Alternative Name in certificates
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (3 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 04/23] X.509: Make certificate parser public alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:25 ` [PATCH v5 06/23] X.509: Move certificate length retrieval into new helper alistair23
` (17 subsequent siblings)
22 siblings, 0 replies; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis, Ilpo Järvinen,
Jonathan Cameron, Dan Williams
From: Lukas Wunner <lukas@wunner.de>
The upcoming support for PCI device authentication with CMA-SPDM
(PCIe r6.1 sec 6.31) requires validating the Subject Alternative Name
in X.509 certificates.
Store a pointer to the Subject Alternative Name upon parsing for
consumption by CMA-SPDM.
Signed-off-by: Lukas Wunner <lukas@wunner.de>
Reviewed-by: Wilfred Mallawa <wilfred.mallawa@wdc.com>
Reviewed-by: Alistair Francis <alistair.francis@wdc.com>
Reviewed-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
Reviewed-by: Jonathan Cameron <Jonathan.Cameron@huawei.com>
Acked-by: Dan Williams <dan.j.williams@intel.com>
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
crypto/asymmetric_keys/x509_cert_parser.c | 9 +++++++++
include/keys/x509-parser.h | 2 ++
2 files changed, 11 insertions(+)
diff --git a/crypto/asymmetric_keys/x509_cert_parser.c b/crypto/asymmetric_keys/x509_cert_parser.c
index bfd10f0195e01..c3ec2846695a5 100644
--- a/crypto/asymmetric_keys/x509_cert_parser.c
+++ b/crypto/asymmetric_keys/x509_cert_parser.c
@@ -596,6 +596,15 @@ int x509_process_extension(void *context, size_t hdrlen,
return 0;
}
+ if (ctx->last_oid == OID_subjectAltName) {
+ if (ctx->cert->raw_san)
+ return -EBADMSG;
+
+ ctx->cert->raw_san = v;
+ ctx->cert->raw_san_size = vlen;
+ return 0;
+ }
+
if (ctx->last_oid == OID_keyUsage) {
/*
* Get hold of the keyUsage bit string
diff --git a/include/keys/x509-parser.h b/include/keys/x509-parser.h
index 8b68e720693a6..4e6a05a8c7a66 100644
--- a/include/keys/x509-parser.h
+++ b/include/keys/x509-parser.h
@@ -38,6 +38,8 @@ struct x509_certificate {
unsigned raw_subject_size;
unsigned raw_skid_size;
const void *raw_skid; /* Raw subjectKeyId in ASN.1 */
+ const void *raw_san; /* Raw subjectAltName in ASN.1 */
+ unsigned raw_san_size;
unsigned index;
bool seen; /* Infinite recursion prevention */
bool verified;
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* [PATCH v5 06/23] X.509: Move certificate length retrieval into new helper
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (4 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 05/23] X.509: Parse Subject Alternative Name in certificates alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:25 ` [PATCH v5 07/23] rust: add bindings for hash.h alistair23
` (16 subsequent siblings)
22 siblings, 0 replies; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Dan Williams, Alistair Francis, Jonathan Cameron
From: Lukas Wunner <lukas@wunner.de>
The upcoming in-kernel SPDM library (Security Protocol and Data Model,
https://www.dmtf.org/dsp/DSP0274) needs to retrieve the length from
ASN.1 DER-encoded X.509 certificates.
Such code already exists in x509_load_certificate_list(), so move it
into a new helper for reuse by SPDM.
Export the helper so that SPDM can be tristate. (Some upcoming users of
the SPDM libray may be modular, such as SCSI and ATA.)
No functional change intended.
Signed-off-by: Lukas Wunner <lukas@wunner.de>
Reviewed-by: Dan Williams <dan.j.williams@intel.com>
Reviewed-by: Alistair Francis <alistair.francis@wdc.com>
Reviewed-by: Jonathan Cameron <Jonathan.Cameron@huawei.com>
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
crypto/asymmetric_keys/x509_loader.c | 38 +++++++++++++++++++---------
include/keys/asymmetric-type.h | 2 ++
2 files changed, 28 insertions(+), 12 deletions(-)
diff --git a/crypto/asymmetric_keys/x509_loader.c b/crypto/asymmetric_keys/x509_loader.c
index 0d516c77cc26a..174451b93edad 100644
--- a/crypto/asymmetric_keys/x509_loader.c
+++ b/crypto/asymmetric_keys/x509_loader.c
@@ -4,28 +4,42 @@
#include <linux/key.h>
#include <keys/asymmetric-type.h>
+ssize_t x509_get_certificate_length(const u8 *p, unsigned long buflen)
+{
+ ssize_t plen;
+
+ /* Each cert begins with an ASN.1 SEQUENCE tag and must be more
+ * than 256 bytes in size.
+ */
+ if (buflen < 4)
+ return -EINVAL;
+
+ if (p[0] != 0x30 ||
+ p[1] != 0x82)
+ return -EINVAL;
+
+ plen = (p[2] << 8) | p[3];
+ plen += 4;
+ if (plen > buflen)
+ return -EINVAL;
+
+ return plen;
+}
+EXPORT_SYMBOL_GPL(x509_get_certificate_length);
+
int x509_load_certificate_list(const u8 cert_list[],
const unsigned long list_size,
const struct key *keyring)
{
key_ref_t key;
const u8 *p, *end;
- size_t plen;
+ ssize_t plen;
p = cert_list;
end = p + list_size;
while (p < end) {
- /* Each cert begins with an ASN.1 SEQUENCE tag and must be more
- * than 256 bytes in size.
- */
- if (end - p < 4)
- goto dodgy_cert;
- if (p[0] != 0x30 ||
- p[1] != 0x82)
- goto dodgy_cert;
- plen = (p[2] << 8) | p[3];
- plen += 4;
- if (plen > end - p)
+ plen = x509_get_certificate_length(p, end - p);
+ if (plen < 0)
goto dodgy_cert;
key = key_create_or_update(make_key_ref(keyring, 1),
diff --git a/include/keys/asymmetric-type.h b/include/keys/asymmetric-type.h
index 1b91c8f98688d..301efa952e263 100644
--- a/include/keys/asymmetric-type.h
+++ b/include/keys/asymmetric-type.h
@@ -84,6 +84,8 @@ extern struct key *find_asymmetric_key(struct key *keyring,
const struct asymmetric_key_id *id_2,
bool partial);
+ssize_t x509_get_certificate_length(const u8 *p, unsigned long buflen);
+
int x509_load_certificate_list(const u8 cert_list[], const unsigned long list_size,
const struct key *keyring);
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* [PATCH v5 07/23] rust: add bindings for hash.h
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (5 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 06/23] X.509: Move certificate length retrieval into new helper alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:25 ` [PATCH v5 08/23] rust: device: Expose Device from_raw() publically alistair23
` (15 subsequent siblings)
22 siblings, 0 replies; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Alistair Francis <alistair.francis@wdc.com>
Make the functions crypto_shash_descsize(), crypto_shash_digestsize()
and crypto_free_shash() available to Rust.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
rust/bindings/bindings_helper.h | 1 +
rust/helpers/hash.c | 22 ++++++++++++++++++++++
rust/helpers/helpers.c | 1 +
3 files changed, 24 insertions(+)
create mode 100644 rust/helpers/hash.c
diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h
index 3791af0aadc73..4992bc3194e7e 100644
--- a/rust/bindings/bindings_helper.h
+++ b/rust/bindings/bindings_helper.h
@@ -28,6 +28,7 @@
*/
#include <linux/hrtimer_types.h>
+#include <crypto/hash.h>
#include <linux/acpi.h>
#include <linux/gpu_buddy.h>
#include <drm/drm_device.h>
diff --git a/rust/helpers/hash.c b/rust/helpers/hash.c
new file mode 100644
index 0000000000000..0d88752084673
--- /dev/null
+++ b/rust/helpers/hash.c
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <crypto/hash.h>
+
+#ifdef CONFIG_CRYPTO
+
+__rust_helper unsigned int rust_helper_crypto_shash_descsize(struct crypto_shash *tfm)
+{
+ return crypto_shash_descsize(tfm);
+}
+
+__rust_helper unsigned int rust_helper_crypto_shash_digestsize(struct crypto_shash *tfm)
+{
+ return crypto_shash_digestsize(tfm);
+}
+
+__rust_helper void rust_helper_crypto_free_shash(struct crypto_shash *tfm)
+{
+ crypto_free_shash(tfm);
+}
+
+#endif /* CONFIG_CRYPTO */
diff --git a/rust/helpers/helpers.c b/rust/helpers/helpers.c
index d62c7be1ca7bd..0dcdad905f0ef 100644
--- a/rust/helpers/helpers.c
+++ b/rust/helpers/helpers.c
@@ -67,6 +67,7 @@
#include "fs.c"
#include "fwctl.c"
#include "gpu.c"
+#include "hash.c"
#include "interrupt.c"
#include "io.c"
#include "irq.c"
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* [PATCH v5 08/23] rust: device: Expose Device from_raw() publically
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (6 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 07/23] rust: add bindings for hash.h alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:34 ` sashiko-bot
2026-10-27 5:25 ` [PATCH v5 09/23] lib: rspdm: Initial commit of Rust SPDM alistair23
` (14 subsequent siblings)
22 siblings, 1 reply; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Alistair Francis <alistair.francis@wdc.com>
We are going to call the from_raw() function later as part of the SPDM
library, so expose it publically.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
rust/kernel/device.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rust/kernel/device.rs b/rust/kernel/device.rs
index 2291d85b6849e..a355ac9803410 100644
--- a/rust/kernel/device.rs
+++ b/rust/kernel/device.rs
@@ -262,7 +262,7 @@ pub unsafe fn drvdata_borrow<T>(&self) -> Pin<&T> {
impl<Ctx: DeviceContext> Device<Ctx> {
/// Obtain the raw `struct device *`.
- pub(crate) fn as_raw(&self) -> *mut bindings::device {
+ pub fn as_raw(&self) -> *mut bindings::device {
self.0.get()
}
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* Re: [PATCH v5 08/23] rust: device: Expose Device from_raw() publically
2026-10-27 5:25 ` [PATCH v5 08/23] rust: device: Expose Device from_raw() publically alistair23
@ 2026-10-27 5:34 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-10-27 5:34 UTC (permalink / raw)
To: alistair23, linux-pci, linux-cxl; +Cc: ojeda, gary
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] rust: device: commit message claims to expose from_raw() instead of as_raw()
- [Low] rust: device: missing #[inline] annotation on public Device::as_raw()
--
commit 4612538f4ce0b7f345e904370c4f1a9efe8adc51
Author: Alistair Francis <alistair.francis@wdc.com>
rust: device: Expose Device from_raw() publically
This commit changes the visibility of Device::as_raw() in
rust/kernel/device.rs from pub(crate) to pub so it can be called from outside
the kernel crate.
> rust: device: Expose Device from_raw() publically
>
> We are going to call the from_raw() function later as part of the SPDM
> library, so expose it publically.
>
> Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
[Severity: Medium]
Should the commit subject and message refer to Device::as_raw() instead of
from_raw(), and fix the typo "publically" to "publicly"?
In rust/kernel/device.rs, Device::from_raw() was already public prior to this
patch:
pub unsafe fn from_raw<'a>(ptr: *mut bindings::device) -> &'a Self {
// SAFETY: Guaranteed by the safety requirements of the function.
unsafe { &*ptr.cast() }
}
Whereas the diff below actually exposes Device::as_raw() publicly so that
callers in lib/rspdm/lib.rs and lib/rspdm/state.rs can pass the raw device
pointer to C callbacks.
> diff --git a/rust/kernel/device.rs b/rust/kernel/device.rs
> index 2291d85b6849e..a355ac9803410 100644
> --- a/rust/kernel/device.rs
> +++ b/rust/kernel/device.rs
> @@ -262,7 +262,7 @@ pub unsafe fn drvdata_borrow<T>(&self) -> Pin<&T> {
>
> impl<Ctx: DeviceContext> Device<Ctx> {
> /// Obtain the raw `struct device *`.
> - pub(crate) fn as_raw(&self) -> *mut bindings::device {
> + pub fn as_raw(&self) -> *mut bindings::device {
[Severity: Low]
This isn't a bug, but since Device::as_raw() in rust/kernel/device.rs is a
small one-line accessor in the kernel crate now exposed across crate
boundaries, should it be annotated with #[inline]?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027052536.1722081-1-alistair.francis@wdc.com?part=8
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH v5 09/23] lib: rspdm: Initial commit of Rust SPDM
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (7 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 08/23] rust: device: Expose Device from_raw() publically alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:42 ` sashiko-bot
2026-10-27 5:25 ` [PATCH v5 10/23] PCI/TSM: Rename pf0 to host alistair23
` (13 subsequent siblings)
22 siblings, 1 reply; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis, Jonathan Cameron
From: Alistair Francis <alistair.francis@wdc.com>
This is the initial commit of the Rust SPDM library.
Security Protocols and Data Models (SPDM) [1] is used for authentication,
attestation and key exchange. SPDM is generally used over a range of
transports, such as PCIe, MCTP/SMBus/I3C, ATA, SCSI, NVMe or TCP.
From the kernels perspective SPDM is used to authenticate and attest devices.
This initial commit setups the framework for SPDM support. This uses the
`Validate` and `Untrusted` traits, which means we can treat the
responses as `Untrusted` until we validate them.
The next few patches add SPDM functionality, which allows us to perform
basic authentication.
1: https://www.dmtf.org/standards/spdm
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
Reviewed-by: Jonathan Cameron <jonathan.cameron@oss.qualcomm.com>
---
MAINTAINERS | 12 ++
include/linux/spdm.h | 66 ++++++++
lib/Kconfig | 17 +++
lib/Makefile | 2 +
lib/rspdm/Makefile | 10 ++
lib/rspdm/consts.rs | 18 +++
lib/rspdm/lib.rs | 263 ++++++++++++++++++++++++++++++++
lib/rspdm/state.rs | 145 ++++++++++++++++++
lib/rspdm/validator.rs | 92 +++++++++++
rust/bindings/bindings_helper.h | 1 +
10 files changed, 626 insertions(+)
create mode 100644 include/linux/spdm.h
create mode 100644 lib/rspdm/Makefile
create mode 100644 lib/rspdm/consts.rs
create mode 100644 lib/rspdm/lib.rs
create mode 100644 lib/rspdm/state.rs
create mode 100644 lib/rspdm/validator.rs
diff --git a/MAINTAINERS b/MAINTAINERS
index 03649f08c7c75..783da0b234541 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25290,6 +25290,18 @@ M: Security Officers <security@kernel.org>
S: Supported
F: Documentation/process/security-bugs.rst
+SECURITY PROTOCOL AND DATA MODEL (SPDM)
+M: Alistair Francis <alistair@alistair23.me>
+M: Lukas Wunner <lukas@wunner.de>
+M: Jonathan Cameron <jic23@kernel.org>
+L: linux-coco@lists.linux.dev
+L: linux-cxl@vger.kernel.org
+L: linux-pci@vger.kernel.org
+S: Maintained
+T: git git://git.kernel.org/pub/scm/linux/kernel/git/devsec/spdm.git
+F: include/linux/spdm.h
+F: lib/rspdm/
+
SECURITY SUBSYSTEM
M: Paul Moore <paul@paul-moore.com>
M: James Morris <jmorris@namei.org>
diff --git a/include/linux/spdm.h b/include/linux/spdm.h
new file mode 100644
index 0000000000000..3cd5c1bf01def
--- /dev/null
+++ b/include/linux/spdm.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * DMTF Security Protocol and Data Model (SPDM)
+ * https://www.dmtf.org/dsp/DSP0274
+ *
+ * Copyright (C) 2021-22 Huawei
+ * Jonathan Cameron <Jonathan.Cameron@huawei.com>
+ *
+ * Copyright (C) 2022-24 Intel Corporation
+ */
+
+#ifndef _SPDM_H_
+#define _SPDM_H_
+
+#include <linux/types.h>
+
+struct key;
+struct device;
+struct spdm_state;
+struct x509_certificate;
+
+typedef int (spdm_transport)(void *priv, struct device *dev,
+ const void *request, size_t request_sz,
+ void *response, size_t response_sz);
+
+typedef int (spdm_validate)(struct device *dev, u8 slot,
+ struct x509_certificate *leaf_cert);
+
+/**
+ * spdm_create() - Allocate SPDM session
+ * @dev: Responder device. Used for error reporting and passed to @transport.
+ * @transport: Transport function to perform one message exchange.
+ * @transport_priv: Transport function private data.
+ * @transport_sz: Maximum message size the transport is capable of
+ * (in bytes).
+ * @validate: Function to validate additional leaf certificate requirements
+ * (optional, may be %NULL).
+ *
+ * Return a pointer to the allocated SPDM session state or NULL on error.
+ * The returned session must be freed with spdm_destroy().
+ */
+struct spdm_state *spdm_create(struct device *dev, spdm_transport *transport,
+ void *transport_priv, u32 transport_sz,
+ spdm_validate *validate);
+
+/**
+ * spdm_authenticate() - Authenticate device
+ * @spdm_state: SPDM session state
+ *
+ * Authenticate a device through a sequence of GET_VERSION, GET_CAPABILITIES,
+ * NEGOTIATE_ALGORITHMS, GET_DIGESTS, GET_CERTIFICATE and CHALLENGE exchanges.
+ *
+ * Return 0 on success or a negative errno. In particular, -EPROTONOSUPPORT
+ * indicates authentication is not supported by the device.
+ */
+int spdm_authenticate(struct spdm_state *spdm_state);
+
+/**
+ * spdm_destroy() - Destroy SPDM session
+ * @spdm_state: SPDM session state
+ *
+ * Free a session allocated by spdm_create().
+ */
+void spdm_destroy(struct spdm_state *spdm_state);
+
+#endif
diff --git a/lib/Kconfig b/lib/Kconfig
index 4e6b34c3346d5..b84facfa88bc9 100644
--- a/lib/Kconfig
+++ b/lib/Kconfig
@@ -588,6 +588,23 @@ config LWQ_TEST
help
Run boot-time test of light-weight queuing.
+config RSPDM
+ bool "Rust SPDM"
+ depends on RUST
+ select KEYS
+ select CRYPTO
+ select ASYMMETRIC_KEY_TYPE
+ select ASYMMETRIC_PUBLIC_KEY_SUBTYPE
+ select X509_CERTIFICATE_PARSER
+ help
+ The Rust implementation of the Security Protocol and Data Model (SPDM)
+ allows for device authentication, measurement, key exchange and
+ encrypted sessions.
+
+ Crypto algorithms negotiated with SPDM are limited to those enabled
+ in .config. Users of SPDM therefore need to also select
+ any algorithms they deem mandatory.
+
endmenu
config GENERIC_IOREMAP
diff --git a/lib/Makefile b/lib/Makefile
index a2afe954805cb..3a79a3738975f 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -297,6 +297,8 @@ obj-$(CONFIG_PERCPU_TEST) += percpu_test.o
obj-$(CONFIG_ASN1) += asn1_decoder.o
obj-$(CONFIG_ASN1_ENCODER) += asn1_encoder.o
+obj-$(CONFIG_RSPDM) += rspdm/
+
obj-$(CONFIG_FONT_SUPPORT) += fonts/
#
diff --git a/lib/rspdm/Makefile b/lib/rspdm/Makefile
new file mode 100644
index 0000000000000..3f5cae33ccb8d
--- /dev/null
+++ b/lib/rspdm/Makefile
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Rust implementation of the DMTF Security Protocol and Data Model (SPDM)
+# https://www.dmtf.org/dsp/DSP0274
+#
+# Copyright (C) 2026 Western Digital
+
+obj-$(CONFIG_RSPDM) += spdm.o
+
+spdm-y := lib.o
diff --git a/lib/rspdm/consts.rs b/lib/rspdm/consts.rs
new file mode 100644
index 0000000000000..ef6d61d4998b5
--- /dev/null
+++ b/lib/rspdm/consts.rs
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: GPL-2.0
+
+// Copyright (C) 2026 Western Digital
+
+//! Constants used by the library
+//!
+//! Rust implementation of the DMTF Security Protocol and Data Model (SPDM)
+//! <https://www.dmtf.org/dsp/DSP0274>
+
+// SPDM versions supported by this implementation
+pub(crate) const SPDM_VER_10: u8 = 0x10;
+
+pub(crate) const SPDM_MIN_VER: u8 = SPDM_VER_10;
+
+#[allow(dead_code)]
+pub(crate) const SPDM_REQ: u8 = 0x80;
+#[allow(dead_code)]
+pub(crate) const SPDM_ERROR: u8 = 0x7f;
diff --git a/lib/rspdm/lib.rs b/lib/rspdm/lib.rs
new file mode 100644
index 0000000000000..27449ac1ca25d
--- /dev/null
+++ b/lib/rspdm/lib.rs
@@ -0,0 +1,263 @@
+// SPDX-License-Identifier: GPL-2.0
+
+// Copyright (C) 2026 Western Digital
+
+//! Top level library for SPDM
+//!
+//! Rust implementation of the DMTF Security Protocol and Data Model (SPDM)
+//! <https://www.dmtf.org/dsp/DSP0274>
+//!
+//! Top level library, including C compatible public functions to be called
+//! from other subsytems.
+
+use crate::bindings::{
+ spdm_state,
+ EPROTONOSUPPORT, //
+};
+use core::ffi::{
+ c_int,
+ c_void, //
+};
+use core::ptr;
+use kernel::prelude::*;
+use kernel::{
+ alloc::flags,
+ bindings,
+ device::{
+ Bound,
+ Device, //
+ },
+ error::to_result,
+ new_mutex,
+ sync::Mutex,
+ types::ForeignOwnable, //
+};
+
+use crate::state::{SpdmState, Transport};
+
+const __LOG_PREFIX: &[u8] = b"spdm\0";
+
+mod consts;
+mod state;
+mod validator;
+
+/// FFI shim implementing [`Transport`] for C transport functions.
+///
+/// The private data pointer is owned by the C caller; see the safety
+/// requirements of [`spdm_create()`].
+struct CTransport {
+ func: bindings::spdm_transport,
+ priv_: *mut c_void,
+}
+
+impl Transport for CTransport {
+ fn exchange(
+ &self,
+ dev: &Device<Bound>,
+ request: &[u8],
+ response: &mut [u8],
+ ) -> Result<usize, Error> {
+ let func = self.func.ok_or(EINVAL)?;
+
+ // SAFETY: `func` is non-NULL as checked above. By the safety
+ // requirements of `spdm_create()`, `priv_` remains valid until
+ // `spdm_destroy()` is called, and `dev` remains alive and bound for
+ // the lifetime of the session. `request` and `response` are valid
+ // slices that outlive this call, and the callback must not retain
+ // either of them.
+ let length = unsafe {
+ func(
+ self.priv_,
+ dev.as_raw(),
+ request.as_ptr().cast::<c_void>(),
+ request.len(),
+ response.as_mut_ptr().cast::<c_void>(),
+ response.len(),
+ )
+ };
+ to_result(length)?;
+
+ Ok(length as usize)
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+#[repr(u8)]
+pub(crate) enum SpdmErrorCode {
+ InvalidRequest = 0x01,
+ /// This was removed in version 1.2.0 and is now reserved
+ InvalidSession = 0x02,
+ Busy = 0x03,
+ UnexpectedRequest = 0x04,
+ Unspecified = 0x05,
+ DecryptError = 0x06,
+ UnsupportedRequest = 0x07,
+ RequestInFlight = 0x08,
+ InvalidResponseCode = 0x09,
+ SessionLimitExceeded = 0x0a,
+ SessionRequired = 0x0b,
+ ResetRequired = 0x0c,
+ ResponseTooLarge = 0x0d,
+ RequestTooLarge = 0x0e,
+ LargeResponse = 0x0f,
+ MessageLost = 0x10,
+ InvalidPolicy = 0x11,
+ VersionMismatch = 0x41,
+ ResponseNotReady = 0x42,
+ RequestResynch = 0x43,
+ OperationFailed = 0x44,
+ NoPendingRequests = 0x45,
+ RequestSessionTerminated = 0x46,
+ InvalidState = 0x47,
+ VendorDefinedError = 0xff,
+}
+
+impl TryFrom<u8> for SpdmErrorCode {
+ type Error = Error;
+
+ fn try_from(value: u8) -> Result<Self, Self::Error> {
+ Ok(match value {
+ 0x01 => Self::InvalidRequest,
+ 0x02 => Self::InvalidSession,
+ 0x03 => Self::Busy,
+ 0x04 => Self::UnexpectedRequest,
+ 0x05 => Self::Unspecified,
+ 0x06 => Self::DecryptError,
+ 0x07 => Self::UnsupportedRequest,
+ 0x08 => Self::RequestInFlight,
+ 0x09 => Self::InvalidResponseCode,
+ 0x0a => Self::SessionLimitExceeded,
+ 0x0b => Self::SessionRequired,
+ 0x0c => Self::ResetRequired,
+ 0x0d => Self::ResponseTooLarge,
+ 0x0e => Self::RequestTooLarge,
+ 0x0f => Self::LargeResponse,
+ 0x10 => Self::MessageLost,
+ 0x11 => Self::InvalidPolicy,
+ 0x41 => Self::VersionMismatch,
+ 0x42 => Self::ResponseNotReady,
+ 0x43 => Self::RequestResynch,
+ 0x44 => Self::OperationFailed,
+ 0x45 => Self::NoPendingRequests,
+ 0x46 => Self::RequestSessionTerminated,
+ 0x47 => Self::InvalidState,
+ 0xff => Self::VendorDefinedError,
+ _ => return Err(EINVAL),
+ })
+ }
+}
+
+impl From<SpdmErrorCode> for Error {
+ fn from(code: SpdmErrorCode) -> Self {
+ match code {
+ SpdmErrorCode::InvalidRequest => EINVAL,
+ SpdmErrorCode::InvalidSession => EINVAL,
+ SpdmErrorCode::Busy => EBUSY,
+ SpdmErrorCode::UnexpectedRequest => EINVAL,
+ SpdmErrorCode::Unspecified => EINVAL,
+ SpdmErrorCode::DecryptError => EIO,
+ SpdmErrorCode::UnsupportedRequest => EINVAL,
+ SpdmErrorCode::RequestInFlight => EINVAL,
+ SpdmErrorCode::InvalidResponseCode => EINVAL,
+ SpdmErrorCode::SessionLimitExceeded => EBUSY,
+ SpdmErrorCode::SessionRequired => EINVAL,
+ SpdmErrorCode::ResetRequired => ECONNRESET,
+ SpdmErrorCode::ResponseTooLarge => EINVAL,
+ SpdmErrorCode::RequestTooLarge => EINVAL,
+ SpdmErrorCode::LargeResponse => EMSGSIZE,
+ SpdmErrorCode::MessageLost => EIO,
+ SpdmErrorCode::InvalidPolicy => EINVAL,
+ SpdmErrorCode::VersionMismatch => EINVAL,
+ SpdmErrorCode::ResponseNotReady => EINPROGRESS,
+ SpdmErrorCode::RequestResynch => ECONNRESET,
+ SpdmErrorCode::OperationFailed => EINVAL,
+ SpdmErrorCode::NoPendingRequests => ENOENT,
+ SpdmErrorCode::RequestSessionTerminated => EINVAL,
+ SpdmErrorCode::InvalidState => EINVAL,
+ SpdmErrorCode::VendorDefinedError => EINVAL,
+ }
+ }
+}
+
+impl core::fmt::LowerHex for SpdmErrorCode {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ write!(f, "{:#x}", *self as u8)
+ }
+}
+
+/// Allocates an SPDM session.
+///
+/// See `include/linux/spdm.h` for the full C API documentation.
+///
+/// # Safety
+///
+/// The caller must guarantee that `dev` is a valid pointer to a
+/// `struct device` that stays alive and bound to its driver for the
+/// lifetime of the returned session, and must pass the session to
+/// `spdm_destroy()` before `dev` is unbound. The caller must also
+/// guarantee that `transport_priv` remains valid until `spdm_destroy()`
+/// is called, as it is passed back to `transport` on each exchange.
+#[export]
+pub unsafe extern "C" fn spdm_create(
+ dev: *mut bindings::device,
+ transport: bindings::spdm_transport,
+ transport_priv: *mut c_void,
+ transport_sz: u32,
+ validate: bindings::spdm_validate,
+) -> *mut spdm_state {
+ // SAFETY: `dev` points to a valid `struct device` that stays alive for the
+ // lifetime of the session, so the returned reference is valid for the
+ // session's lifetime.
+ let dev: &Device = unsafe { Device::from_raw(dev) };
+
+ // SAFETY: By the safety requirements of this function, the device stays
+ // bound for the lifetime of the session, so the returned
+ // `&Device<Bound>` is valid for the session's lifetime.
+ let dev = unsafe { dev.as_bound() };
+
+ // Wrap the `SpdmState` in a `Mutex` so that concurrent FFI callers (for
+ // example, two threads racing on `spdm_authenticate()` for the same
+ // device) serialize on the lock and never form aliased `&mut SpdmState`
+ // references.
+ let state = SpdmState::new(
+ dev,
+ CTransport {
+ func: transport,
+ priv_: transport_priv,
+ },
+ transport_sz,
+ validate,
+ );
+ match KBox::pin_init(new_mutex!(state), flags::GFP_KERNEL) {
+ Ok(b) => b.into_foreign().cast::<spdm_state>(),
+ Err(_) => ptr::null_mut(),
+ }
+}
+
+/// Authenticates a device through a sequence of SPDM exchanges.
+///
+/// See `include/linux/spdm.h` for the full C API documentation.
+#[export]
+pub extern "C" fn spdm_authenticate(_state_ptr: *mut spdm_state) -> c_int {
+ -(EPROTONOSUPPORT as i32)
+}
+
+/// Destroys an SPDM session.
+///
+/// See `include/linux/spdm.h` for the full C API documentation.
+#[export]
+pub extern "C" fn spdm_destroy(state_ptr: *mut spdm_state) {
+ if state_ptr.is_null() {
+ return;
+ }
+
+ // SAFETY: `state_ptr` was returned from `spdm_create()` which used
+ // `into_foreign()` to create the pointer.
+ let mutex = unsafe {
+ <Pin<KBox<Mutex<SpdmState<'_, CTransport>>>> as ForeignOwnable>::from_foreign(
+ state_ptr.cast::<c_void>(),
+ )
+ };
+
+ drop(mutex);
+}
diff --git a/lib/rspdm/state.rs b/lib/rspdm/state.rs
new file mode 100644
index 0000000000000..f864d869eff0c
--- /dev/null
+++ b/lib/rspdm/state.rs
@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: GPL-2.0
+
+// Copyright (C) 2026 Western Digital
+
+//! The `SpdmState` struct and implementation.
+//!
+//! Rust implementation of the DMTF Security Protocol and Data Model (SPDM)
+//! <https://www.dmtf.org/dsp/DSP0274>
+
+use kernel::prelude::*;
+use kernel::{
+ bindings,
+ device::{
+ Bound,
+ Device, //
+ },
+ error::Error,
+ validate::Untrusted,
+};
+
+use crate::consts::{
+ SPDM_ERROR,
+ SPDM_MIN_VER,
+ SPDM_REQ, //
+};
+use crate::validator::{
+ SpdmErrorRsp,
+ SpdmHeader, //
+};
+
+/// Transport for exchanging SPDM messages with a responder device.
+///
+/// This trait abstracts the bus-specific message transport (for example
+/// a DOE mailbox for PCIe CMA) from the SPDM session logic.
+pub(crate) trait Transport {
+ /// Perform a single SPDM message exchange.
+ ///
+ /// Send `request` to the responder and write the response into
+ /// `response`, returning the number of bytes received on success.
+ fn exchange(
+ &self,
+ dev: &Device<Bound>,
+ request: &[u8],
+ response: &mut [u8],
+ ) -> Result<usize, Error>;
+}
+
+/// The current SPDM session state for a device.
+///
+/// Concurrent access is serialised by wrapping the whole struct in a
+/// `Mutex<SpdmState>` at the FFI boundary, so `spdm_authenticate()` callers
+/// run one at a time and the locked `&mut SpdmState` is the only way to
+/// reach the inner fields.
+///
+/// The lifetime `'a` is the duration for which the responder device and the
+/// transport are guaranteed to stay alive, bounded by the responder device
+/// staying bound to its driver.
+///
+/// `T` is data and an exchange function owned by the caller,
+/// so the borrow checker enforces that the transport data outlives `SpdmState`.
+#[expect(dead_code)]
+pub(crate) struct SpdmState<'a, T: Transport> {
+ /// Responder device. Used for error reporting and passed to `transport`.
+ pub(crate) dev: &'a Device<Bound>,
+ /// Transport used to perform message exchanges. Typically a `&'a`
+ /// reference to transport data owned by the caller, which then must
+ /// outlive the session.
+ pub(crate) transport: T,
+ /// Maximum message size the transport is capable of (in bytes). Used as
+ /// DataTransferSize in GET_CAPABILITIES exchange.
+ pub(crate) transport_sz: u32,
+ /// Function to validate additional leaf certificate requirements.
+ pub(crate) validate: bindings::spdm_validate,
+
+ /// Maximum common supported version of requester and responder.
+ /// Negotiated during GET_VERSION exchange.
+ pub(crate) version: u8,
+}
+
+impl<'a, T: Transport> SpdmState<'a, T> {
+ pub(crate) fn new(
+ dev: &'a Device<Bound>,
+ transport: T,
+ transport_sz: u32,
+ validate: bindings::spdm_validate,
+ ) -> Self {
+ SpdmState {
+ dev,
+ transport,
+ transport_sz,
+ validate,
+ version: SPDM_MIN_VER,
+ }
+ }
+
+ /// Start a SPDM exchange
+ ///
+ /// The data in `request_buf` is sent to the device and the response
+ /// is returned as untrusted data to be validated by the caller.
+ #[allow(dead_code)]
+ pub(crate) fn spdm_exchange(
+ &self,
+ request_buf: &[u8],
+ mut response_buf: KVec<u8>,
+ ) -> Result<Untrusted<KVec<u8>>, Error> {
+ let header_size = core::mem::size_of::<SpdmHeader>();
+ let request: SpdmHeader = Untrusted::new(request_buf).validate(self)?;
+
+ let length = self
+ .transport
+ .exchange(self.dev, request_buf, response_buf.as_mut_slice())?;
+
+ // The transport must not report more bytes than the buffer can hold.
+ if length > response_buf.len() {
+ pr_err!("Transport returned invalid length {length}\n");
+ return Err(EMSGSIZE);
+ }
+
+ // Only validate the bytes actually received from the transport
+ // Note that `length` might be longer then the actual SPDM packet as
+ // some transports will pad the final message
+ response_buf.truncate(length);
+
+ let response: SpdmHeader =
+ Untrusted::new(response_buf.get(..header_size).ok_or(EPROTO)?).validate(self)?;
+
+ if response.code == SPDM_ERROR {
+ let error_rsp: SpdmErrorRsp =
+ Untrusted::new(response_buf.get(..header_size).ok_or(EPROTO)?).validate(self)?;
+ pr_err!("Error response: {error_rsp:?}\n");
+ return Err(error_rsp.error_code.into());
+ }
+
+ if response.code != request.code & !SPDM_REQ {
+ pr_err!(
+ "Response code {:#x} does not match request code {:#x}\n",
+ response.code,
+ request.code
+ );
+ return Err(EPROTO);
+ }
+
+ Ok(Untrusted::new(response_buf))
+ }
+}
diff --git a/lib/rspdm/validator.rs b/lib/rspdm/validator.rs
new file mode 100644
index 0000000000000..0fe341b8dce12
--- /dev/null
+++ b/lib/rspdm/validator.rs
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: GPL-2.0
+
+// Copyright (C) 2026 Western Digital
+
+//! Related structs and their Validate implementations.
+//!
+//! Rust implementation of the DMTF Security Protocol and Data Model (SPDM)
+//! <https://www.dmtf.org/dsp/DSP0274>
+
+use crate::{
+ SpdmErrorCode,
+ SpdmState,
+ Transport, //
+};
+use kernel::prelude::*;
+use kernel::{
+ error::Error,
+ validate::{
+ Untrusted,
+ Validate, //
+ },
+};
+
+#[derive(FromBytes)]
+#[repr(C)]
+pub(crate) struct SpdmHeader {
+ pub(crate) version: u8,
+ pub(crate) code: u8, /* RequestResponseCode */
+ pub(crate) param1: u8,
+ pub(crate) param2: u8,
+}
+
+impl SpdmHeader {
+ #[expect(dead_code)]
+ pub(crate) fn new(code: u8) -> Self {
+ SpdmHeader {
+ version: 0,
+ code,
+ param1: 0,
+ param2: 0,
+ }
+ }
+
+ #[expect(dead_code)]
+ pub(crate) fn to_bytes(&self) -> Result<KVec<u8>> {
+ let mut out = KVec::new();
+
+ out.extend_from_slice(
+ &[self.version, self.code, self.param1, self.param2],
+ GFP_KERNEL,
+ )?;
+
+ Ok(out)
+ }
+}
+
+impl<'a, 'c, T: Transport> Validate<'c, Untrusted<&'a [u8]>, &'c SpdmState<'c, T>> for SpdmHeader {
+ type Err = Error;
+
+ #[allow(clippy::get_first)]
+ fn validate(unvalidated: &[u8], _context: &'c SpdmState<'c, T>) -> Result<Self, Self::Err> {
+ SpdmHeader::read_from_bytes(unvalidated).or(Err(EPROTO))
+ }
+}
+
+#[expect(dead_code)]
+#[derive(Debug)]
+pub(crate) struct SpdmErrorRsp {
+ pub(crate) version: u8,
+ /// This will always be SPDM_ERROR (0x7F)
+ pub(crate) code: u8,
+ pub(crate) error_code: SpdmErrorCode,
+ pub(crate) error_data: u8,
+}
+
+impl<'a, 'c, T: Transport> Validate<'c, Untrusted<&'a [u8]>, &'c SpdmState<'c, T>>
+ for SpdmErrorRsp
+{
+ type Err = Error;
+
+ #[allow(clippy::get_first)]
+ fn validate(unvalidated: &[u8], _context: &'c SpdmState<'c, T>) -> Result<Self, Self::Err> {
+ Ok(SpdmErrorRsp {
+ version: *unvalidated.get(0).ok_or(EPROTO)?,
+ code: *unvalidated.get(1).ok_or(EPROTO)?,
+ // `try_from` rejects unknown `SpdmErrorCode` discriminants.
+ error_code: SpdmErrorCode::try_from(*unvalidated.get(2).ok_or(EPROTO)?)
+ .map_err(|_| EPROTO)?,
+ error_data: *unvalidated.get(3).ok_or(EPROTO)?,
+ })
+ }
+}
diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h
index 4992bc3194e7e..a2512fda77e1f 100644
--- a/rust/bindings/bindings_helper.h
+++ b/rust/bindings/bindings_helper.h
@@ -90,6 +90,7 @@
#include <linux/security.h>
#include <linux/serdev.h>
#include <linux/slab.h>
+#include <linux/spdm.h>
#include <linux/sys_soc.h>
#include <linux/task_work.h>
#include <linux/tracepoint.h>
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* Re: [PATCH v5 09/23] lib: rspdm: Initial commit of Rust SPDM
2026-10-27 5:25 ` [PATCH v5 09/23] lib: rspdm: Initial commit of Rust SPDM alistair23
@ 2026-10-27 5:42 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-10-27 5:42 UTC (permalink / raw)
To: alistair23, linux-pci, linux-cxl; +Cc: ojeda, gary
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] rspdm: un-sliced request_buf in spdm_exchange() fails SpdmHeader validation
- [Low] lib: rspdm: missing unsafe qualifier on spdm_destroy() and spdm_authenticate()
- [Low] lib: rspdm: non-vertical import and core::ffi usage in lib.rs
--
commit 8332eb50f9f3f308b55adfa6b54fbbcfc9566854
Author: Alistair Francis <alistair.francis@wdc.com>
lib: rspdm: Initial commit of Rust SPDM
This commit introduces the initial framework for the Rust Security Protocol
and Data Model (SPDM) library, including C FFI entry points, session state
management, and message header validation.
> diff --git a/lib/rspdm/lib.rs b/lib/rspdm/lib.rs
> new file mode 100644
> index 0000000000000..27449ac1ca25d
> --- /dev/null
> +++ b/lib/rspdm/lib.rs
> @@ -0,0 +1,263 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +// Copyright (C) 2026 Western Digital
> +
> +//! Top level library for SPDM
> +//!
> +//! Rust implementation of the DMTF Security Protocol and Data Model (SPDM)
> +//! <https://www.dmtf.org/dsp/DSP0274>
> +//!
> +//! Top level library, including C compatible public functions to be called
> +//! from other subsytems.
> +
> +use crate::bindings::{
> + spdm_state,
> + EPROTONOSUPPORT, //
> +};
> +use core::ffi::{
> + c_int,
> + c_void, //
> +};
> +use core::ptr;
> +use kernel::prelude::*;
> +use kernel::{
> + alloc::flags,
> + bindings,
> + device::{
> + Bound,
> + Device, //
> + },
> + error::to_result,
> + new_mutex,
> + sync::Mutex,
> + types::ForeignOwnable, //
> +};
> +
> +use crate::state::{SpdmState, Transport};
[Severity: Low]
This isn't a bug, but per Documentation/rust/coding-guidelines.rst, should
c_int and c_void come from kernel::prelude::* rather than core::ffi, and
should the crate::state import use the vertical import style?
[ ... ]
> +/// Authenticates a device through a sequence of SPDM exchanges.
> +///
> +/// See `include/linux/spdm.h` for the full C API documentation.
> +#[export]
> +pub extern "C" fn spdm_authenticate(_state_ptr: *mut spdm_state) -> c_int {
> + -(EPROTONOSUPPORT as i32)
> +}
> +
> +/// Destroys an SPDM session.
> +///
> +/// See `include/linux/spdm.h` for the full C API documentation.
> +#[export]
> +pub extern "C" fn spdm_destroy(state_ptr: *mut spdm_state) {
> + if state_ptr.is_null() {
> + return;
> + }
> +
> + // SAFETY: `state_ptr` was returned from `spdm_create()` which used
> + // `into_foreign()` to create the pointer.
> + let mutex = unsafe {
> + <Pin<KBox<Mutex<SpdmState<'_, CTransport>>>> as ForeignOwnable>::from_foreign(
> + state_ptr.cast::<c_void>(),
> + )
> + };
> +
> + drop(mutex);
> +}
[Severity: Low]
Should spdm_destroy() and spdm_authenticate() be marked unsafe extern "C" fn
with # Safety documentation sections, matching spdm_create()?
In spdm_destroy(), when state_ptr is non-null, it is passed to
ForeignOwnable::from_foreign(), which requires the caller to guarantee that
the pointer was returned by a previous call to into_foreign() and is not
reclaimed more than once:
rust/kernel/types.rs:ForeignOwnable::from_foreign() {
/// # Safety
///
/// The provided pointer must have been returned by a previous call to [`into_foreign`], and it
/// must not be passed to `from_foreign` more than once.
unsafe fn from_foreign(ptr: *mut c_void) -> Self;
}
Similarly, spdm_authenticate() takes a raw *mut spdm_state pointer from C
that later patches in this series pass to ForeignOwnable::borrow(). Without
an unsafe qualifier on these functions, their internal SAFETY comments rely
on caller preconditions that are not part of a safety contract.
> diff --git a/lib/rspdm/state.rs b/lib/rspdm/state.rs
> new file mode 100644
> index 0000000000000..f864d869eff0c
> --- /dev/null
> +++ b/lib/rspdm/state.rs
> @@ -0,0 +1,145 @@
[ ... ]
> + /// Start a SPDM exchange
> + ///
> + /// The data in `request_buf` is sent to the device and the response
> + /// is returned as untrusted data to be validated by the caller.
> + #[allow(dead_code)]
> + pub(crate) fn spdm_exchange(
> + &self,
> + request_buf: &[u8],
> + mut response_buf: KVec<u8>,
> + ) -> Result<Untrusted<KVec<u8>>, Error> {
> + let header_size = core::mem::size_of::<SpdmHeader>();
> + let request: SpdmHeader = Untrusted::new(request_buf).validate(self)?;
[Severity: High]
Will SpdmState::spdm_exchange() fail with EPROTO for any SPDM request
message that has a payload after the 4-byte SpdmHeader?
Here, the full request_buf slice is passed to SpdmHeader::validate() without
slicing it to ..header_size (unlike response_buf.get(..header_size) below).
In lib/rspdm/validator.rs, SpdmHeader::validate() calls
SpdmHeader::read_from_bytes(unvalidated):
lib/rspdm/validator.rs:<SpdmHeader as Validate>::validate() {
fn validate(unvalidated: &[u8], _context: &'c SpdmState<'c, T>) -> Result<Self, Self::Err> {
SpdmHeader::read_from_bytes(unvalidated).or(Err(EPROTO))
}
}
Because zerocopy's FromBytes::read_from_bytes() enforces exact length
equality via Ref::sized_from():
rust/zerocopy/src/ref.rs:Ref::sized_from() {
if bytes.len() != mem::size_of::<T>() {
return Err(SizeError::new(bytes).into());
}
}
any request_buf longer than 4 bytes is rejected with Err(EPROTO) before
self.transport.exchange() is ever called. In the subsequent patches in this
series (such as get_capabilities, negotiate_algorithms, get_certificate, and
challenge), requests with payloads beyond the 4-byte header are passed to
spdm_exchange(), causing spdm_authenticate() to fail unconditionally once it
moves past the 4-byte GET_VERSION request.
Should request_buf be sliced to ..header_size before validating the request
header, or should SpdmHeader::validate() read from the prefix of the slice?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027052536.1722081-1-alistair.francis@wdc.com?part=9
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH v5 10/23] PCI/TSM: Rename pf0 to host
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (8 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 09/23] lib: rspdm: Initial commit of Rust SPDM alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:25 ` [PATCH v5 11/23] PCI/TSM: Support connecting to PCIe CMA devices alistair23
` (12 subsequent siblings)
22 siblings, 0 replies; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Alistair Francis <alistair.francis@wdc.com>
Rename pci_tsm_pf0 to pci_tsm_host (and rename variables and function
names from pf0 to host) as part of converting pci_tsm_host
to be any device that knows how to speak any of CMA, IDE, or
TDISP.
This commit just renames the functions and provides no functional
change. That will happen in the next commit.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
drivers/crypto/ccp/sev-dev-tio.h | 4 +-
drivers/crypto/ccp/sev-dev-tsm.c | 12 ++---
drivers/pci/tsm.c | 84 ++++++++++++++++----------------
include/linux/pci-tsm.h | 18 ++++---
4 files changed, 61 insertions(+), 57 deletions(-)
diff --git a/drivers/crypto/ccp/sev-dev-tio.h b/drivers/crypto/ccp/sev-dev-tio.h
index 67512b3dbc538..78d598686487f 100644
--- a/drivers/crypto/ccp/sev-dev-tio.h
+++ b/drivers/crypto/ccp/sev-dev-tio.h
@@ -53,9 +53,9 @@ struct tsm_dsm_tio {
struct pci_ide *ide[TIO_IDE_MAX_TC];
};
-/* Describes TSM structure for PF0 pointed by pci_dev->tsm */
+/* Describes TSM structure for the link host pointed by pci_dev->tsm */
struct tio_dsm {
- struct pci_tsm_pf0 tsm;
+ struct pci_tsm_host tsm;
struct tsm_dsm_tio data;
struct sev_device *sev;
};
diff --git a/drivers/crypto/ccp/sev-dev-tsm.c b/drivers/crypto/ccp/sev-dev-tsm.c
index 48df8c579c74d..1d2561628f432 100644
--- a/drivers/crypto/ccp/sev-dev-tsm.c
+++ b/drivers/crypto/ccp/sev-dev-tsm.c
@@ -205,7 +205,7 @@ static int stream_alloc(struct pci_dev *pdev, struct pci_ide **ide,
return 0;
}
-static struct pci_tsm *tio_pf0_probe(struct pci_dev *pdev, struct sev_device *sev)
+static struct pci_tsm *tio_host_probe(struct pci_dev *pdev, struct sev_device *sev)
{
struct tio_dsm *dsm __free(kfree) = kzalloc_obj(*dsm);
int rc;
@@ -213,7 +213,7 @@ static struct pci_tsm *tio_pf0_probe(struct pci_dev *pdev, struct sev_device *se
if (!dsm)
return NULL;
- rc = pci_tsm_pf0_constructor(pdev, &dsm->tsm, sev->tsmdev);
+ rc = pci_tsm_host_constructor(pdev, &dsm->tsm, sev->tsmdev);
if (rc)
return NULL;
@@ -226,8 +226,8 @@ static struct pci_tsm *dsm_probe(struct tsm_dev *tsmdev, struct pci_dev *pdev)
{
struct sev_device *sev = tsm_dev_to_sev(tsmdev);
- if (is_pci_tsm_pf0(pdev))
- return tio_pf0_probe(pdev, sev);
+ if (is_pci_tsm_host(pdev))
+ return tio_host_probe(pdev, sev);
return NULL;
}
@@ -237,10 +237,10 @@ static void dsm_remove(struct pci_tsm *tsm)
pci_dbg(pdev, "TSM disabled\n");
- if (is_pci_tsm_pf0(pdev)) {
+ if (is_pci_tsm_host(pdev)) {
struct tio_dsm *dsm = container_of(tsm, struct tio_dsm, tsm.base_tsm);
- pci_tsm_pf0_destructor(&dsm->tsm);
+ pci_tsm_host_destructor(&dsm->tsm);
kfree(dsm);
}
}
diff --git a/drivers/pci/tsm.c b/drivers/pci/tsm.c
index 5fdcd7f2e8207..10c9c6696624d 100644
--- a/drivers/pci/tsm.c
+++ b/drivers/pci/tsm.c
@@ -45,22 +45,22 @@ static inline bool has_tee(struct pci_dev *pdev)
return pdev->devcap & PCI_EXP_DEVCAP_TEE;
}
-/* 'struct pci_tsm_pf0' wraps 'struct pci_tsm' when ->dsm_dev == ->pdev (self) */
-static struct pci_tsm_pf0 *to_pci_tsm_pf0(struct pci_tsm *tsm)
+/* 'struct pci_tsm_host' wraps 'struct pci_tsm' when ->dsm_dev == ->pdev (self) */
+static struct pci_tsm_host *to_pci_tsm_host(struct pci_tsm *tsm)
{
/*
* All "link" TSM contexts reference the device that hosts the DSM
* interface for a set of devices. Walk to the DSM device and cast its
- * ->tsm context to a 'struct pci_tsm_pf0 *'.
+ * ->tsm context to a 'struct pci_tsm_host *'.
*/
- struct pci_dev *pf0 = tsm->dsm_dev;
+ struct pci_dev *host = tsm->dsm_dev;
- if (!is_pci_tsm_pf0(pf0) || !is_dsm(pf0)) {
+ if (!is_pci_tsm_host(host) || !is_dsm(host)) {
pci_WARN_ONCE(tsm->pdev, 1, "invalid context object\n");
return NULL;
}
- return container_of(pf0->tsm, struct pci_tsm_pf0, base_tsm);
+ return container_of(host->tsm, struct pci_tsm_host, base_tsm);
}
static void tsm_remove(struct pci_tsm *tsm)
@@ -186,7 +186,7 @@ static int probe_fn(struct pci_dev *pdev, void *dsm)
static int pci_tsm_connect(struct pci_dev *pdev, struct tsm_dev *tsm_dev)
{
int rc;
- struct pci_tsm_pf0 *tsm_pf0;
+ struct pci_tsm_host *tsm_host;
const struct pci_tsm_ops *ops = tsm_dev->pci_ops;
struct pci_tsm *pci_tsm __free(tsm_remove) = ops->probe(tsm_dev, pdev);
@@ -197,10 +197,10 @@ static int pci_tsm_connect(struct pci_dev *pdev, struct tsm_dev *tsm_dev)
return -ENXIO;
pdev->tsm = pci_tsm;
- tsm_pf0 = to_pci_tsm_pf0(pdev->tsm);
+ tsm_host = to_pci_tsm_host(pdev->tsm);
/* mutex_intr assumes connect() is always sysfs/user driven */
- ACQUIRE(mutex_intr, lock)(&tsm_pf0->lock);
+ ACQUIRE(mutex_intr, lock)(&tsm_host->lock);
if ((rc = ACQUIRE_ERR(mutex_intr, &lock)))
return rc;
@@ -300,15 +300,15 @@ static int remove_fn(struct pci_dev *pdev, void *data)
static int __pci_tsm_unbind(struct pci_dev *pdev, void *data)
{
struct pci_tdi *tdi;
- struct pci_tsm_pf0 *tsm_pf0;
+ struct pci_tsm_host *tsm_host;
lockdep_assert_held(&pci_tsm_rwsem);
if (!pdev->tsm)
return 0;
- tsm_pf0 = to_pci_tsm_pf0(pdev->tsm);
- guard(mutex)(&tsm_pf0->lock);
+ tsm_host = to_pci_tsm_host(pdev->tsm);
+ guard(mutex)(&tsm_host->lock);
tdi = pdev->tsm->tdi;
if (!tdi)
@@ -341,7 +341,7 @@ EXPORT_SYMBOL_GPL(pci_tsm_unbind);
*/
int pci_tsm_bind(struct pci_dev *pdev, struct kvm *kvm, u32 tdi_id)
{
- struct pci_tsm_pf0 *tsm_pf0;
+ struct pci_tsm_host *tsm_host;
struct pci_tdi *tdi;
if (!kvm)
@@ -355,8 +355,8 @@ int pci_tsm_bind(struct pci_dev *pdev, struct kvm *kvm, u32 tdi_id)
if (!is_link_tsm(pdev->tsm->tsm_dev))
return -ENXIO;
- tsm_pf0 = to_pci_tsm_pf0(pdev->tsm);
- guard(mutex)(&tsm_pf0->lock);
+ tsm_host = to_pci_tsm_host(pdev->tsm);
+ guard(mutex)(&tsm_host->lock);
/* Resolve races to bind a TDI */
if (pdev->tsm->tdi) {
@@ -404,7 +404,7 @@ ssize_t pci_tsm_guest_req(struct pci_dev *pdev, enum pci_tsm_req_scope scope,
sockptr_t req_in, size_t in_len, sockptr_t req_out,
size_t out_len, u64 *tsm_code)
{
- struct pci_tsm_pf0 *tsm_pf0;
+ struct pci_tsm_host *tsm_host;
struct pci_tdi *tdi;
int rc;
@@ -422,8 +422,8 @@ ssize_t pci_tsm_guest_req(struct pci_dev *pdev, enum pci_tsm_req_scope scope,
if (!is_link_tsm(pdev->tsm->tsm_dev))
return -ENXIO;
- tsm_pf0 = to_pci_tsm_pf0(pdev->tsm);
- ACQUIRE(mutex_intr, ops_lock)(&tsm_pf0->lock);
+ tsm_host = to_pci_tsm_host(pdev->tsm);
+ ACQUIRE(mutex_intr, ops_lock)(&tsm_host->lock);
if ((rc = ACQUIRE_ERR(mutex_intr, &ops_lock)))
return rc;
@@ -443,7 +443,7 @@ static void pci_tsm_unbind_all(struct pci_dev *pdev)
static void __pci_tsm_disconnect(struct pci_dev *pdev)
{
- struct pci_tsm_pf0 *tsm_pf0 = to_pci_tsm_pf0(pdev->tsm);
+ struct pci_tsm_host *tsm_host = to_pci_tsm_host(pdev->tsm);
const struct pci_tsm_ops *ops = to_pci_tsm_ops(pdev->tsm);
/* disconnect() mutually exclusive with subfunction pci_tsm_init() */
@@ -455,7 +455,7 @@ static void __pci_tsm_disconnect(struct pci_dev *pdev)
* disconnect() is uninterruptible as it may be called for device
* teardown
*/
- guard(mutex)(&tsm_pf0->lock);
+ guard(mutex)(&tsm_host->lock);
pci_tsm_walk_fns_reverse(pdev, remove_fn, NULL);
ops->disconnect(pdev);
}
@@ -494,7 +494,7 @@ static ssize_t bound_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct pci_dev *pdev = to_pci_dev(dev);
- struct pci_tsm_pf0 *tsm_pf0;
+ struct pci_tsm_host *tsm_host;
struct pci_tsm *tsm;
int rc;
@@ -505,9 +505,9 @@ static ssize_t bound_show(struct device *dev,
tsm = pdev->tsm;
if (!tsm)
return sysfs_emit(buf, "\n");
- tsm_pf0 = to_pci_tsm_pf0(tsm);
+ tsm_host = to_pci_tsm_host(tsm);
- ACQUIRE(mutex_intr, ops_lock)(&tsm_pf0->lock);
+ ACQUIRE(mutex_intr, ops_lock)(&tsm_host->lock);
if ((rc = ACQUIRE_ERR(mutex_intr, &ops_lock)))
return rc;
@@ -547,7 +547,7 @@ static bool pci_tsm_link_group_visible(struct kobject *kobj)
if (!pci_is_pcie(pdev))
return false;
- if (is_pci_tsm_pf0(pdev))
+ if (is_pci_tsm_host(pdev))
return true;
/*
@@ -572,14 +572,14 @@ static umode_t pci_tsm_attr_visible(struct kobject *kobj,
struct pci_dev *pdev = to_pci_dev(kobj_to_dev(kobj));
if (attr == &dev_attr_bound.attr) {
- if (is_pci_tsm_pf0(pdev) && has_tee(pdev))
+ if (is_pci_tsm_host(pdev) && has_tee(pdev))
return attr->mode;
if (pdev->tsm && has_tee(pdev->tsm->dsm_dev))
return attr->mode;
}
if (attr == &dev_attr_dsm.attr) {
- if (is_pci_tsm_pf0(pdev))
+ if (is_pci_tsm_host(pdev))
return attr->mode;
if (pdev->tsm && has_tee(pdev->tsm->dsm_dev))
return attr->mode;
@@ -587,7 +587,7 @@ static umode_t pci_tsm_attr_visible(struct kobject *kobj,
if (attr == &dev_attr_connect.attr ||
attr == &dev_attr_disconnect.attr) {
- if (is_pci_tsm_pf0(pdev))
+ if (is_pci_tsm_host(pdev))
return attr->mode;
}
}
@@ -662,7 +662,7 @@ static struct pci_dev *find_dsm_dev(struct pci_dev *pdev)
struct device *grandparent;
struct pci_dev *uport;
- if (is_pci_tsm_pf0(pdev))
+ if (is_pci_tsm_host(pdev))
return pdev;
struct pci_dev *pf0 __free(pci_dev_put) = pf0_dev_get(pdev);
@@ -735,13 +735,13 @@ int pci_tsm_link_constructor(struct pci_dev *pdev, struct pci_tsm *tsm,
EXPORT_SYMBOL_GPL(pci_tsm_link_constructor);
/**
- * pci_tsm_pf0_constructor() - common 'struct pci_tsm_pf0' (DSM) initialization
- * @pdev: Physical Function 0 PCI device (as indicated by is_pci_tsm_pf0())
+ * pci_tsm_host_constructor() - common 'struct pci_tsm_host' (DSM) initialization
+ * @pdev: TSM host PCI device (as indicated by is_pci_tsm_host())
* @tsm: context to initialize
* @tsm_dev: Platform TEE Security Manager, initiator of security operations
*/
-int pci_tsm_pf0_constructor(struct pci_dev *pdev, struct pci_tsm_pf0 *tsm,
- struct tsm_dev *tsm_dev)
+int pci_tsm_host_constructor(struct pci_dev *pdev, struct pci_tsm_host *tsm,
+ struct tsm_dev *tsm_dev)
{
mutex_init(&tsm->lock);
tsm->doe_mb = pci_find_doe_mailbox(pdev, PCI_VENDOR_ID_PCI_SIG,
@@ -753,13 +753,13 @@ int pci_tsm_pf0_constructor(struct pci_dev *pdev, struct pci_tsm_pf0 *tsm,
return pci_tsm_link_constructor(pdev, &tsm->base_tsm, tsm_dev);
}
-EXPORT_SYMBOL_GPL(pci_tsm_pf0_constructor);
+EXPORT_SYMBOL_GPL(pci_tsm_host_constructor);
-void pci_tsm_pf0_destructor(struct pci_tsm_pf0 *pf0_tsm)
+void pci_tsm_host_destructor(struct pci_tsm_host *host_tsm)
{
- mutex_destroy(&pf0_tsm->lock);
+ mutex_destroy(&host_tsm->lock);
}
-EXPORT_SYMBOL_GPL(pci_tsm_pf0_destructor);
+EXPORT_SYMBOL_GPL(pci_tsm_host_destructor);
int pci_tsm_register(struct tsm_dev *tsm_dev)
{
@@ -780,7 +780,7 @@ int pci_tsm_register(struct tsm_dev *tsm_dev)
/* On first enable, update sysfs groups */
if (is_link_tsm(tsm_dev) && pci_tsm_link_count++ == 0) {
for_each_pci_dev(pdev)
- if (is_pci_tsm_pf0(pdev))
+ if (is_pci_tsm_host(pdev))
link_sysfs_enable(pdev);
} else if (is_devsec_tsm(tsm_dev)) {
pci_tsm_devsec_count++;
@@ -815,7 +815,7 @@ static void __pci_tsm_destroy(struct pci_dev *pdev, struct tsm_dev *tsm_dev)
* skipped if the device itself is being removed since sysfs goes away
* naturally at that point
*/
- if (is_link_tsm(tsm_dev) && is_pci_tsm_pf0(pdev) && !pci_tsm_link_count)
+ if (is_link_tsm(tsm_dev) && is_pci_tsm_host(pdev) && !pci_tsm_link_count)
link_sysfs_disable(pdev);
/* Nothing else to do if this device never attached to the departing TSM */
@@ -828,7 +828,7 @@ static void __pci_tsm_destroy(struct pci_dev *pdev, struct tsm_dev *tsm_dev)
else if (tsm_dev != tsm->tsm_dev)
return;
- if (is_link_tsm(tsm_dev) && is_pci_tsm_pf0(pdev))
+ if (is_link_tsm(tsm_dev) && is_pci_tsm_host(pdev))
pci_tsm_disconnect(pdev);
else
pci_tsm_fn_exit(pdev);
@@ -885,12 +885,12 @@ void pci_tsm_unregister(struct tsm_dev *tsm_dev)
int pci_tsm_doe_transfer(struct pci_dev *pdev, u8 type, const void *req,
size_t req_sz, void *resp, size_t resp_sz)
{
- struct pci_tsm_pf0 *tsm;
+ struct pci_tsm_host *tsm;
- if (!pdev->tsm || !is_pci_tsm_pf0(pdev))
+ if (!pdev->tsm || !is_pci_tsm_host(pdev))
return -ENXIO;
- tsm = to_pci_tsm_pf0(pdev->tsm);
+ tsm = to_pci_tsm_host(pdev->tsm);
if (!tsm->doe_mb)
return -ENXIO;
diff --git a/include/linux/pci-tsm.h b/include/linux/pci-tsm.h
index a6435aba03f9b..950e2c36a4cae 100644
--- a/include/linux/pci-tsm.h
+++ b/include/linux/pci-tsm.h
@@ -40,7 +40,7 @@ struct pci_tsm_ops {
* pci_tsm_rwsem held for write to sync with TSM unregistration and
* mutual exclusion of @connect and @disconnect. @connect and
* @disconnect additionally run under the DSM lock (struct
- * pci_tsm_pf0::lock) as well as @probe and @remove of the subfunctions.
+ * pci_tsm_host::lock) as well as @probe and @remove of the subfunctions.
* @bind, @unbind, and @guest_req run under pci_tsm_rwsem held for read
* and the DSM lock.
*/
@@ -115,19 +115,23 @@ struct pci_tsm {
};
/**
- * struct pci_tsm_pf0 - Physical Function 0 TDISP link context
+ * struct pci_tsm_host - TSM host link context (CMA, IDE, or TDISP)
* @base_tsm: generic core "tsm" context
* @lock: mutual exclustion for pci_tsm_ops invocation
* @doe_mb: PCIe Data Object Exchange mailbox
+ *
+ * The host of a TSM link is the device that knows how to speak
+ * CMA, IDE, or TDISP. For TDISP / IDE that is a Physical Function 0.
+ * For CMA-only it is a CMA DOE mailbox.
*/
-struct pci_tsm_pf0 {
+struct pci_tsm_host {
struct pci_tsm base_tsm;
struct mutex lock;
struct pci_doe_mb *doe_mb;
};
/* physical function0 and capable of 'connect' */
-static inline bool is_pci_tsm_pf0(struct pci_dev *pdev)
+static inline bool is_pci_tsm_host(struct pci_dev *pdev)
{
if (!pdev)
return false;
@@ -204,9 +208,9 @@ int pci_tsm_register(struct tsm_dev *tsm_dev);
void pci_tsm_unregister(struct tsm_dev *tsm_dev);
int pci_tsm_link_constructor(struct pci_dev *pdev, struct pci_tsm *tsm,
struct tsm_dev *tsm_dev);
-int pci_tsm_pf0_constructor(struct pci_dev *pdev, struct pci_tsm_pf0 *tsm,
- struct tsm_dev *tsm_dev);
-void pci_tsm_pf0_destructor(struct pci_tsm_pf0 *tsm);
+int pci_tsm_host_constructor(struct pci_dev *pdev, struct pci_tsm_host *tsm,
+ struct tsm_dev *tsm_dev);
+void pci_tsm_host_destructor(struct pci_tsm_host *tsm);
int pci_tsm_doe_transfer(struct pci_dev *pdev, u8 type, const void *req,
size_t req_sz, void *resp, size_t resp_sz);
int pci_tsm_bind(struct pci_dev *pdev, struct kvm *kvm, u32 tdi_id);
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* [PATCH v5 11/23] PCI/TSM: Support connecting to PCIe CMA devices
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (9 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 10/23] PCI/TSM: Rename pf0 to host alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:50 ` sashiko-bot
2026-10-27 5:25 ` [PATCH v5 12/23] PCI/CMA: Add a PCI TSM CMA driver using SPDM alistair23
` (11 subsequent siblings)
22 siblings, 1 reply; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Alistair Francis <alistair.francis@wdc.com>
In the next patch we are going to add a PCIe CMA TSM driver, as such we
need to ensure that is_pci_tsm_host() will allow us to connect to CMA
capable devices. These devices don't necessarily has DEVCAP_TEE or IDE
support.
To avoid calling pci_find_doe_mailbox() everytime is_pci_tsm_host() is
called we can cache the CMA support in struct pci_dev and just check
against that.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
drivers/crypto/ccp/sev-dev-tsm.c | 4 ++--
drivers/pci/doe.c | 3 +++
drivers/pci/tsm.c | 38 +++++++++++++++++++++++++++-----
include/linux/pci-tsm.h | 32 +++++++++++++++++++++++++--
include/linux/pci.h | 1 +
5 files changed, 68 insertions(+), 10 deletions(-)
diff --git a/drivers/crypto/ccp/sev-dev-tsm.c b/drivers/crypto/ccp/sev-dev-tsm.c
index 1d2561628f432..22f2855f397bd 100644
--- a/drivers/crypto/ccp/sev-dev-tsm.c
+++ b/drivers/crypto/ccp/sev-dev-tsm.c
@@ -226,7 +226,7 @@ static struct pci_tsm *dsm_probe(struct tsm_dev *tsmdev, struct pci_dev *pdev)
{
struct sev_device *sev = tsm_dev_to_sev(tsmdev);
- if (is_pci_tsm_host(pdev))
+ if (is_pci_tsm_dsm(pdev))
return tio_host_probe(pdev, sev);
return NULL;
}
@@ -237,7 +237,7 @@ static void dsm_remove(struct pci_tsm *tsm)
pci_dbg(pdev, "TSM disabled\n");
- if (is_pci_tsm_host(pdev)) {
+ if (is_pci_tsm_dsm(pdev)) {
struct tio_dsm *dsm = container_of(tsm, struct tio_dsm, tsm.base_tsm);
pci_tsm_host_destructor(&dsm->tsm);
diff --git a/drivers/pci/doe.c b/drivers/pci/doe.c
index 6b5d9ba8e9697..365634fb0df7a 100644
--- a/drivers/pci/doe.c
+++ b/drivers/pci/doe.c
@@ -871,6 +871,9 @@ void pci_doe_init(struct pci_dev *pdev)
pci_doe_destroy_mb(doe_mb);
}
}
+
+ pdev->doe_cma = pci_find_doe_mailbox(pdev, PCI_VENDOR_ID_PCI_SIG,
+ PCI_DOE_FEATURE_CMA);
}
void pci_doe_destroy(struct pci_dev *pdev)
diff --git a/drivers/pci/tsm.c b/drivers/pci/tsm.c
index 10c9c6696624d..03a84630bd69d 100644
--- a/drivers/pci/tsm.c
+++ b/drivers/pci/tsm.c
@@ -88,8 +88,8 @@ static void pci_tsm_walk_fns(struct pci_dev *pdev,
if (!pf)
continue;
- /* on entry function 0 has already run @cb */
- if (i > 0)
+ /* the caller is responsible for running @cb on the host itself */
+ if (pf != pdev)
cb(pf, data);
/* walk virtual functions of each pf */
@@ -145,8 +145,8 @@ static void pci_tsm_walk_fns_reverse(struct pci_dev *pdev,
cb(vf, data);
}
- /* on exit, caller will run @cb on function 0 */
- if (i > 0)
+ /* the caller is responsible for running @cb on the host itself */
+ if (pf != pdev)
cb(pf, data);
}
}
@@ -175,6 +175,9 @@ static int probe_fn(struct pci_dev *pdev, void *dsm)
struct pci_dev *dsm_dev = dsm;
const struct pci_tsm_ops *ops = to_pci_tsm_ops(dsm_dev->tsm);
+ if (pdev->tsm)
+ return 0;
+
pdev->tsm = ops->probe(dsm_dev->tsm->tsm_dev, pdev);
pci_dbg(pdev, "setup TSM context: DSM: %s status: %s\n",
pci_name(dsm_dev), pdev->tsm ? "success" : "failed");
@@ -287,6 +290,19 @@ static DEVICE_ATTR_RW(connect);
static int remove_fn(struct pci_dev *pdev, void *data)
{
+ struct pci_dev *host = data;
+ struct pci_tsm_host *tsm_host;
+
+ /*
+ * Only teardown the security context of functions that belong to the
+ * DSM being disconnected, leaving any sibling DSM in the same slot
+ * untouched.
+ */
+ if (!pdev->tsm || pdev->tsm->dsm_dev != host)
+ return 0;
+
+ tsm_host = to_pci_tsm_host(pdev->tsm);
+ guard(mutex)(&tsm_host->lock);
tsm_remove(pdev->tsm);
link_sysfs_disable(pdev);
return 0;
@@ -299,6 +315,7 @@ static int remove_fn(struct pci_dev *pdev, void *data)
*/
static int __pci_tsm_unbind(struct pci_dev *pdev, void *data)
{
+ struct pci_dev *host = data;
struct pci_tdi *tdi;
struct pci_tsm_host *tsm_host;
@@ -307,6 +324,14 @@ static int __pci_tsm_unbind(struct pci_dev *pdev, void *data)
if (!pdev->tsm)
return 0;
+ /*
+ * When walking a DSM's dependent functions skip any that belong to a
+ * different DSM in the same slot. A NULL @host is a direct unbind of
+ * @pdev itself.
+ */
+ if (host && pdev->tsm->dsm_dev != host)
+ return 0;
+
tsm_host = to_pci_tsm_host(pdev->tsm);
guard(mutex)(&tsm_host->lock);
@@ -437,7 +462,7 @@ EXPORT_SYMBOL_GPL(pci_tsm_guest_req);
static void pci_tsm_unbind_all(struct pci_dev *pdev)
{
- pci_tsm_walk_fns_reverse(pdev, __pci_tsm_unbind, NULL);
+ pci_tsm_walk_fns_reverse(pdev, __pci_tsm_unbind, pdev);
__pci_tsm_unbind(pdev, NULL);
}
@@ -451,12 +476,13 @@ static void __pci_tsm_disconnect(struct pci_dev *pdev)
pci_tsm_unbind_all(pdev);
+ pci_tsm_walk_fns_reverse(pdev, remove_fn, pdev);
+
/*
* disconnect() is uninterruptible as it may be called for device
* teardown
*/
guard(mutex)(&tsm_host->lock);
- pci_tsm_walk_fns_reverse(pdev, remove_fn, NULL);
ops->disconnect(pdev);
}
diff --git a/include/linux/pci-tsm.h b/include/linux/pci-tsm.h
index 950e2c36a4cae..ddaec2168c7e9 100644
--- a/include/linux/pci-tsm.h
+++ b/include/linux/pci-tsm.h
@@ -3,6 +3,7 @@
#define __PCI_TSM_H
#include <linux/mutex.h>
#include <linux/pci.h>
+#include <linux/pci-doe.h>
#include <linux/sockptr.h>
struct pci_tsm;
@@ -130,8 +131,11 @@ struct pci_tsm_host {
struct pci_doe_mb *doe_mb;
};
-/* physical function0 and capable of 'connect' */
-static inline bool is_pci_tsm_host(struct pci_dev *pdev)
+
+/* device is a Device Security Manager (DSM), similar to
+ * is_pci_tsm_host() but doesnt' support CMA
+ */
+static inline bool is_pci_tsm_dsm(struct pci_dev *pdev)
{
if (!pdev)
return false;
@@ -163,6 +167,30 @@ static inline bool is_pci_tsm_host(struct pci_dev *pdev)
return PCI_FUNC(pdev->devfn) == 0;
}
+/* device is a TSM host and capable of 'connect' */
+static inline bool is_pci_tsm_host(struct pci_dev *pdev)
+{
+ if (!pdev)
+ return false;
+
+ if (!pci_is_pcie(pdev))
+ return false;
+
+ if (pdev->is_virtfn)
+ return false;
+
+ /*
+ * Report capable if CMA is supported, which can be supported on any PCIe
+ * device.
+ */
+#ifdef CONFIG_PCI_DOE
+ if (pdev->doe_cma)
+ return true;
+#endif
+
+ return is_pci_tsm_dsm(pdev);
+}
+
/**
* enum pci_tsm_req_scope - Scope of guest requests to be validated by TSM
*
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 97a1096d5a8ac..26eeb43e34a12 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -566,6 +566,7 @@ struct pci_dev {
#endif
#ifdef CONFIG_PCI_DOE
struct xarray doe_mbs; /* Data Object Exchange mailboxes */
+ bool doe_cma; /* A CMA/SPDM DOE mailbox is present */
#endif
#ifdef CONFIG_PCI_NPEM
struct npem *npem; /* Native PCIe Enclosure Management */
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* Re: [PATCH v5 11/23] PCI/TSM: Support connecting to PCIe CMA devices
2026-10-27 5:25 ` [PATCH v5 11/23] PCI/TSM: Support connecting to PCIe CMA devices alistair23
@ 2026-10-27 5:50 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-10-27 5:50 UTC (permalink / raw)
To: alistair23, linux-pci, linux-cxl; +Cc: ojeda, gary
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] PCI/TSM: probe_fn() probes sibling TSM hosts without calling connect()
- [High] PCI/TSM: find_dsm_dev() misidentifies CMA subfunctions as their own DSM
--
commit 42f160df056909252113f2ee2453b7fc4a07f4ab
Author: Alistair Francis <alistair.francis@wdc.com>
PCI/TSM: Support connecting to PCIe CMA devices
This commit caches PCIe CMA DOE mailbox support in struct pci_dev, splits
the existing DSM check into is_pci_tsm_dsm(), and updates is_pci_tsm_host()
and function walks in drivers/pci/tsm.c so PCIe CMA devices can connect to
a TSM driver.
> diff --git a/drivers/pci/tsm.c b/drivers/pci/tsm.c
> index 10c9c6696624d..03a84630bd69d 100644
> --- a/drivers/pci/tsm.c
> +++ b/drivers/pci/tsm.c
> @@ -88,8 +88,8 @@ static void pci_tsm_walk_fns(struct pci_dev *pdev,
> if (!pf)
> continue;
>
> - /* on entry function 0 has already run @cb */
> - if (i > 0)
> + /* the caller is responsible for running @cb on the host itself */
> + if (pf != pdev)
> cb(pf, data);
>
> /* walk virtual functions of each pf */
[ ... ]
> @@ -175,6 +175,9 @@ static int probe_fn(struct pci_dev *pdev, void *dsm)
> struct pci_dev *dsm_dev = dsm;
> const struct pci_tsm_ops *ops = to_pci_tsm_ops(dsm_dev->tsm);
>
> + if (pdev->tsm)
> + return 0;
> +
> pdev->tsm = ops->probe(dsm_dev->tsm->tsm_dev, pdev);
> pci_dbg(pdev, "setup TSM context: DSM: %s status: %s\n",
> pci_name(dsm_dev), pdev->tsm ? "success" : "failed");
> @@ -287,6 +290,19 @@ static DEVICE_ATTR_RW(connect);
>
> static int remove_fn(struct pci_dev *pdev, void *data)
> {
> + struct pci_dev *host = data;
> + struct pci_tsm_host *tsm_host;
> +
> + /*
> + * Only teardown the security context of functions that belong to the
> + * DSM being disconnected, leaving any sibling DSM in the same slot
> + * untouched.
> + */
> + if (!pdev->tsm || pdev->tsm->dsm_dev != host)
> + return 0;
> +
> + tsm_host = to_pci_tsm_host(pdev->tsm);
> + guard(mutex)(&tsm_host->lock);
> tsm_remove(pdev->tsm);
> link_sysfs_disable(pdev);
> return 0;
[Severity: High]
When connecting a function that has PCI_EXP_DEVCAP_TEE set, can
pci_tsm_connect() -> pci_tsm_walk_fns() -> probe_fn() end up probing
unconnected sibling TSM hosts without calling ops->connect()?
In pci_tsm_connect(), pci_tsm_walk_fns() is invoked whenever has_tee(pdev)
is true:
drivers/pci/tsm.c:pci_tsm_connect() {
...
if (has_tee(pdev))
pci_tsm_walk_fns(pdev, probe_fn, pdev);
return 0;
}
Because is_pci_tsm_host() now returns true for any non-VF PCIe function with
pdev->doe_cma set, and pci_tsm_walk_fns() visits every physical function in
the slot where pf != pdev, connecting either PF0 to a CMA TSM driver or
connecting a non-PF0 CMA host that has PCI_EXP_DEVCAP_TEE set will call
probe_fn(pf, pdev) on all other physical functions in the slot (as well as
downstream devices when pdev is an upstream port DSM).
In probe_fn(), only functions with pdev->tsm already set are skipped. For an
unconnected sibling host (pf->tsm == NULL), ops->probe() is called and
initializes a struct pci_tsm_host via:
pci_tsm_host_constructor()
-> pci_tsm_link_constructor()
-> find_dsm_dev(pf)
Because is_pci_tsm_host(pf) is true, find_dsm_dev(pf) returns pf itself, so
pf->tsm->dsm_dev is set to pf without ops->connect(pf) ever being called.
Wouldn't this leave the sibling function with a populated pf->tsm where:
1. authenticated_show() and connect_show() report the sibling function as
authenticated and connected without an established SPDM session, and
connect_store() rejects subsequent connection attempts with -EBUSY.
2. Disconnecting the original host skips the sibling in remove_fn() because
pf->tsm->dsm_dev != host, leaking pf->tsm.
3. Later disconnecting or removing the sibling invokes
__pci_tsm_disconnect() -> ops->disconnect() on a device where
ops->connect() was never called.
Should pci_tsm_connect() only walk subordinate functions when the connected
device is acting as a TDISP DSM, and should probe_fn() avoid probing
independent TSM hosts as subordinate functions?
> diff --git a/include/linux/pci-tsm.h b/include/linux/pci-tsm.h
> index 950e2c36a4cae..ddaec2168c7e9 100644
> --- a/include/linux/pci-tsm.h
> +++ b/include/linux/pci-tsm.h
[ ... ]
> @@ -163,6 +167,30 @@ static inline bool is_pci_tsm_host(struct pci_dev *pdev)
> return PCI_FUNC(pdev->devfn) == 0;
> }
>
> +/* device is a TSM host and capable of 'connect' */
> +static inline bool is_pci_tsm_host(struct pci_dev *pdev)
> +{
> + if (!pdev)
> + return false;
> +
> + if (!pci_is_pcie(pdev))
> + return false;
> +
> + if (pdev->is_virtfn)
> + return false;
> +
> + /*
> + * Report capable if CMA is supported, which can be supported on any PCIe
> + * device.
> + */
> +#ifdef CONFIG_PCI_DOE
> + if (pdev->doe_cma)
> + return true;
> +#endif
> +
> + return is_pci_tsm_dsm(pdev);
> +}
[Severity: High]
How does find_dsm_dev() behave when a subordinate physical function
(PF1..PF7) or a downstream endpoint has a CMA DOE mailbox (pdev->doe_cma ==
true), while PF0 or an upstream switch port is acting as the TDISP DSM?
In drivers/pci/tsm.c, find_dsm_dev() checks is_pci_tsm_host(pdev) first:
drivers/pci/tsm.c:find_dsm_dev() {
if (is_pci_tsm_host(pdev))
return pdev;
struct pci_dev *pf0 __free(pci_dev_put) = pf0_dev_get(pdev);
if (!pf0)
return NULL;
if (is_dsm(pf0))
return pf0;
...
}
Because is_pci_tsm_host(pdev) now returns true for any non-VF PCIe function
with pdev->doe_cma, find_dsm_dev(pdev) will unconditionally return pdev
itself before checking whether PF0 or an upstream port has an active DSM
context (is_dsm(pf0) or is_dsm(uport)):
1. In pci_tsm_init(pdev), dsm = find_dsm_dev(pdev) returns pdev instead of
PF0 or uport, so if (!dsm->tsm) returns early and fails to attach the
subfunction to the connected DSM.
2. When a TDISP DSM driver probes a subordinate function via probe_fn() ->
ops->probe() -> pci_tsm_link_constructor(pdev, tsm, tsm_dev), where tsm
is a base struct pci_tsm rather than a struct pci_tsm_host,
find_dsm_dev(pdev) sets tsm->dsm_dev = pdev. That makes both
is_dsm(pdev) and is_pci_tsm_host(pdev) true, causing to_pci_tsm_host()
to perform an invalid container_of(host->tsm, struct pci_tsm_host,
base_tsm) on a non-host struct pci_tsm and access out-of-bounds memory
when acquiring &tsm_host->lock in remove_fn(), __pci_tsm_unbind(),
pci_tsm_bind(), pci_tsm_guest_req(), and bound_show().
3. During DSM disconnect, remove_fn() and __pci_tsm_unbind() skip the
subordinate function because pdev->tsm->dsm_dev != host, leaking its TSM
context.
4. In __pci_tsm_destroy(), the check:
if (is_link_tsm(tsm_dev) && is_pci_tsm_host(pdev))
pci_tsm_disconnect(pdev);
else
pci_tsm_fn_exit(pdev);
will call pci_tsm_disconnect(pdev) -> ops->disconnect(pdev) instead of
pci_tsm_fn_exit(pdev) on a subordinate function just because it has
pdev->doe_cma set.
Should find_dsm_dev() and __pci_tsm_destroy() distinguish between a device
that was actually initialized as a host/DSM (such as via is_dsm()) versus a
subordinate function that happens to have a CMA DOE mailbox?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027052536.1722081-1-alistair.francis@wdc.com?part=11
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH v5 12/23] PCI/CMA: Add a PCI TSM CMA driver using SPDM
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (10 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 11/23] PCI/TSM: Support connecting to PCIe CMA devices alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:46 ` sashiko-bot
2026-10-27 5:25 ` [PATCH v5 13/23] PCI/CMA: Validate Subject Alternative Name in certificates alistair23
` (10 subsequent siblings)
22 siblings, 1 reply; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis, Jonathan Cameron
From: Alistair Francis <alistair.francis@wdc.com>
Component Measurement and Authentication (CMA, PCIe r6.2 sec 6.31)
allows for measurement and authentication of PCIe devices. It is
based on the Security Protocol and Data Model specification (SPDM,
https://www.dmtf.org/dsp/DSP0274).
CMA-SPDM in turn forms the basis for Integrity and Data Encryption
(IDE, PCIe r6.2 sec 6.33) because the key material used by IDE is
transmitted over a CMA-SPDM session.
As a first step, add support for authentication via a CMA TSM driver.
This was previously discusd here:
http://lore.kernel.org/69976d7d39c60_2f4a1009@dwillia2-mobl4.notmuch
By utilising a TSM driver we get a lot of the TSM driver probe policies
"for free". Currently there is no mechanism to provide evidence to
userspace, as the TSM system doesn't support that at the moment. That
can be added later when support by TSM.
Credits: Jonathan wrote the original proof-of-concept for a CMA implementation.
Lukas reworked that for upstream. Wilfred contributed fixes for issues
discovered during testing. Alistair reworked it as a TSM driver.
Signed-off-by: Jonathan Cameron <Jonathan.Cameron@huawei.com>
Co-developed-by: Wilfred Mallawa <wilfred.mallawa@wdc.com>
Signed-off-by: Wilfred Mallawa <wilfred.mallawa@wdc.com>
Co-developed-by: Lukas Wunner <lukas@wunner.de>
Signed-off-by: Lukas Wunner <lukas@wunner.de>
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
MAINTAINERS | 1 +
drivers/pci/Kconfig | 14 ++++
drivers/pci/Makefile | 2 +
drivers/pci/cma.c | 150 ++++++++++++++++++++++++++++++++++++++++
drivers/pci/doe.c | 3 -
include/linux/pci-doe.h | 4 ++
6 files changed, 171 insertions(+), 3 deletions(-)
create mode 100644 drivers/pci/cma.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 783da0b234541..5140ed5cc108b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25299,6 +25299,7 @@ L: linux-cxl@vger.kernel.org
L: linux-pci@vger.kernel.org
S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/devsec/spdm.git
+F: drivers/pci/cma.c
F: include/linux/spdm.h
F: lib/rspdm/
diff --git a/drivers/pci/Kconfig b/drivers/pci/Kconfig
index 0c7408509ba24..0862ef69aa3fd 100644
--- a/drivers/pci/Kconfig
+++ b/drivers/pci/Kconfig
@@ -124,6 +124,20 @@ config PCI_ATS
config PCI_IDE
bool
+config PCI_CMA
+ bool "Component Measurement and Authentication (CMA-SPDM)"
+ depends on RSPDM
+ select CRYPTO_ECDSA
+ select CRYPTO_RSA
+ select CRYPTO_SHA256
+ select CRYPTO_SHA512
+ select PCI_DOE
+ select PCI_TSM
+ help
+ Authenticate devices on enumeration per PCIe r6.2 sec 6.31.
+ A PCI DOE mailbox is used as transport for DMTF SPDM based
+ authentication, measurement and secure channel establishment.
+
config PCI_TSM
bool "PCI TSM: Device security protocol support"
select PCI_IDE
diff --git a/drivers/pci/Makefile b/drivers/pci/Makefile
index 41ebc3b9a5182..16abfd0e17e19 100644
--- a/drivers/pci/Makefile
+++ b/drivers/pci/Makefile
@@ -41,6 +41,8 @@ obj-$(CONFIG_PCI_NPEM) += npem.o
obj-$(CONFIG_PCIE_TPH) += tph.o
obj-$(CONFIG_CARDBUS) += setup-cardbus.o
+obj-$(CONFIG_PCI_CMA) += cma.o
+
# Endpoint library must be initialized before its users
obj-$(CONFIG_PCI_ENDPOINT) += endpoint/
diff --git a/drivers/pci/cma.c b/drivers/pci/cma.c
new file mode 100644
index 0000000000000..6b52b129574ec
--- /dev/null
+++ b/drivers/pci/cma.c
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Component Measurement and Authentication (CMA-SPDM, PCIe r6.2 sec 6.31)
+ *
+ * Copyright (C) 2021 Huawei
+ * Jonathan Cameron <Jonathan.Cameron@huawei.com>
+ * Copyright (C) 2022-24 Intel Corporation
+ * Copyright (C) 2026 Western Digital
+ * Alistair Francis <alistair.francis@wdc.com>
+ */
+
+#define dev_fmt(fmt) "CMA: " fmt
+
+#include <linux/err.h>
+#include <linux/pci.h>
+#include <linux/pci-doe.h>
+#include <linux/pci-tsm.h>
+#include <linux/pm_runtime.h>
+#include <linux/slab.h>
+#include <linux/spdm.h>
+#include <linux/tsm.h>
+
+#include "pci.h"
+
+static int pci_doe_transport(void *priv, struct device *dev,
+ const void *request, size_t request_sz,
+ void *response, size_t response_sz)
+{
+ struct pci_doe_mb *doe = priv;
+
+ return pci_doe(doe, PCI_VENDOR_ID_PCI_SIG, PCI_DOE_FEATURE_CMA,
+ request, request_sz, response, response_sz);
+}
+
+struct pci_cma_tsm {
+ struct pci_tsm_host host;
+ struct spdm_state *spdm;
+};
+
+static struct pci_cma_tsm *cma_tsm_from_tsm(struct pci_tsm *tsm)
+{
+ struct pci_tsm_host *host = container_of(tsm, struct pci_tsm_host, base_tsm);
+
+ return container_of(host, struct pci_cma_tsm, host);
+}
+
+static struct pci_tsm *pci_cma_tsm_probe(struct tsm_dev *tsm_dev,
+ struct pci_dev *pdev)
+{
+ struct pci_cma_tsm *cma;
+ int rc;
+
+ if (!pdev->doe_cma)
+ return NULL;
+
+ cma = kzalloc(sizeof(*cma), GFP_KERNEL);
+ if (!cma)
+ return NULL;
+
+ rc = pci_tsm_host_constructor(pdev, &cma->host, tsm_dev);
+ if (rc) {
+ kfree(cma);
+ return NULL;
+ }
+
+ cma->spdm = spdm_create(&pdev->dev, pci_doe_transport, cma->host.doe_mb,
+ PCI_DOE_MAX_PAYLOAD, NULL);
+ if (!cma->spdm) {
+ pci_tsm_host_destructor(&cma->host);
+ kfree(cma);
+ return NULL;
+ }
+
+ return &cma->host.base_tsm;
+}
+
+static void pci_cma_tsm_remove(struct pci_tsm *tsm)
+{
+ struct pci_cma_tsm *cma = cma_tsm_from_tsm(tsm);
+
+ spdm_destroy(cma->spdm);
+ pci_tsm_host_destructor(&cma->host);
+ kfree(cma);
+}
+
+static int pci_cma_tsm_connect(struct pci_dev *pdev)
+{
+ struct pci_cma_tsm *cma = cma_tsm_from_tsm(pdev->tsm);
+
+ /*
+ * The DOE mailbox lives in the device's config space, so the device
+ * must be runtime-resumed for the duration of the SPDM exchange.
+ */
+ PM_RUNTIME_ACQUIRE(&pdev->dev, pm);
+ if (PM_RUNTIME_ACQUIRE_ERR(&pm))
+ return -ENXIO;
+
+ return spdm_authenticate(cma->spdm);
+}
+
+static void pci_cma_tsm_disconnect(struct pci_dev *pdev)
+{
+ /* SPDM state is freed in pci_cma_tsm_remove() */
+}
+
+static struct pci_tdi *pci_cma_tsm_bind(struct pci_dev *pdev,
+ struct kvm *kvm, u32 tdi_id)
+{
+ return ERR_PTR(-EOPNOTSUPP);
+}
+
+static void pci_cma_tsm_unbind(struct pci_tdi *tdi)
+{
+}
+
+static ssize_t pci_cma_tsm_guest_req(struct pci_tdi *tdi,
+ enum pci_tsm_req_scope scope,
+ sockptr_t req_in, size_t in_len,
+ sockptr_t req_out, size_t out_len,
+ u64 *tsm_code)
+{
+ return -EOPNOTSUPP;
+}
+
+static const struct pci_tsm_ops pci_cma_tsm_ops = {
+ .link_ops = {
+ .probe = pci_cma_tsm_probe,
+ .remove = pci_cma_tsm_remove,
+ .connect = pci_cma_tsm_connect,
+ .disconnect = pci_cma_tsm_disconnect,
+ .bind = pci_cma_tsm_bind,
+ .unbind = pci_cma_tsm_unbind,
+ .guest_req = pci_cma_tsm_guest_req,
+ },
+};
+
+static struct tsm_dev *pci_cma_tsm_dev;
+
+static int __init pci_cma_tsm_init(void)
+{
+ struct tsm_dev *tsm_dev;
+
+ tsm_dev = tsm_register(NULL, &pci_cma_tsm_ops);
+ if (IS_ERR(tsm_dev))
+ return PTR_ERR(tsm_dev);
+
+ pci_cma_tsm_dev = tsm_dev;
+ return 0;
+}
+late_initcall(pci_cma_tsm_init);
diff --git a/drivers/pci/doe.c b/drivers/pci/doe.c
index 365634fb0df7a..5f79307e0f087 100644
--- a/drivers/pci/doe.c
+++ b/drivers/pci/doe.c
@@ -31,9 +31,6 @@
#define PCI_DOE_FLAG_CANCEL 0
#define PCI_DOE_FLAG_DEAD 1
-/* Max data object length is 2^18 dwords */
-#define PCI_DOE_MAX_LENGTH (1 << 18)
-
/**
* struct pci_doe_mb - State for a single DOE mailbox
*
diff --git a/include/linux/pci-doe.h b/include/linux/pci-doe.h
index bd4346a7c4e75..7540396336def 100644
--- a/include/linux/pci-doe.h
+++ b/include/linux/pci-doe.h
@@ -19,6 +19,10 @@ struct pci_doe_mb;
#define PCI_DOE_FEATURE_CMA 1
#define PCI_DOE_FEATURE_SSESSION 2
+/* Max data object length is 2^18 dwords (including 2 dwords for header) */
+#define PCI_DOE_MAX_LENGTH (1 << 18)
+#define PCI_DOE_MAX_PAYLOAD ((PCI_DOE_MAX_LENGTH - 2) * sizeof(u32))
+
struct pci_doe_mb *pci_find_doe_mailbox(struct pci_dev *pdev, u16 vendor,
u8 type);
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* Re: [PATCH v5 12/23] PCI/CMA: Add a PCI TSM CMA driver using SPDM
2026-10-27 5:25 ` [PATCH v5 12/23] PCI/CMA: Add a PCI TSM CMA driver using SPDM alistair23
@ 2026-10-27 5:46 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-10-27 5:46 UTC (permalink / raw)
To: alistair23, linux-pci, linux-cxl; +Cc: ojeda, gary
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] PCI/CMA: pci_cma_tsm_probe() initializes subfunctions without connect()
--
commit 422c44a59294e74ffe91f6512ceb7d64705e55f8
Author: Alistair Francis <alistair.francis@wdc.com>
PCI/CMA: Add a PCI TSM CMA driver using SPDM
This commit adds a PCI TSM driver for Component Measurement and
Authentication (CMA-SPDM) over a PCI DOE mailbox to support device
authentication.
> diff --git a/drivers/pci/cma.c b/drivers/pci/cma.c
> new file mode 100644
> index 0000000000000..6b52b129574ec
> --- /dev/null
> +++ b/drivers/pci/cma.c
[ ... ]
> +static struct pci_tsm *pci_cma_tsm_probe(struct tsm_dev *tsm_dev,
> + struct pci_dev *pdev)
> +{
> + struct pci_cma_tsm *cma;
> + int rc;
> +
> + if (!pdev->doe_cma)
> + return NULL;
> +
> + cma = kzalloc(sizeof(*cma), GFP_KERNEL);
> + if (!cma)
> + return NULL;
> +
> + rc = pci_tsm_host_constructor(pdev, &cma->host, tsm_dev);
> + if (rc) {
> + kfree(cma);
> + return NULL;
> + }
> +
> + cma->spdm = spdm_create(&pdev->dev, pci_doe_transport, cma->host.doe_mb,
> + PCI_DOE_MAX_PAYLOAD, NULL);
> + if (!cma->spdm) {
> + pci_tsm_host_destructor(&cma->host);
> + kfree(cma);
> + return NULL;
> + }
> +
> + return &cma->host.base_tsm;
> +}
[Severity: High]
Can pci_cma_tsm_probe() be invoked via probe_fn() for subordinate or
sibling PCI functions without pci_cma_tsm_connect() ever being called on
them?
In drivers/pci/tsm.c, ops->probe() is called not only on the target host
device in pci_tsm_connect(), but also via probe_fn() when walking
functions in pci_tsm_connect() (when has_tee(pdev) is true) or when SR-IOV
VFs are initialized in pci_tsm_init():
drivers/pci/tsm.c:probe_fn() {
if (pdev->tsm)
return 0;
pdev->tsm = ops->probe(dsm_dev->tsm->tsm_dev, pdev);
pci_dbg(pdev, "setup TSM context: DSM: %s status: %s\n",
pci_name(dsm_dev), pdev->tsm ? "success" : "failed");
if (pdev->tsm)
link_sysfs_enable(pdev);
return 0;
}
drivers/pci/tsm.c:pci_tsm_connect() {
...
if (has_tee(pdev))
pci_tsm_walk_fns(pdev, probe_fn, pdev);
return 0;
}
Because pci_cma_tsm_probe() only checks pdev->doe_cma, any walked sibling
PF, downstream device, or VF with doe_cma set will have cma->host and
cma->spdm allocated and assigned to pdev->tsm, followed by
link_sysfs_enable(), without pci_cma_tsm_connect() (and thus
spdm_authenticate()) running on that function.
Doesn't this cause a few issues for those walked functions:
1. In drivers/pci/tsm.c, authenticated_show() delegates to connect_show(),
which reports the TSM device name whenever pdev->tsm is non-NULL, falsely
reporting the unauthenticated function as authenticated in sysfs.
2. Any subsequent attempt by userspace to authenticate that function via
connect_store() will fail with -EBUSY because pdev->tsm is already
populated.
3. For sibling PFs and downstream endpoints where is_pci_tsm_host() is
true, pci_tsm_host_constructor() sets sub_pdev->tsm->dsm_dev = sub_pdev
via find_dsm_dev(). When the parent device later disconnects in
__pci_tsm_disconnect(), remove_fn() skips cleaning up sub_pdev:
drivers/pci/tsm.c:remove_fn() {
if (!pdev->tsm || pdev->tsm->dsm_dev != host)
return 0;
...
}
leaving the unauthenticated sub_pdev->tsm and cma->spdm leaked and active.
4. If a VF (pdev->is_virtfn) has doe_cma set, calling
pci_tsm_host_constructor() on it violates the is_pci_tsm_host()
expectation for struct pci_tsm_host.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027052536.1722081-1-alistair.francis@wdc.com?part=12
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH v5 13/23] PCI/CMA: Validate Subject Alternative Name in certificates
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (11 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 12/23] PCI/CMA: Add a PCI TSM CMA driver using SPDM alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:25 ` [PATCH v5 14/23] lib: rspdm: Support SPDM get_version alistair23
` (9 subsequent siblings)
22 siblings, 0 replies; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Jonathan Cameron, Alistair Francis
From: Lukas Wunner <lukas@wunner.de>
PCIe r6.1 sec 6.31.3 stipulates requirements for Leaf Certificates
presented by devices, in particular the presence of a Subject Alternative
Name which encodes the Vendor ID, Device ID, Device Serial Number, etc.
This prevents a mismatch between the device identity in Config Space and
the certificate. A device cannot misappropriate a certificate from a
different device without also spoofing Config Space. As a corollary,
it cannot dupe an arbitrary driver into binding to it. Only drivers
which bind to the device identity in the Subject Alternative Name work
(PCIe r6.1 sec 6.31 "Implementation Note: Overview of Threat Model").
The Subject Alternative Name is signed, hence constitutes a signed copy
of a Config Space portion. It's the same concept as web certificates
which contain a set of domain names in the Subject Alternative Name for
identity verification.
Parse the Subject Alternative Name using a small ASN.1 module and
validate its contents. The theory of operation is explained in a
comment at the top of the newly inserted code.
This functionality is introduced in a separate commit on top of basic
CMA-SPDM support to split the code into digestible, reviewable chunks.
The CMA OID added here is taken from the official OID Repository
(it's not documented in the PCIe Base Spec):
https://oid-rep.orange-labs.fr/get/2.23.147
Side notes:
* PCIe r6.2 removes the spec language on the Subject Alternative Name.
It still "requires the leaf certificate to include the information
typically used by system software for device driver binding", but no
longer specifies how that information is encoded into the certificate.
According to the editor of the PCIe Base Spec and the author of the
CMA 1.1 ECN (which caused this change), FPGA cards which mutate their
device identity at runtime (due to a firmware update) were thought as
unable to satisfy the previous spec language. The Protocol Working
Group could not agree on a better solution and therefore dropped the
spec language entirely. They acknowledge that the requirement is now
under-spec'd. Because products already exist which adhere to the
Subject Alternative Name requirement per PCIe r6.1 sec 6.31.3, they
recommended to "push through" and use it as the de facto standard.
The FPGA concerns are easily overcome by reauthenticating the device
after a firmware update, either via sysfs or pci_cma_reauthenticate()
(added by a subsequent commit).
* PCIe r6.1 sec 6.31.3 strongly recommends to verify that "the
information provided in the Subject Alternative Name entry is signed
by the vendor indicated by the Vendor ID." In other words, the root
certificate on pci_cma_keyring which signs the device's certificate
chain must have been created for a particular Vendor ID.
Unfortunately the spec neglects to define how the Vendor ID shall be
encoded into the root certificate. So the recommendation cannot be
implemented at this point and it is thus possible that a vendor signs
device certificates of a different vendor.
* Instead of a Subject Alternative Name, Leaf Certificates may include
"a Reference Integrity Manifest, e.g., see Trusted Computing Group" or
"a pointer to a location where such a Reference Integrity Manifest can
be obtained" (PCIe r6.1 sec 6.31.3).
A Reference Integrity Manifest contains "golden" measurements which
can be compared to actual measurements retrieved from a device.
It serves a different purpose than the Subject Alternative Name,
hence it is unclear why the spec says only either of them is necessary.
It is also unclear how a Reference Integrity Manifest shall be encoded
into a certificate.
Hence ignore the Reference Integrity Manifest requirement.
Signed-off-by: Lukas Wunner <lukas@wunner.de>
Reviewed-by: Jonathan Cameron <Jonathan.Cameron@huawei.com> # except ASN.1
[ Changed by AF:
- Fixup a few issues caught by Sashiko
]
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
drivers/pci/Makefile | 5 +-
drivers/pci/cma.asn1 | 41 ++++++++++++
drivers/pci/cma.c | 123 ++++++++++++++++++++++++++++++++++-
include/linux/oid_registry.h | 3 +
4 files changed, 170 insertions(+), 2 deletions(-)
create mode 100644 drivers/pci/cma.asn1
diff --git a/drivers/pci/Makefile b/drivers/pci/Makefile
index 16abfd0e17e19..882cbb1083646 100644
--- a/drivers/pci/Makefile
+++ b/drivers/pci/Makefile
@@ -41,7 +41,10 @@ obj-$(CONFIG_PCI_NPEM) += npem.o
obj-$(CONFIG_PCIE_TPH) += tph.o
obj-$(CONFIG_CARDBUS) += setup-cardbus.o
-obj-$(CONFIG_PCI_CMA) += cma.o
+obj-$(CONFIG_PCI_CMA) += pci-cma.o
+pci-cma-y := cma.o cma.asn1.o
+$(obj)/cma.o: $(obj)/cma.asn1.h
+$(obj)/cma.asn1.o: $(obj)/cma.asn1.c $(obj)/cma.asn1.h
# Endpoint library must be initialized before its users
obj-$(CONFIG_PCI_ENDPOINT) += endpoint/
diff --git a/drivers/pci/cma.asn1 b/drivers/pci/cma.asn1
new file mode 100644
index 0000000000000..da41421d40856
--- /dev/null
+++ b/drivers/pci/cma.asn1
@@ -0,0 +1,41 @@
+-- SPDX-License-Identifier: BSD-3-Clause
+--
+-- Component Measurement and Authentication (CMA-SPDM, PCIe r6.1 sec 6.31.3)
+-- X.509 Subject Alternative Name (RFC 5280 sec 4.2.1.6)
+--
+-- Copyright (C) 2008 IETF Trust and the persons identified as authors
+-- of the code
+--
+-- https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.6
+--
+-- The ASN.1 module in RFC 5280 appendix A.1 uses EXPLICIT TAGS whereas the one
+-- in appendix A.2 uses IMPLICIT TAGS. The kernel's simplified asn1_compiler.c
+-- always uses EXPLICIT TAGS, hence this ASN.1 module differs from RFC 5280 in
+-- that it adds IMPLICIT to definitions from appendix A.2 (such as GeneralName)
+-- and omits EXPLICIT in those definitions.
+
+SubjectAltName ::= GeneralNames
+
+GeneralNames ::= SEQUENCE OF GeneralName
+
+GeneralName ::= CHOICE {
+ otherName [0] IMPLICIT OtherName,
+ rfc822Name [1] IMPLICIT IA5String,
+ dNSName [2] IMPLICIT IA5String,
+ x400Address [3] ANY,
+ directoryName [4] ANY,
+ ediPartyName [5] IMPLICIT EDIPartyName,
+ uniformResourceIdentifier [6] IMPLICIT IA5String,
+ iPAddress [7] IMPLICIT OCTET STRING,
+ registeredID [8] IMPLICIT OBJECT IDENTIFIER
+ }
+
+OtherName ::= SEQUENCE {
+ type-id OBJECT IDENTIFIER ({ pci_cma_note_oid }),
+ value [0] ANY ({ pci_cma_note_san })
+ }
+
+EDIPartyName ::= SEQUENCE {
+ nameAssigner [0] ANY OPTIONAL,
+ partyName [1] ANY
+ }
diff --git a/drivers/pci/cma.c b/drivers/pci/cma.c
index 6b52b129574ec..6e4eb3b921131 100644
--- a/drivers/pci/cma.c
+++ b/drivers/pci/cma.c
@@ -12,6 +12,9 @@
#define dev_fmt(fmt) "CMA: " fmt
#include <linux/err.h>
+#include <keys/x509-parser.h>
+#include <linux/asn1_decoder.h>
+#include <linux/oid_registry.h>
#include <linux/pci.h>
#include <linux/pci-doe.h>
#include <linux/pci-tsm.h>
@@ -20,8 +23,126 @@
#include <linux/spdm.h>
#include <linux/tsm.h>
+#include "cma.asn1.h"
#include "pci.h"
+/*
+ * The spdm_requester.c library calls pci_cma_validate() to check requirements
+ * for Leaf Certificates per PCIe r6.1 sec 6.31.3.
+ *
+ * pci_cma_validate() parses the Subject Alternative Name using the ASN.1
+ * module cma.asn1, which calls pci_cma_note_oid() and pci_cma_note_san()
+ * to compare an OtherName against the expected name.
+ *
+ * The expected name is constructed beforehand by pci_cma_construct_san().
+ *
+ * PCIe r6.2 drops the Subject Alternative Name spec language, even though
+ * it continues to require "the leaf certificate to include the information
+ * typically used by system software for device driver binding". Use the
+ * Subject Alternative Name per PCIe r6.1 for lack of a replacement and
+ * because it is the de facto standard among existing products.
+ */
+#define CMA_NAME_MAX sizeof("Vendor=1234:Device=1234:CC=123456:" \
+ "REV=12:SSVID=1234:SSID=1234:1234567890123456")
+
+struct pci_cma_x509_context {
+ struct pci_dev *pdev;
+ u8 slot;
+ enum OID last_oid;
+ char expected_name[CMA_NAME_MAX];
+ unsigned int expected_len;
+ unsigned int found:1;
+};
+
+int pci_cma_note_oid(void *context, size_t hdrlen, unsigned char tag,
+ const void *value, size_t vlen)
+{
+ struct pci_cma_x509_context *ctx = context;
+
+ ctx->last_oid = look_up_OID(value, vlen);
+
+ return 0;
+}
+
+int pci_cma_note_san(void *context, size_t hdrlen, unsigned char tag,
+ const void *value, size_t vlen)
+{
+ struct pci_cma_x509_context *ctx = context;
+
+ /* These aren't the drOIDs we're looking for. */
+ if (ctx->last_oid != OID_CMA)
+ return 0;
+
+ if (tag != ASN1_UTF8STR ||
+ vlen != ctx->expected_len ||
+ memcmp(value, ctx->expected_name, vlen) != 0) {
+ pci_err(ctx->pdev, "Leaf certificate of slot %u "
+ "has invalid Subject Alternative Name\n", ctx->slot);
+ return -EINVAL;
+ }
+
+ ctx->found = true;
+
+ return 0;
+}
+
+static unsigned int pci_cma_construct_san(struct pci_dev *pdev, char *name)
+{
+ unsigned int len;
+ u64 serial;
+
+ len = scnprintf(name, CMA_NAME_MAX,
+ "Vendor=%04hx:Device=%04hx:CC=%06x:REV=%02hhx",
+ pdev->vendor, pdev->device, pdev->class, pdev->revision);
+
+ if (pdev->hdr_type == PCI_HEADER_TYPE_NORMAL)
+ len += scnprintf(name + len, CMA_NAME_MAX - len,
+ ":SSVID=%04hx:SSID=%04hx",
+ pdev->subsystem_vendor, pdev->subsystem_device);
+
+ serial = pci_get_dsn(pdev);
+ if (serial)
+ len += scnprintf(name + len, CMA_NAME_MAX - len,
+ ":%016llx", serial);
+
+ return len;
+}
+
+static int pci_cma_validate(struct device *dev, u8 slot,
+ struct x509_certificate *leaf_cert)
+{
+ struct pci_dev *pdev = to_pci_dev(dev);
+ struct pci_cma_x509_context ctx;
+ int ret;
+
+ if (!leaf_cert->raw_san) {
+ pci_err(pdev, "Leaf certificate of slot %u "
+ "has no Subject Alternative Name\n", slot);
+ return -EINVAL;
+ }
+
+ ctx.pdev = pdev;
+ ctx.slot = slot;
+ ctx.found = false;
+ ctx.expected_len = pci_cma_construct_san(pdev, ctx.expected_name);
+
+ ret = asn1_ber_decoder(&cma_decoder, &ctx, leaf_cert->raw_san,
+ leaf_cert->raw_san_size);
+ if (ret == -EBADMSG || ret == -EMSGSIZE)
+ pci_err(pdev, "Leaf certificate of slot %u "
+ "has malformed Subject Alternative Name\n", slot);
+ if (ret < 0)
+ return ret;
+
+ if (!ctx.found) {
+ pci_err(pdev, "Leaf certificate of slot %u "
+ "has no OtherName with CMA OID\n", slot);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
static int pci_doe_transport(void *priv, struct device *dev,
const void *request, size_t request_sz,
void *response, size_t response_sz)
@@ -64,7 +185,7 @@ static struct pci_tsm *pci_cma_tsm_probe(struct tsm_dev *tsm_dev,
}
cma->spdm = spdm_create(&pdev->dev, pci_doe_transport, cma->host.doe_mb,
- PCI_DOE_MAX_PAYLOAD, NULL);
+ PCI_DOE_MAX_PAYLOAD, pci_cma_validate);
if (!cma->spdm) {
pci_tsm_host_destructor(&cma->host);
kfree(cma);
diff --git a/include/linux/oid_registry.h b/include/linux/oid_registry.h
index ebce402854de4..113f4e802ec40 100644
--- a/include/linux/oid_registry.h
+++ b/include/linux/oid_registry.h
@@ -150,6 +150,9 @@ enum OID {
OID_id_ml_dsa_65, /* 2.16.840.1.101.3.4.3.18 */
OID_id_ml_dsa_87, /* 2.16.840.1.101.3.4.3.19 */
+ /* PCI */
+ OID_CMA, /* 2.23.147 */
+
OID__NR
};
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* [PATCH v5 14/23] lib: rspdm: Support SPDM get_version
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (12 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 13/23] PCI/CMA: Validate Subject Alternative Name in certificates alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:41 ` sashiko-bot
2026-10-27 5:25 ` [PATCH v5 15/23] lib: rspdm: Support SPDM get_capabilities alistair23
` (8 subsequent siblings)
22 siblings, 1 reply; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis, Jonathan Cameron
From: Alistair Francis <alistair.francis@wdc.com>
Support the GET_VERSION SPDM command.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
Reviewed-by: Jonathan Cameron <jonathan.cameron@oss.qualcomm.com>
---
lib/rspdm/consts.rs | 17 +++++++-
lib/rspdm/lib.rs | 25 +++++++++++-
lib/rspdm/state.rs | 43 +++++++++++++++++++-
lib/rspdm/validator.rs | 90 +++++++++++++++++++++++++++++++++++++++++-
4 files changed, 169 insertions(+), 6 deletions(-)
diff --git a/lib/rspdm/consts.rs b/lib/rspdm/consts.rs
index ef6d61d4998b5..a130673b6473c 100644
--- a/lib/rspdm/consts.rs
+++ b/lib/rspdm/consts.rs
@@ -7,12 +7,25 @@
//! Rust implementation of the DMTF Security Protocol and Data Model (SPDM)
//! <https://www.dmtf.org/dsp/DSP0274>
+use crate::validator::SpdmHeader;
+use core::mem;
+
// SPDM versions supported by this implementation
pub(crate) const SPDM_VER_10: u8 = 0x10;
+#[expect(dead_code)]
+pub(crate) const SPDM_VER_11: u8 = 0x11;
+#[expect(dead_code)]
+pub(crate) const SPDM_VER_12: u8 = 0x12;
+#[expect(dead_code)]
+pub(crate) const SPDM_VER_13: u8 = 0x13;
+pub(crate) const SPDM_VER_14: u8 = 0x14;
pub(crate) const SPDM_MIN_VER: u8 = SPDM_VER_10;
+pub(crate) const SPDM_MAX_VER: u8 = SPDM_VER_14;
-#[allow(dead_code)]
pub(crate) const SPDM_REQ: u8 = 0x80;
-#[allow(dead_code)]
pub(crate) const SPDM_ERROR: u8 = 0x7f;
+
+pub(crate) const SPDM_GET_VERSION: u8 = 0x84;
+pub(crate) const SPDM_GET_VERSION_LEN: usize =
+ mem::size_of::<SpdmHeader>() + 2 + (u8::MAX as usize) * mem::size_of::<u16>();
diff --git a/lib/rspdm/lib.rs b/lib/rspdm/lib.rs
index 27449ac1ca25d..e0fb7b49d4a07 100644
--- a/lib/rspdm/lib.rs
+++ b/lib/rspdm/lib.rs
@@ -238,7 +238,30 @@ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
///
/// See `include/linux/spdm.h` for the full C API documentation.
#[export]
-pub extern "C" fn spdm_authenticate(_state_ptr: *mut spdm_state) -> c_int {
+pub extern "C" fn spdm_authenticate(state_ptr: *mut spdm_state) -> c_int {
+ if state_ptr.is_null() {
+ return -(bindings::EINVAL as c_int);
+ }
+
+ // SAFETY: `state_ptr` was returned from `spdm_create()` which used `into_foreign()`
+ // to create the pointer, and it remains valid until `spdm_destroy()` is called.
+ // We only borrow here (rather than `from_foreign()`) so that ownership stays
+ // with the foreign (C) caller.
+ // The exclusive `&mut SpdmState` lives entirely inside the lock guard, so
+ // concurrent FFI callers serialize on the mutex and can never form
+ // aliased `&mut SpdmState` references.
+ let mutex = unsafe {
+ <Pin<KBox<Mutex<SpdmState<'_, CTransport>>>> as ForeignOwnable>::borrow(
+ state_ptr.cast::<c_void>(),
+ )
+ };
+
+ let mut state = mutex.lock();
+
+ if let Err(e) = state.get_version() {
+ return e.to_errno() as c_int;
+ }
+
-(EPROTONOSUPPORT as i32)
}
diff --git a/lib/rspdm/state.rs b/lib/rspdm/state.rs
index f864d869eff0c..22655b2aff258 100644
--- a/lib/rspdm/state.rs
+++ b/lib/rspdm/state.rs
@@ -20,10 +20,14 @@
use crate::consts::{
SPDM_ERROR,
+ SPDM_GET_VERSION_LEN,
+ SPDM_MAX_VER,
SPDM_MIN_VER,
SPDM_REQ, //
};
use crate::validator::{
+ GetVersionReq,
+ GetVersionRsp,
SpdmErrorRsp,
SpdmHeader, //
};
@@ -97,7 +101,6 @@ pub(crate) fn new(
///
/// The data in `request_buf` is sent to the device and the response
/// is returned as untrusted data to be validated by the caller.
- #[allow(dead_code)]
pub(crate) fn spdm_exchange(
&self,
request_buf: &[u8],
@@ -142,4 +145,42 @@ pub(crate) fn spdm_exchange(
Ok(Untrusted::new(response_buf))
}
+
+ /// Negotiate a supported SPDM version and store the information
+ /// in the `SpdmState`.
+ pub(crate) fn get_version(&mut self) -> Result<(), Error> {
+ let mut request = GetVersionReq::default();
+ request.header.version = SPDM_MIN_VER;
+ self.version = SPDM_MIN_VER;
+
+ let mut request_buf = request.to_bytes()?;
+
+ let response_vec: KVec<u8> = KVec::from_elem(0u8, SPDM_GET_VERSION_LEN, GFP_KERNEL)?;
+
+ let response: GetVersionRsp = self
+ .spdm_exchange(request_buf.as_mut_slice(), response_vec)?
+ .validate(&*self)?;
+
+ let mut foundver = false;
+ for &entry in response.version_number_entries.iter() {
+ let alpha_version = (entry & 0xF) as u8;
+ let version = (entry >> 8) as u8;
+
+ if alpha_version != 0 {
+ pr_warn!("Alpha version {alpha_version} is not specifically supported\n");
+ }
+
+ if version >= self.version && version <= SPDM_MAX_VER {
+ self.version = version;
+ foundver = true;
+ }
+ }
+
+ if !foundver {
+ pr_err!("No common supported version\n");
+ return Err(EPROTO);
+ }
+
+ Ok(())
+ }
}
diff --git a/lib/rspdm/validator.rs b/lib/rspdm/validator.rs
index 0fe341b8dce12..98922638a228b 100644
--- a/lib/rspdm/validator.rs
+++ b/lib/rspdm/validator.rs
@@ -7,11 +7,16 @@
//! Rust implementation of the DMTF Security Protocol and Data Model (SPDM)
//! <https://www.dmtf.org/dsp/DSP0274>
+use crate::consts::{
+ SPDM_GET_VERSION,
+ SPDM_MIN_VER, //
+};
use crate::{
SpdmErrorCode,
SpdmState,
Transport, //
};
+use core::mem;
use kernel::prelude::*;
use kernel::{
error::Error,
@@ -31,7 +36,6 @@ pub(crate) struct SpdmHeader {
}
impl SpdmHeader {
- #[expect(dead_code)]
pub(crate) fn new(code: u8) -> Self {
SpdmHeader {
version: 0,
@@ -41,7 +45,6 @@ pub(crate) fn new(code: u8) -> Self {
}
}
- #[expect(dead_code)]
pub(crate) fn to_bytes(&self) -> Result<KVec<u8>> {
let mut out = KVec::new();
@@ -90,3 +93,86 @@ fn validate(unvalidated: &[u8], _context: &'c SpdmState<'c, T>) -> Result<Self,
})
}
}
+
+pub(crate) struct GetVersionReq {
+ pub(crate) header: SpdmHeader,
+}
+
+impl GetVersionReq {
+ pub(crate) fn to_bytes(&self) -> Result<KVec<u8>> {
+ self.header.to_bytes()
+ }
+}
+
+impl Default for GetVersionReq {
+ fn default() -> Self {
+ GetVersionReq {
+ header: SpdmHeader::new(SPDM_GET_VERSION),
+ }
+ }
+}
+
+#[expect(dead_code)]
+pub(crate) struct GetVersionRsp {
+ pub(crate) header: SpdmHeader,
+
+ pub(crate) version_number_entry_count: u8,
+ pub(crate) version_number_entries: KVec<u16>,
+
+ /// 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 GetVersionRsp {
+ #[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 GetVersionRsp {
+ 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_VERSION - 0x80 {
+ return Err(EPROTO);
+ }
+
+ if header.version != SPDM_MIN_VER {
+ return Err(EPROTO);
+ }
+
+ let version_number_entry_count = *unvalidated.get(5).ok_or(EPROTO)?;
+
+ // Entries follow header (4) + reserved (1) + count (1) = 6 bytes.
+ let mut offset = mem::size_of::<SpdmHeader>() + 2;
+
+ let mut version_number_entries = KVec::new();
+ for _ in 0..version_number_entry_count {
+ let entry = u16::from_le_bytes(
+ unvalidated
+ .get(offset..offset + mem::size_of::<u16>())
+ .ok_or(EPROTO)?
+ .try_into()
+ .map_err(|_| EPROTO)?,
+ );
+ version_number_entries.push(entry, GFP_KERNEL)?;
+ offset += mem::size_of::<u16>();
+ }
+
+ Ok(GetVersionRsp {
+ header,
+ version_number_entry_count,
+ version_number_entries,
+ length: offset,
+ raw_bytes: unvalidated,
+ })
+ }
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* Re: [PATCH v5 14/23] lib: rspdm: Support SPDM get_version
2026-10-27 5:25 ` [PATCH v5 14/23] lib: rspdm: Support SPDM get_version alistair23
@ 2026-10-27 5:41 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-10-27 5:41 UTC (permalink / raw)
To: alistair23, linux-pci, linux-cxl; +Cc: ojeda, gary
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] rspdm: missing unsafe and Safety doc on spdm_authenticate()
--
commit 80429ad18bdb5fc720a23fcc20975ddc687dd8ec
Author: Alistair Francis <alistair.francis@wdc.com>
lib: rspdm: Support SPDM get_version
This commit adds support for the GET_VERSION SPDM command and calls
state.get_version() from spdm_authenticate().
> diff --git a/lib/rspdm/lib.rs b/lib/rspdm/lib.rs
> index 27449ac1ca25d..e0fb7b49d4a07 100644
> --- a/lib/rspdm/lib.rs
> +++ b/lib/rspdm/lib.rs
> @@ -238,7 +238,30 @@ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
> ///
> /// See `include/linux/spdm.h` for the full C API documentation.
> #[export]
> -pub extern "C" fn spdm_authenticate(_state_ptr: *mut spdm_state) -> c_int {
> +pub extern "C" fn spdm_authenticate(state_ptr: *mut spdm_state) -> c_int {
> + if state_ptr.is_null() {
> + return -(bindings::EINVAL as c_int);
> + }
> +
> + // SAFETY: `state_ptr` was returned from `spdm_create()` which used `into_foreign()`
> + // to create the pointer, and it remains valid until `spdm_destroy()` is called.
> + // We only borrow here (rather than `from_foreign()`) so that ownership stays
> + // with the foreign (C) caller.
> + // The exclusive `&mut SpdmState` lives entirely inside the lock guard, so
> + // concurrent FFI callers serialize on the mutex and can never form
> + // aliased `&mut SpdmState` references.
> + let mutex = unsafe {
> + <Pin<KBox<Mutex<SpdmState<'_, CTransport>>>> as ForeignOwnable>::borrow(
> + state_ptr.cast::<c_void>(),
> + )
> + };
[Severity: Medium]
Should spdm_authenticate() in lib/rspdm/lib.rs be declared as pub unsafe
extern "C" fn with a # Safety documentation section, similar to
spdm_create() in the same file?
Now that spdm_authenticate() passes a non-null state_ptr to
ForeignOwnable::borrow() and locks the returned mutex, calling it with an
invalid or dangling pointer (or concurrently with spdm_destroy()) causes
undefined behavior. The SAFETY comment here relies on the caller
guaranteeing that state_ptr was returned from spdm_create() and remains
valid until spdm_destroy() is called, which requires the function to be
marked unsafe.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027052536.1722081-1-alistair.francis@wdc.com?part=14
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH v5 15/23] lib: rspdm: Support SPDM get_capabilities
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (13 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 14/23] lib: rspdm: Support SPDM get_version alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:36 ` sashiko-bot
2026-10-27 5:25 ` [PATCH v5 16/23] rust: kernel: Add crypto hash bindings alistair23
` (7 subsequent siblings)
22 siblings, 1 reply; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis, Jonathan Cameron
From: Alistair Francis <alistair.francis@wdc.com>
Support the GET_CAPABILITIES SPDM command.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
Reviewed-by: Jonathan Cameron <jonathan.cameron@oss.qualcomm.com>
---
lib/rspdm/consts.rs | 23 ++-
lib/rspdm/lib.rs | 4 +
lib/rspdm/state.rs | 60 +++++++-
lib/rspdm/validator.rs | 319 ++++++++++++++++++++++++++++++++++++++++-
4 files changed, 401 insertions(+), 5 deletions(-)
diff --git a/lib/rspdm/consts.rs b/lib/rspdm/consts.rs
index a130673b6473c..fe5c79e0dd911 100644
--- a/lib/rspdm/consts.rs
+++ b/lib/rspdm/consts.rs
@@ -9,12 +9,14 @@
use crate::validator::SpdmHeader;
use core::mem;
+use kernel::bits::{
+ bit_u32,
+ bit_u8, //
+};
// SPDM versions supported by this implementation
pub(crate) const SPDM_VER_10: u8 = 0x10;
-#[expect(dead_code)]
pub(crate) const SPDM_VER_11: u8 = 0x11;
-#[expect(dead_code)]
pub(crate) const SPDM_VER_12: u8 = 0x12;
#[expect(dead_code)]
pub(crate) const SPDM_VER_13: u8 = 0x13;
@@ -29,3 +31,20 @@
pub(crate) const SPDM_GET_VERSION: u8 = 0x84;
pub(crate) const SPDM_GET_VERSION_LEN: usize =
mem::size_of::<SpdmHeader>() + 2 + (u8::MAX as usize) * mem::size_of::<u16>();
+
+pub(crate) const SPDM_GET_CAPABILITIES: u8 = 0xe1;
+pub(crate) const SPDM_CAP_SUPPORTED_ALGORITHMS: u8 = bit_u8(0);
+pub(crate) const SPDM_MIN_DATA_TRANSFER_SIZE: u32 = 42; // SPDM 1.2.0 margin no 226
+
+// SPDM cryptographic timeout of this implementation:
+// Assume calculations may take up to 1 sec on a busy machine, which equals
+// roughly 1 << 20. That's within the limits mandated for responders by CMA
+// (1 << 23 usec, PCIe r6.2 sec 6.31.3) and DOE (1 sec, PCIe r6.2 sec 6.30.2).
+// Used in GET_CAPABILITIES exchange.
+pub(crate) const SPDM_CTEXPONENT: u8 = 20;
+
+pub(crate) const SPDM_CERT_CAP: u32 = bit_u32(1);
+pub(crate) const SPDM_CHAL_CAP: u32 = bit_u32(2);
+
+pub(crate) const SPDM_REQ_CAPS: u32 = SPDM_CERT_CAP | SPDM_CHAL_CAP;
+pub(crate) const SPDM_RSP_MIN_CAPS: u32 = SPDM_CERT_CAP | SPDM_CHAL_CAP;
diff --git a/lib/rspdm/lib.rs b/lib/rspdm/lib.rs
index e0fb7b49d4a07..728910273a2ce 100644
--- a/lib/rspdm/lib.rs
+++ b/lib/rspdm/lib.rs
@@ -262,6 +262,10 @@ pub extern "C" fn spdm_authenticate(state_ptr: *mut spdm_state) -> c_int {
return e.to_errno() as c_int;
}
+ if let Err(e) = state.get_capabilities() {
+ return e.to_errno() as c_int;
+ }
+
-(EPROTONOSUPPORT as i32)
}
diff --git a/lib/rspdm/state.rs b/lib/rspdm/state.rs
index 22655b2aff258..815f332fabad8 100644
--- a/lib/rspdm/state.rs
+++ b/lib/rspdm/state.rs
@@ -23,13 +23,20 @@
SPDM_GET_VERSION_LEN,
SPDM_MAX_VER,
SPDM_MIN_VER,
- SPDM_REQ, //
+ SPDM_REQ,
+ SPDM_RSP_MIN_CAPS,
+ SPDM_VER_10,
+ SPDM_VER_11,
+ SPDM_VER_12, //
};
use crate::validator::{
+ GetCapabilitiesReq,
+ GetCapabilitiesRsp,
GetVersionReq,
GetVersionRsp,
SpdmErrorRsp,
- SpdmHeader, //
+ SpdmHeader,
+ GET_CAPABILITIES_RSP_SZ, //
};
/// Transport for exchanging SPDM messages with a responder device.
@@ -79,6 +86,9 @@ pub(crate) struct SpdmState<'a, T: Transport> {
/// Maximum common supported version of requester and responder.
/// Negotiated during GET_VERSION exchange.
pub(crate) version: u8,
+ /// Cached capabilities of responder. Received during GET_CAPABILITIES
+ /// exchange.
+ pub(crate) rsp_caps: u32,
}
impl<'a, T: Transport> SpdmState<'a, T> {
@@ -94,6 +104,7 @@ pub(crate) fn new(
transport_sz,
validate,
version: SPDM_MIN_VER,
+ rsp_caps: 0,
}
}
@@ -183,4 +194,49 @@ pub(crate) fn get_version(&mut self) -> Result<(), Error> {
Ok(())
}
+
+ /// Obtain the supported capabilities from an SPDM session and store the
+ /// information in the `SpdmState`.
+ pub(crate) fn get_capabilities(&mut self) -> Result<(), Error> {
+ let mut request = GetCapabilitiesReq::default();
+ request.header.version = self.version;
+
+ let rsp_sz = match self.version {
+ SPDM_VER_10 | SPDM_VER_11 => {
+ core::mem::size_of::<SpdmHeader>() + 4 + core::mem::size_of::<u32>()
+ }
+ _ => {
+ request.data_transfer_size = self.transport_sz;
+ request.max_spdm_msg_size = self.transport_sz;
+
+ (GET_CAPABILITIES_RSP_SZ as u32 + u32::from(u16::MAX)).min(self.transport_sz)
+ as usize
+ }
+ };
+
+ let mut request_buf = request.to_bytes()?;
+
+ let response_vec: KVec<u8> = KVec::from_elem(0u8, rsp_sz, GFP_KERNEL)?;
+
+ let response: GetCapabilitiesRsp = self
+ .spdm_exchange(request_buf.as_mut_slice(), response_vec)?
+ .validate(&*self)?;
+
+ self.rsp_caps = response.flags;
+ if (self.rsp_caps & SPDM_RSP_MIN_CAPS) != SPDM_RSP_MIN_CAPS {
+ pr_err!(
+ "{:#x} capabilities are supported, which don't meet required {:#x}\n",
+ self.rsp_caps,
+ SPDM_RSP_MIN_CAPS
+ );
+ self.rsp_caps = 0;
+ return Err(EPROTONOSUPPORT);
+ }
+
+ if self.version >= SPDM_VER_12 {
+ self.transport_sz = self.transport_sz.min(response.data_transfer_size);
+ }
+
+ Ok(())
+ }
}
diff --git a/lib/rspdm/validator.rs b/lib/rspdm/validator.rs
index 98922638a228b..080c2f2c49376 100644
--- a/lib/rspdm/validator.rs
+++ b/lib/rspdm/validator.rs
@@ -8,8 +8,16 @@
//! <https://www.dmtf.org/dsp/DSP0274>
use crate::consts::{
+ SPDM_CAP_SUPPORTED_ALGORITHMS,
+ SPDM_CTEXPONENT,
+ SPDM_GET_CAPABILITIES,
SPDM_GET_VERSION,
- SPDM_MIN_VER, //
+ SPDM_MIN_DATA_TRANSFER_SIZE,
+ SPDM_MIN_VER,
+ SPDM_REQ_CAPS,
+ SPDM_VER_10,
+ SPDM_VER_11,
+ SPDM_VER_12, //
};
use crate::{
SpdmErrorCode,
@@ -176,3 +184,312 @@ fn validate(unvalidated: KVec<u8>, context: &'c SpdmState<'c, T>) -> Result<Self
})
}
}
+
+pub(crate) struct GetCapabilitiesReq {
+ pub(crate) header: SpdmHeader,
+
+ pub(crate) ctexponent: u8,
+
+ pub(crate) flags: u32,
+
+ /* End of SPDM 1.1 structure */
+ pub(crate) data_transfer_size: u32,
+ pub(crate) max_spdm_msg_size: u32,
+}
+
+impl GetCapabilitiesReq {
+ pub(crate) fn to_bytes(&self) -> Result<KVec<u8>> {
+ let mut out = self.header.to_bytes()?;
+
+ if self.header.version <= SPDM_VER_10 {
+ return Ok(out);
+ }
+
+ out.push(0u8, GFP_KERNEL)?;
+ out.push(self.ctexponent, GFP_KERNEL)?;
+
+ out.extend_from_slice(&[0u8; 2], GFP_KERNEL)?;
+ out.extend_from_slice(&self.flags.to_le_bytes(), GFP_KERNEL)?;
+
+ if self.header.version >= SPDM_VER_12 {
+ out.extend_from_slice(&self.data_transfer_size.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&self.max_spdm_msg_size.to_le_bytes(), GFP_KERNEL)?;
+ }
+
+ Ok(out)
+ }
+}
+
+impl Default for GetCapabilitiesReq {
+ fn default() -> Self {
+ GetCapabilitiesReq {
+ header: SpdmHeader::new(SPDM_GET_CAPABILITIES),
+
+ ctexponent: SPDM_CTEXPONENT,
+ flags: SPDM_REQ_CAPS,
+ data_transfer_size: 0,
+ max_spdm_msg_size: 0,
+ }
+ }
+}
+
+/// Response AlgStructure field format
+#[expect(dead_code)]
+pub(crate) struct RespAlgStruct {
+ pub(crate) alg_type: u8,
+ pub(crate) alg_count: u8,
+ pub(crate) alg_supported: KVec<u8>,
+ pub(crate) alg_external: KVec<u32>,
+}
+
+pub(crate) const GET_CAPABILITIES_RSP_SZ: usize = mem::size_of::<SpdmHeader>() + 16;
+
+/// The GET_CAPABILITIES SupportedAlgorithms block (SPDM 1.3+).
+///
+/// Conforms to the NEGOTIATE_ALGORITHMS request message format,
+/// including all fields from Param1 through the end of the message inclusive.
+/// The `Length` field is equal to the total size of the block (`AlgSize`).
+#[expect(dead_code)]
+pub(crate) struct SupportedAlgorithms {
+ /// param1
+ pub(crate) alg_struct_count: u8,
+ pub(crate) length: u16,
+
+ pub(crate) measurement_specification: u8,
+ pub(crate) other_params_support: u8,
+
+ pub(crate) base_asym_algo: u32,
+ pub(crate) base_hash_algo: u32,
+
+ pub(crate) ext_asym_count: u8,
+ pub(crate) ext_hash_count: u8,
+
+ pub(crate) mel_specification: u8,
+
+ pub(crate) ext_asym: KVec<u32>,
+ pub(crate) ext_hash: KVec<u32>,
+ pub(crate) alg_struct: KVec<RespAlgStruct>,
+}
+
+impl SupportedAlgorithms {
+ #[allow(clippy::get_first)]
+ fn from_bytes(buf: &[u8]) -> Result<Self, Error> {
+ let read_le16 = |off: usize| -> Result<u16, Error> {
+ Ok(u16::from_le_bytes(
+ buf.get(off..off + mem::size_of::<u16>())
+ .ok_or(EPROTO)?
+ .try_into()
+ .map_err(|_| EPROTO)?,
+ ))
+ };
+ let read_le32 = |off: usize| -> Result<u32, Error> {
+ Ok(u32::from_le_bytes(
+ buf.get(off..off + mem::size_of::<u32>())
+ .ok_or(EPROTO)?
+ .try_into()
+ .map_err(|_| EPROTO)?,
+ ))
+ };
+
+ let alg_struct_count = *buf.get(0).ok_or(EPROTO)?;
+ let length = read_le16(2)?;
+ let measurement_specification = *buf.get(4).ok_or(EPROTO)?;
+ let other_params_support = *buf.get(5).ok_or(EPROTO)?;
+ let base_asym_algo = read_le32(6)?;
+ let base_hash_algo = read_le32(10)?;
+ let ext_asym_count = *buf.get(26).ok_or(EPROTO)?;
+ let ext_hash_count = *buf.get(27).ok_or(EPROTO)?;
+ let mel_specification = *buf.get(29).ok_or(EPROTO)?;
+
+ let mut offset = 30;
+
+ let mut ext_asym = KVec::new();
+ for _ in 0..ext_asym_count {
+ ext_asym.push(read_le32(offset)?, GFP_KERNEL)?;
+ offset += mem::size_of::<u32>();
+ }
+
+ let mut ext_hash = KVec::new();
+ for _ in 0..ext_hash_count {
+ ext_hash.push(read_le32(offset)?, GFP_KERNEL)?;
+ offset += mem::size_of::<u32>();
+ }
+
+ let mut alg_struct = KVec::new();
+ for _ in 0..alg_struct_count {
+ let alg_type = *buf.get(offset).ok_or(EPROTO)?;
+ let alg_count = *buf.get(offset + 1).ok_or(EPROTO)?;
+
+ let fixed_alg_count = (alg_count & 0xf) as usize;
+ let ext_alg_count = (alg_count >> 4) as usize;
+
+ offset += 2;
+
+ let mut alg_supported = KVec::new();
+ alg_supported.extend_from_slice(
+ buf.get(offset..offset + fixed_alg_count).ok_or(EPROTO)?,
+ GFP_KERNEL,
+ )?;
+ offset += fixed_alg_count;
+
+ let mut alg_external = KVec::new();
+ for _ in 0..ext_alg_count {
+ alg_external.push(read_le32(offset)?, GFP_KERNEL)?;
+ offset += mem::size_of::<u32>();
+ }
+
+ alg_struct.push(
+ RespAlgStruct {
+ alg_type,
+ alg_count,
+ alg_supported,
+ alg_external,
+ },
+ GFP_KERNEL,
+ )?;
+ }
+
+ if length as usize != offset {
+ pr_err!("Malformed SupportedAlgorithms block\n");
+ return Err(EPROTO);
+ }
+
+ Ok(SupportedAlgorithms {
+ alg_struct_count,
+ length,
+ measurement_specification,
+ other_params_support,
+ base_asym_algo,
+ base_hash_algo,
+ ext_asym_count,
+ ext_hash_count,
+ mel_specification,
+ ext_asym,
+ ext_hash,
+ alg_struct,
+ })
+ }
+}
+
+#[expect(dead_code)]
+pub(crate) struct GetCapabilitiesRsp {
+ pub(crate) header: SpdmHeader,
+
+ pub(crate) ctexponent: u8,
+ pub(crate) flags: u32,
+
+ // End of SPDM 1.1 structure
+ pub(crate) data_transfer_size: u32,
+ pub(crate) max_spdm_msg_size: u32,
+
+ pub(crate) supported_algorithms: Option<SupportedAlgorithms>,
+
+ /// 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 GetCapabilitiesRsp {
+ #[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 GetCapabilitiesRsp
+{
+ 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_CAPABILITIES - 0x80 {
+ return Err(EPROTO);
+ }
+
+ if header.version != context.version {
+ pr_err!("Invalid version response\n");
+ return Err(EPROTO);
+ }
+
+ let ctexponent = *unvalidated.get(5).ok_or(EPROTO)?;
+ let flags = u32::from_le_bytes(
+ unvalidated
+ .get(8..12)
+ .ok_or(EPROTO)?
+ .try_into()
+ .map_err(|_| EPROTO)?,
+ );
+
+ // DataTransferSize and MaxSPDMmsgSize only exist in SPDM 1.2 and later.
+ let (data_transfer_size, max_spdm_msg_size, supported_algorithms, length) = if context
+ .version
+ <= SPDM_VER_11
+ {
+ (
+ 0,
+ 0,
+ None,
+ mem::size_of::<SpdmHeader>() + 4 + mem::size_of::<u32>(),
+ )
+ } else {
+ let data_transfer_size = u32::from_le_bytes(
+ unvalidated
+ .get(12..16)
+ .ok_or(EPROTO)?
+ .try_into()
+ .map_err(|_| EPROTO)?,
+ );
+ let max_spdm_msg_size = u32::from_le_bytes(
+ unvalidated
+ .get(16..20)
+ .ok_or(EPROTO)?
+ .try_into()
+ .map_err(|_| EPROTO)?,
+ );
+
+ if data_transfer_size < SPDM_MIN_DATA_TRANSFER_SIZE {
+ pr_err!(
+ "Invalid minimum transport size {data_transfer_size}, must be at least {SPDM_MIN_DATA_TRANSFER_SIZE}\n"
+ );
+ return Err(EPROTONOSUPPORT);
+ }
+
+ let supported_algorithms = if header.param1 & SPDM_CAP_SUPPORTED_ALGORITHMS != 0 {
+ Some(SupportedAlgorithms::from_bytes(
+ unvalidated.get(GET_CAPABILITIES_RSP_SZ..).ok_or(EPROTO)?,
+ )?)
+ } else {
+ None
+ };
+
+ let length = GET_CAPABILITIES_RSP_SZ
+ + supported_algorithms
+ .as_ref()
+ .map_or(0, |s| s.length as usize);
+
+ (
+ data_transfer_size,
+ max_spdm_msg_size,
+ supported_algorithms,
+ length,
+ )
+ };
+
+ Ok(GetCapabilitiesRsp {
+ header,
+ ctexponent,
+ flags,
+ data_transfer_size,
+ max_spdm_msg_size,
+ supported_algorithms,
+ length,
+ raw_bytes: unvalidated,
+ })
+ }
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* Re: [PATCH v5 15/23] lib: rspdm: Support SPDM get_capabilities
2026-10-27 5:25 ` [PATCH v5 15/23] lib: rspdm: Support SPDM get_capabilities alistair23
@ 2026-10-27 5:36 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-10-27 5:36 UTC (permalink / raw)
To: alistair23, linux-pci, linux-cxl; +Cc: ojeda, gary
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] rspdm: swapped fixed and extended alg_count nibbles in from_bytes()
--
commit 9a97b825dcf3405e60a1e8252ec1b1e885dfcb08
Author: Alistair Francis <alistair.francis@wdc.com>
lib: rspdm: Support SPDM get_capabilities
Add support for the SPDM GET_CAPABILITIES command and response validation,
including parsing the SPDM 1.3+ SupportedAlgorithms block.
> diff --git a/lib/rspdm/validator.rs b/lib/rspdm/validator.rs
> index 98922638a228b..080c2f2c49376 100644
> --- a/lib/rspdm/validator.rs
> +++ b/lib/rspdm/validator.rs
[ ... ]
> @@ -176,3 +184,312 @@ fn validate(unvalidated: KVec<u8>, context: &'c SpdmState<'c, T>) -> Result<Self
> })
> }
> }
[ ... ]
> +impl SupportedAlgorithms {
> + #[allow(clippy::get_first)]
> + fn from_bytes(buf: &[u8]) -> Result<Self, Error> {
[ ... ]
> + let mut alg_struct = KVec::new();
> + for _ in 0..alg_struct_count {
> + let alg_type = *buf.get(offset).ok_or(EPROTO)?;
> + let alg_count = *buf.get(offset + 1).ok_or(EPROTO)?;
> +
> + let fixed_alg_count = (alg_count & 0xf) as usize;
> + let ext_alg_count = (alg_count >> 4) as usize;
[Severity: High]
Are the high and low nibbles of alg_count swapped here in
SupportedAlgorithms::from_bytes()?
Per DSP0274, the AlgCount field in an AlgStructure entry encodes
FixedAlgCount in bits [7:4] (alg_count >> 4) and ExtAlgCount in bits [3:0]
(alg_count & 0xf).
When an SPDM 1.3+ responder sets SPDM_CAP_SUPPORTED_ALGORITHMS in
header.param1 and includes AlgStructure entries in the CAPABILITIES
response, spdm_authenticate() -> SpdmState::get_capabilities() ->
GetCapabilitiesRsp::validate() -> SupportedAlgorithms::from_bytes() will
compute the wrong sizes for alg_supported and alg_external (for example,
treating a standard alg_count of 0x20 as 0 fixed bytes and 2 extended
32-bit algorithms instead of 2 fixed bytes and 0 extended algorithms),
causing offset validation to fail with EPROTO.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027052536.1722081-1-alistair.francis@wdc.com?part=15
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH v5 16/23] rust: kernel: Add crypto hash bindings
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (14 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 15/23] lib: rspdm: Support SPDM get_capabilities alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:41 ` sashiko-bot
2026-10-27 5:25 ` [PATCH v5 17/23] lib: rspdm: Support SPDM negotiate_algorithms alistair23
` (6 subsequent siblings)
22 siblings, 1 reply; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Alistair Francis <alistair.francis@wdc.com>
Initial support for the kernel crypto hash bindings, which will be used
by the SPDM library.
This is just a few functions that the Rust SPDM implementation will use
in the next few commits.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
rust/kernel/hash.rs | 145 ++++++++++++++++++++++++++++++++++++++++++++
rust/kernel/lib.rs | 2 +
2 files changed, 147 insertions(+)
create mode 100644 rust/kernel/hash.rs
diff --git a/rust/kernel/hash.rs b/rust/kernel/hash.rs
new file mode 100644
index 0000000000000..1800065d98767
--- /dev/null
+++ b/rust/kernel/hash.rs
@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: GPL-2.0
+
+// Copyright (C) 2026 Western Digital
+
+//! Wrappers around the Linux kernel crypto hash APIs.
+//!
+//! C headers: [`include/crypto/hash.h`](srctree/include/crypto/hash.h).
+//!
+//! Reference: <https://www.kernel.org/doc/html/latest/crypto/devel-algos.html#hashing-hash>
+
+use core::ptr::NonNull;
+use kernel::prelude::*;
+use kernel::{
+ bindings,
+ error::{
+ from_err_ptr,
+ to_result, //
+ },
+ str::CStr,
+};
+
+/// Synchronous hash transformation plus its descriptor buffer.
+pub struct Shash {
+ shash: NonNull<bindings::crypto_shash>,
+ desc: NonNull<bindings::shash_desc>,
+ desc_len: usize,
+ desc_capacity: usize,
+ hash_len: usize,
+ name: &'static CStr,
+}
+
+impl Shash {
+ /// Allocates a hash transformation and initialises a descriptor for it.
+ pub fn new(name: &'static CStr) -> Result<Self> {
+ // SAFETY: `name.as_char_ptr()` is a valid NUL-terminated C string.
+ // `crypto_alloc_shash()` returns a valid pointer or an ERR_PTR.
+ let shash =
+ from_err_ptr(unsafe { bindings::crypto_alloc_shash(name.as_char_ptr(), 0, 0) })?;
+ let shash = NonNull::new(shash).ok_or(ENOMEM)?;
+
+ // SAFETY: `shash` is a valid `crypto_shash` handle.
+ let desc_size = unsafe { bindings::crypto_shash_descsize(shash.as_ptr()) } as usize;
+ // SAFETY: `shash` is a valid `crypto_shash` handle.
+ let hash_len = unsafe { bindings::crypto_shash_digestsize(shash.as_ptr()) } as usize;
+ let desc_len = core::mem::size_of::<bindings::shash_desc>() + desc_size;
+
+ let desc_vec = match KVec::from_elem(0u8, desc_len, GFP_KERNEL) {
+ Ok(desc_vec) => desc_vec,
+ Err(e) => {
+ // SAFETY: `shash` was allocated above and must be freed if the
+ // descriptor allocation fails.
+ unsafe {
+ bindings::crypto_free_shash(shash.as_ptr());
+ }
+ return Err(e.into());
+ }
+ };
+
+ let (desc_buf, _desc_len, desc_capacity) = desc_vec.into_raw_parts();
+ let desc = NonNull::new(desc_buf.cast::<bindings::shash_desc>()).ok_or(ENOMEM)?;
+
+ // SAFETY: `desc` points to an allocation of `desc_len` bytes, which is
+ // exactly large enough for a `shash_desc` header and its trailing
+ // algorithm state.
+ unsafe {
+ (*desc.as_ptr()).tfm = shash.as_ptr();
+ }
+
+ // SAFETY: `desc` is a valid `shash_desc` buffer with `tfm` initialised.
+ if let Err(e) = unsafe { to_result(bindings::crypto_shash_init(desc.as_ptr())) } {
+ // SAFETY: `desc.as_ptr()`, `desc_len`, and `desc_capacity` are the
+ // exact parts returned by `into_raw_parts()` above.
+ let desc_vec = unsafe {
+ KVec::<u8>::from_raw_parts(desc.as_ptr().cast::<u8>(), desc_len, desc_capacity)
+ };
+ drop(desc_vec);
+
+ // SAFETY: `shash` is a valid handle created above.
+ unsafe {
+ bindings::crypto_free_shash(shash.as_ptr());
+ }
+ return Err(e);
+ }
+
+ Ok(Self {
+ shash,
+ desc,
+ desc_len,
+ desc_capacity,
+ hash_len,
+ name,
+ })
+ }
+
+ /// Return the name of the hash
+ pub fn name(&self) -> &'static CStr {
+ self.name
+ }
+
+ /// Return the name of the hash output
+ pub fn hash_len(&self) -> usize {
+ self.hash_len
+ }
+
+ /// Computes a one-shot digest into `out`, which must be exactly the digest
+ /// size of the selected hash algorithm.
+ pub fn digest(&self, data: &[u8], out: &mut [u8]) -> Result {
+ if out.len() != self.hash_len {
+ return Err(EINVAL);
+ }
+
+ // SAFETY: `self.desc` was created by `Shash::new()` and points to a
+ // valid `shash_desc` with a valid `tfm`. `out` is checked above to be
+ // exactly `crypto_shash_digestsize()` bytes.
+ unsafe {
+ to_result(bindings::crypto_shash_digest(
+ self.desc.as_ptr(),
+ data.as_ptr(),
+ data.len() as u32,
+ out.as_mut_ptr(),
+ ))
+ }
+ }
+}
+
+impl Drop for Shash {
+ fn drop(&mut self) {
+ // SAFETY: `self.desc` was produced from a `KVec<u8>` allocation by
+ // `into_raw_parts()` in `Shash::new()`, and `desc_len` and
+ // `desc_capacity` are the exact values handed out at that time.
+ let desc_vec = unsafe {
+ KVec::<u8>::from_raw_parts(
+ self.desc.as_ptr().cast::<u8>(),
+ self.desc_len,
+ self.desc_capacity,
+ )
+ };
+ drop(desc_vec);
+
+ // SAFETY: `self.shash` is a valid handle created by `crypto_alloc_shash()`.
+ unsafe {
+ bindings::crypto_free_shash(self.shash.as_ptr());
+ }
+ }
+}
diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
index a3c54971e9dea..f7db5975e7621 100644
--- a/rust/kernel/lib.rs
+++ b/rust/kernel/lib.rs
@@ -82,6 +82,8 @@
pub mod fwctl;
#[cfg(CONFIG_GPU_BUDDY = "y")]
pub mod gpu;
+#[cfg(CONFIG_CRYPTO)]
+pub mod hash;
#[cfg(CONFIG_I2C = "y")]
pub mod i2c;
pub mod id_pool;
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* Re: [PATCH v5 16/23] rust: kernel: Add crypto hash bindings
2026-10-27 5:25 ` [PATCH v5 16/23] rust: kernel: Add crypto hash bindings alistair23
@ 2026-10-27 5:41 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-10-27 5:41 UTC (permalink / raw)
To: alistair23, linux-pci, linux-cxl; +Cc: ojeda, gary
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] rust: kernel: hash module gated on CONFIG_CRYPTO breaks m/n builds
- [Low] rust: kernel: unchecked usize to u32 cast in Shash::digest()
- [Low] rust: kernel: wrong doc comment and missing #[inline] in Shash
--
commit 43861816c7010fda266b93e8a13fd64b1fdc4ecd
Author: Alistair Francis <alistair.francis@wdc.com>
rust: kernel: Add crypto hash bindings
This commit adds initial Rust abstractions in the kernel crate for synchronous
crypto hash transformations (Shash) wrapping the C crypto_shash APIs.
> diff --git a/rust/kernel/hash.rs b/rust/kernel/hash.rs
> new file mode 100644
> index 0000000000000..1800065d98767
> --- /dev/null
> +++ b/rust/kernel/hash.rs
[ ... ]
> + /// Return the name of the hash
> + pub fn name(&self) -> &'static CStr {
> + self.name
> + }
> +
> + /// Return the name of the hash output
> + pub fn hash_len(&self) -> usize {
> + self.hash_len
> + }
[Severity: Low]
This isn't a bug, but the doc comment on Shash::hash_len() says "Return the
name of the hash output" instead of the length of the hash output. Also,
should Shash::name(), Shash::hash_len(), and Shash::digest() in
rust/kernel/hash.rs be annotated with #[inline] as small kernel crate
abstraction functions?
> + /// Computes a one-shot digest into `out`, which must be exactly the digest
> + /// size of the selected hash algorithm.
> + pub fn digest(&self, data: &[u8], out: &mut [u8]) -> Result {
> + if out.len() != self.hash_len {
> + return Err(EINVAL);
> + }
> +
> + // SAFETY: `self.desc` was created by `Shash::new()` and points to a
> + // valid `shash_desc` with a valid `tfm`. `out` is checked above to be
> + // exactly `crypto_shash_digestsize()` bytes.
> + unsafe {
> + to_result(bindings::crypto_shash_digest(
> + self.desc.as_ptr(),
> + data.as_ptr(),
> + data.len() as u32,
> + out.as_mut_ptr(),
> + ))
> + }
> + }
[Severity: Low]
In Shash::digest(), could passing a slice with data.len() > u32::MAX on a
64-bit platform silently truncate the input length via data.len() as u32 and
compute a digest over only a prefix of data? Would u32::try_from(data.len())?
avoid the unchecked narrowing cast?
[ ... ]
> diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
> index a3c54971e9dea..f7db5975e7621 100644
> --- a/rust/kernel/lib.rs
> +++ b/rust/kernel/lib.rs
> @@ -82,6 +82,8 @@
> pub mod fwctl;
> #[cfg(CONFIG_GPU_BUDDY = "y")]
> pub mod gpu;
> +#[cfg(CONFIG_CRYPTO)]
> +pub mod hash;
[Severity: Medium]
Will gating the built-in hash module in rust/kernel/lib.rs on
#[cfg(CONFIG_CRYPTO)] break the build when CONFIG_CRYPTO=m, or when
CONFIG_CRYPTO=y and CONFIG_CRYPTO_HASH2 is m or n?
Because CONFIG_CRYPTO is a tristate symbol, Kconfig emits --cfg=CONFIG_CRYPTO
for both CONFIG_CRYPTO=y and CONFIG_CRYPTO=m, whereas in C, CONFIG_CRYPTO is
only defined for built-in builds:
rust/helpers/hash.c:
#ifdef CONFIG_CRYPTO
__rust_helper unsigned int rust_helper_crypto_shash_descsize(struct crypto_shash *tfm)
When CONFIG_CRYPTO=m, Shash::new() and Shash::drop() in rust/kernel/hash.rs
are compiled into built-in rust/kernel.o, but bindgen does not generate
bindings::crypto_shash_descsize, bindings::crypto_shash_digestsize, or
bindings::crypto_free_shash from rust/helpers/hash.c.
In addition, Shash::new() and Shash::digest() call crypto_alloc_shash(),
crypto_shash_init(), and crypto_shash_digest(), which are compiled under
CONFIG_CRYPTO_HASH2 rather than CONFIG_CRYPTO:
crypto/Makefile:
crypto_hash-y += ahash.o
crypto_hash-y += shash.o
obj-$(CONFIG_CRYPTO_HASH2) += crypto_hash.o
If CONFIG_CRYPTO=y and CONFIG_CRYPTO_HASH2 is n or m, vmlinux fails to link
with undefined references to those shash symbols from rust/kernel.o.
Should rust/kernel/lib.rs use #[cfg(CONFIG_CRYPTO_HASH2 = "y")] (and
rust/helpers/hash.c guard on CONFIG_CRYPTO_HASH2) to match the other built-in
subsystem modules in rust/kernel/lib.rs?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027052536.1722081-1-alistair.francis@wdc.com?part=16
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH v5 17/23] lib: rspdm: Support SPDM negotiate_algorithms
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (15 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 16/23] rust: kernel: Add crypto hash bindings alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:43 ` sashiko-bot
2026-10-27 5:25 ` [PATCH v5 18/23] lib: rspdm: Support SPDM get_digests alistair23
` (5 subsequent siblings)
22 siblings, 1 reply; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Alistair Francis <alistair.francis@wdc.com>
Support the NEGOTIATE_ALGORITHMS SPDM command.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
lib/rspdm/consts.rs | 52 ++++++++++
lib/rspdm/lib.rs | 4 +
lib/rspdm/state.rs | 157 +++++++++++++++++++++++++++-
lib/rspdm/validator.rs | 226 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 438 insertions(+), 1 deletion(-)
diff --git a/lib/rspdm/consts.rs b/lib/rspdm/consts.rs
index fe5c79e0dd911..7a75bd1e4649c 100644
--- a/lib/rspdm/consts.rs
+++ b/lib/rspdm/consts.rs
@@ -45,6 +45,58 @@
pub(crate) const SPDM_CERT_CAP: u32 = bit_u32(1);
pub(crate) const SPDM_CHAL_CAP: u32 = bit_u32(2);
+pub(crate) const SPDM_KEY_EX_CAP: u32 = bit_u32(9);
+pub(crate) const SPDM_MEAS_RESP_CAP: u32 = bit_u32(3) | bit_u32(4);
pub(crate) const SPDM_REQ_CAPS: u32 = SPDM_CERT_CAP | SPDM_CHAL_CAP;
pub(crate) const SPDM_RSP_MIN_CAPS: u32 = SPDM_CERT_CAP | SPDM_CHAL_CAP;
+
+pub(crate) const SPDM_NEGOTIATE_ALGS: u8 = 0xe3;
+
+pub(crate) const SPDM_MEAS_SPEC_DMTF: u8 = bit_u8(0);
+
+pub(crate) const SPDM_ASYM_RSASSA_2048: u32 = bit_u32(0);
+pub(crate) const _SPDM_ASYM_RSAPSS_2048: u32 = bit_u32(1);
+pub(crate) const SPDM_ASYM_RSASSA_3072: u32 = bit_u32(2);
+pub(crate) const _SPDM_ASYM_RSAPSS_3072: u32 = bit_u32(3);
+pub(crate) const SPDM_ASYM_ECDSA_ECC_NIST_P256: u32 = bit_u32(4);
+pub(crate) const SPDM_ASYM_RSASSA_4096: u32 = bit_u32(5);
+pub(crate) const _SPDM_ASYM_RSAPSS_4096: u32 = bit_u32(6);
+pub(crate) const SPDM_ASYM_ECDSA_ECC_NIST_P384: u32 = bit_u32(7);
+pub(crate) const SPDM_ASYM_ECDSA_ECC_NIST_P521: u32 = bit_u32(8);
+pub(crate) const _SPDM_ASYM_SM2_ECC_SM2_P256: u32 = bit_u32(9);
+pub(crate) const _SPDM_ASYM_EDDSA_ED25519: u32 = bit_u32(10);
+pub(crate) const _SPDM_ASYM_EDDSA_ED448: u32 = bit_u32(11);
+
+pub(crate) const SPDM_HASH_SHA_256: u32 = bit_u32(0);
+pub(crate) const SPDM_HASH_SHA_384: u32 = bit_u32(1);
+pub(crate) const SPDM_HASH_SHA_512: u32 = bit_u32(2);
+
+// If the crypto support isn't enabled don't offer the algorithms
+// to the responder
+#[cfg(CONFIG_CRYPTO_RSA)]
+pub(crate) const SPDM_ASYM_RSA: u32 =
+ SPDM_ASYM_RSASSA_2048 | SPDM_ASYM_RSASSA_3072 | SPDM_ASYM_RSASSA_4096;
+#[cfg(not(CONFIG_CRYPTO_RSA))]
+pub(crate) const SPDM_ASYM_RSA: u32 = 0;
+
+#[cfg(CONFIG_CRYPTO_ECDSA)]
+pub(crate) const SPDM_ASYM_ECDSA: u32 =
+ SPDM_ASYM_ECDSA_ECC_NIST_P256 | SPDM_ASYM_ECDSA_ECC_NIST_P384 | SPDM_ASYM_ECDSA_ECC_NIST_P521;
+#[cfg(not(CONFIG_CRYPTO_ECDSA))]
+pub(crate) const SPDM_ASYM_ECDSA: u32 = 0;
+
+#[cfg(CONFIG_CRYPTO_SHA256)]
+pub(crate) const SPDM_HASH_SHA2_256: u32 = SPDM_HASH_SHA_256;
+#[cfg(not(CONFIG_CRYPTO_SHA256))]
+pub(crate) const SPDM_HASH_SHA2_256: u32 = 0;
+
+#[cfg(CONFIG_CRYPTO_SHA512)]
+pub(crate) const SPDM_HASH_SHA2_384_512: u32 = SPDM_HASH_SHA_384 | SPDM_HASH_SHA_512;
+#[cfg(not(CONFIG_CRYPTO_SHA512))]
+pub(crate) const SPDM_HASH_SHA2_384_512: u32 = 0;
+
+pub(crate) const SPDM_ASYM_ALGOS: u32 = SPDM_ASYM_RSA | SPDM_ASYM_ECDSA;
+pub(crate) const SPDM_HASH_ALGOS: u32 = SPDM_HASH_SHA2_256 | SPDM_HASH_SHA2_384_512;
+
+pub(crate) const SPDM_OPAQUE_DATA_FMT_GENERAL: u8 = bit_u8(1);
diff --git a/lib/rspdm/lib.rs b/lib/rspdm/lib.rs
index 728910273a2ce..4c219b32098ab 100644
--- a/lib/rspdm/lib.rs
+++ b/lib/rspdm/lib.rs
@@ -266,6 +266,10 @@ pub extern "C" fn spdm_authenticate(state_ptr: *mut spdm_state) -> c_int {
return e.to_errno() as c_int;
}
+ if let Err(e) = state.negotiate_algs() {
+ return e.to_errno() as c_int;
+ }
+
-(EPROTONOSUPPORT as i32)
}
diff --git a/lib/rspdm/state.rs b/lib/rspdm/state.rs
index 815f332fabad8..79819c492b07b 100644
--- a/lib/rspdm/state.rs
+++ b/lib/rspdm/state.rs
@@ -15,14 +15,30 @@
Device, //
},
error::Error,
+ hash::Shash,
+ str::CStr,
validate::Untrusted,
};
use crate::consts::{
+ SPDM_ASYM_ALGOS,
+ SPDM_ASYM_ECDSA_ECC_NIST_P256,
+ SPDM_ASYM_ECDSA_ECC_NIST_P384,
+ SPDM_ASYM_ECDSA_ECC_NIST_P521,
+ SPDM_ASYM_RSASSA_2048,
+ SPDM_ASYM_RSASSA_3072,
+ SPDM_ASYM_RSASSA_4096,
SPDM_ERROR,
SPDM_GET_VERSION_LEN,
+ SPDM_HASH_ALGOS,
+ SPDM_HASH_SHA_256,
+ SPDM_HASH_SHA_384,
+ SPDM_HASH_SHA_512,
+ SPDM_KEY_EX_CAP,
SPDM_MAX_VER,
+ SPDM_MEAS_RESP_CAP,
SPDM_MIN_VER,
+ SPDM_OPAQUE_DATA_FMT_GENERAL,
SPDM_REQ,
SPDM_RSP_MIN_CAPS,
SPDM_VER_10,
@@ -34,9 +50,12 @@
GetCapabilitiesRsp,
GetVersionReq,
GetVersionRsp,
+ NegotiateAlgsReq,
+ NegotiateAlgsRsp,
SpdmErrorRsp,
SpdmHeader,
- GET_CAPABILITIES_RSP_SZ, //
+ GET_CAPABILITIES_RSP_SZ,
+ NEGOTIATE_ALGS_RSP_SZ, //
};
/// Transport for exchanging SPDM messages with a responder device.
@@ -89,6 +108,27 @@ pub(crate) struct SpdmState<'a, T: Transport> {
/// Cached capabilities of responder. Received during GET_CAPABILITIES
/// exchange.
pub(crate) rsp_caps: u32,
+ /// Asymmetric key algorithm for signature verification of CHALLENGE_AUTH
+ /// and MEASUREMENTS messages. Selected by responder during
+ /// NEGOTIATE_ALGORITHMS exchange.
+ pub(crate) base_asym_alg: u32,
+ /// Hash algorithm for signature verification of CHALLENGE_AUTH and
+ /// MEASUREMENTS messages. Selected by responder during
+ /// NEGOTIATE_ALGORITHMS exchange.
+ pub(crate) base_hash_alg: u32,
+ /// Hash algorithm for measurement blocks. Selected by responder during
+ /// NEGOTIATE_ALGORITHMS exchange.
+ pub(crate) meas_hash_alg: u32,
+
+ /// Human-readable name of `base_asym_alg`'s signature encoding. Passed
+ /// to the crypto subsystem when calling `verify_signature()`.
+ base_asym_enc: &'static CStr,
+ /// Signature length of `base_asym_alg` (in bytes). S or SigLen in SPDM
+ /// specification.
+ sig_len: usize,
+
+ /// Hash transformation for `base_hash_alg` computation.
+ crypto: Option<Shash>,
}
impl<'a, T: Transport> SpdmState<'a, T> {
@@ -105,6 +145,12 @@ pub(crate) fn new(
validate,
version: SPDM_MIN_VER,
rsp_caps: 0,
+ base_asym_alg: 0,
+ base_hash_alg: 0,
+ meas_hash_alg: 0,
+ base_asym_enc: c"",
+ sig_len: 0,
+ crypto: None,
}
}
@@ -239,4 +285,113 @@ pub(crate) fn get_capabilities(&mut self) -> Result<(), Error> {
Ok(())
}
+
+ fn update_response_algs(&mut self) -> Result<(), Error> {
+ match self.base_asym_alg {
+ #[cfg(CONFIG_CRYPTO_RSA)]
+ SPDM_ASYM_RSASSA_2048 => {
+ self.sig_len = 256;
+ self.base_asym_enc = c"pkcs1";
+ }
+ #[cfg(CONFIG_CRYPTO_RSA)]
+ SPDM_ASYM_RSASSA_3072 => {
+ self.sig_len = 384;
+ self.base_asym_enc = c"pkcs1";
+ }
+ #[cfg(CONFIG_CRYPTO_RSA)]
+ SPDM_ASYM_RSASSA_4096 => {
+ self.sig_len = 512;
+ self.base_asym_enc = c"pkcs1";
+ }
+ #[cfg(CONFIG_CRYPTO_ECDSA)]
+ SPDM_ASYM_ECDSA_ECC_NIST_P256 => {
+ self.sig_len = 64;
+ self.base_asym_enc = c"p1363";
+ }
+ #[cfg(CONFIG_CRYPTO_ECDSA)]
+ SPDM_ASYM_ECDSA_ECC_NIST_P384 => {
+ self.sig_len = 96;
+ self.base_asym_enc = c"p1363";
+ }
+ #[cfg(CONFIG_CRYPTO_ECDSA)]
+ SPDM_ASYM_ECDSA_ECC_NIST_P521 => {
+ self.sig_len = 132;
+ self.base_asym_enc = c"p1363";
+ }
+ _ => {
+ pr_err!("Unknown asym algorithm\n");
+ return Err(EPROTO);
+ }
+ }
+
+ let hash_name = match self.base_hash_alg {
+ #[cfg(CONFIG_CRYPTO_SHA256)]
+ SPDM_HASH_SHA_256 => c"sha256",
+ #[cfg(CONFIG_CRYPTO_SHA512)]
+ SPDM_HASH_SHA_384 => c"sha384",
+ #[cfg(CONFIG_CRYPTO_SHA512)]
+ SPDM_HASH_SHA_512 => c"sha512",
+ _ => {
+ pr_err!("Unknown hash algorithm\n");
+ return Err(EPROTO);
+ }
+ };
+
+ self.crypto = Some(Shash::new(hash_name)?);
+
+ Ok(())
+ }
+
+ pub(crate) fn negotiate_algs(&mut self) -> Result<(), Error> {
+ let mut request = NegotiateAlgsReq::default();
+ request.header.version = self.version;
+
+ if self.version >= SPDM_VER_12 && (self.rsp_caps & SPDM_KEY_EX_CAP) == SPDM_KEY_EX_CAP {
+ request.other_params_support = SPDM_OPAQUE_DATA_FMT_GENERAL;
+ }
+
+ let rsp_sz =
+ (NEGOTIATE_ALGS_RSP_SZ as u32 + u32::from(u16::MAX)).min(self.transport_sz) as usize;
+
+ request.length = NegotiateAlgsReq::WIRE_SIZE as u16;
+
+ let mut request_buf = request.to_bytes()?;
+
+ let response_vec: KVec<u8> = KVec::from_elem(0u8, rsp_sz, GFP_KERNEL)?;
+
+ let response: NegotiateAlgsRsp = self
+ .spdm_exchange(request_buf.as_mut_slice(), response_vec)?
+ .validate(&*self)?;
+
+ self.base_asym_alg = response.base_asym_sel;
+ self.base_hash_alg = response.base_hash_sel;
+ self.meas_hash_alg = response.measurement_hash_algo;
+
+ if self.base_asym_alg & SPDM_ASYM_ALGOS == 0 || self.base_hash_alg & SPDM_HASH_ALGOS == 0 {
+ pr_err!("No common supported algorithms\n");
+ return Err(EPROTO);
+ }
+
+ let meas_hash_valid = if self.rsp_caps & SPDM_MEAS_RESP_CAP != 0
+ && response.measurement_specification_sel != 0
+ {
+ self.meas_hash_alg.count_ones() == 1
+ } else {
+ self.meas_hash_alg == 0
+ };
+
+ // /* Responder shall select exactly 1 alg (SPDM 1.0.0 table 14) */
+ if self.base_asym_alg.count_ones() != 1
+ || self.base_hash_alg.count_ones() != 1
+ || !meas_hash_valid
+ || response.other_params_sel != request.other_params_support
+ {
+ pr_err!("Malformed algorithms response\n");
+ return Err(EPROTO);
+ }
+
+ self.update_response_algs()?;
+
+ Ok(())
+ }
}
diff --git a/lib/rspdm/validator.rs b/lib/rspdm/validator.rs
index 080c2f2c49376..9afcb3f8bcb6c 100644
--- a/lib/rspdm/validator.rs
+++ b/lib/rspdm/validator.rs
@@ -8,12 +8,16 @@
//! <https://www.dmtf.org/dsp/DSP0274>
use crate::consts::{
+ SPDM_ASYM_ALGOS,
SPDM_CAP_SUPPORTED_ALGORITHMS,
SPDM_CTEXPONENT,
SPDM_GET_CAPABILITIES,
SPDM_GET_VERSION,
+ SPDM_HASH_ALGOS,
+ SPDM_MEAS_SPEC_DMTF,
SPDM_MIN_DATA_TRANSFER_SIZE,
SPDM_MIN_VER,
+ SPDM_NEGOTIATE_ALGS,
SPDM_REQ_CAPS,
SPDM_VER_10,
SPDM_VER_11,
@@ -493,3 +497,225 @@ fn validate(unvalidated: KVec<u8>, context: &'c SpdmState<'c, T>) -> Result<Self
})
}
}
+
+pub(crate) struct NegotiateAlgsReq {
+ pub(crate) header: SpdmHeader,
+
+ pub(crate) length: u16,
+ pub(crate) measurement_specification: u8,
+ pub(crate) other_params_support: u8,
+
+ pub(crate) base_asym_algo: u32,
+ pub(crate) base_hash_algo: u32,
+
+ pub(crate) ext_asym_count: u8,
+ pub(crate) ext_hash_count: u8,
+ pub(crate) mel_specification: u8,
+ // ext_asym
+ // ext_hash
+ // resp_alg_struct
+}
+
+impl NegotiateAlgsReq {
+ pub(crate) const WIRE_SIZE: usize = mem::size_of::<SpdmHeader>() + 28;
+
+ pub(crate) fn to_bytes(&self) -> Result<KVec<u8>> {
+ let mut out = self.header.to_bytes()?;
+
+ out.extend_from_slice(&self.length.to_le_bytes(), GFP_KERNEL)?;
+ out.push(self.measurement_specification, GFP_KERNEL)?;
+ out.push(self.other_params_support, GFP_KERNEL)?;
+
+ out.extend_from_slice(&self.base_asym_algo.to_le_bytes(), GFP_KERNEL)?;
+ out.extend_from_slice(&self.base_hash_algo.to_le_bytes(), GFP_KERNEL)?;
+
+ out.extend_from_slice(&[0u8; 12], GFP_KERNEL)?;
+
+ out.push(self.ext_asym_count, GFP_KERNEL)?;
+ out.push(self.ext_hash_count, GFP_KERNEL)?;
+ out.push(0u8, GFP_KERNEL)?;
+ out.push(self.mel_specification, GFP_KERNEL)?;
+
+ Ok(out)
+ }
+}
+
+impl Default for NegotiateAlgsReq {
+ fn default() -> Self {
+ NegotiateAlgsReq {
+ header: SpdmHeader::new(SPDM_NEGOTIATE_ALGS),
+
+ length: 32,
+ measurement_specification: SPDM_MEAS_SPEC_DMTF,
+ other_params_support: 0,
+ base_asym_algo: SPDM_ASYM_ALGOS,
+ base_hash_algo: SPDM_HASH_ALGOS,
+ ext_asym_count: 0,
+ ext_hash_count: 0,
+ mel_specification: 0,
+ }
+ }
+}
+
+/// Size of everything up to the variable-length algorithm arrays (ExtAsymSel).
+pub(crate) const NEGOTIATE_ALGS_RSP_SZ: usize = mem::size_of::<SpdmHeader>() + 32;
+
+#[expect(dead_code)]
+pub(crate) struct NegotiateAlgsRsp {
+ pub(crate) header: SpdmHeader,
+
+ pub(crate) measurement_specification_sel: u8,
+ pub(crate) other_params_sel: u8,
+
+ pub(crate) measurement_hash_algo: u32,
+ pub(crate) base_asym_sel: u32,
+ pub(crate) base_hash_sel: u32,
+
+ pub(crate) mel_specification_sel: u8,
+ pub(crate) ext_asym_sel_count: u8,
+ pub(crate) ext_hash_sel_count: u8,
+
+ pub(crate) ext_asym: KVec<u32>,
+ pub(crate) ext_hash: KVec<u32>,
+ pub(crate) resp_alg_struct: KVec<RespAlgStruct>,
+
+ /// 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 NegotiateAlgsRsp {
+ #[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 NegotiateAlgsRsp
+{
+ 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_NEGOTIATE_ALGS - 0x80 {
+ return Err(EPROTO);
+ }
+
+ if header.version != context.version {
+ pr_err!("Invalid version response\n");
+ return Err(EPROTO);
+ }
+
+ let resp_len = u16::from_le_bytes(
+ unvalidated
+ .get(4..4 + mem::size_of::<u16>())
+ .ok_or(EPROTO)?
+ .try_into()
+ .map_err(|_| EPROTO)?,
+ );
+
+ let measurement_specification_sel = *unvalidated.get(6).ok_or(EPROTO)?;
+ let other_params_sel = *unvalidated.get(7).ok_or(EPROTO)?;
+
+ // Helper to read a little-endian `u32`
+ let read_le32 = |offset: usize| -> Result<u32, Error> {
+ Ok(u32::from_le_bytes(
+ unvalidated
+ .get(offset..offset + mem::size_of::<u32>())
+ .ok_or(EPROTO)?
+ .try_into()
+ .map_err(|_| EPROTO)?,
+ ))
+ };
+
+ let measurement_hash_algo = read_le32(8)?;
+ let base_asym_sel = read_le32(12)?;
+ let base_hash_sel = read_le32(16)?;
+
+ let mel_specification_sel = *unvalidated.get(31).ok_or(EPROTO)?;
+ let ext_asym_sel_count = *unvalidated.get(32).ok_or(EPROTO)?;
+ let ext_hash_sel_count = *unvalidated.get(33).ok_or(EPROTO)?;
+
+ // We currently never request extended algorithms or algorithm structures
+ if header.param1 != 0 || ext_asym_sel_count != 0 || ext_hash_sel_count != 0 {
+ pr_err!("Responder selected unsupported extended algorithms\n");
+ return Err(EPROTO);
+ }
+
+ let mut offset = NEGOTIATE_ALGS_RSP_SZ;
+
+ let mut ext_asym = KVec::new();
+ for _ in 0..ext_asym_sel_count {
+ ext_asym.push(read_le32(offset)?, GFP_KERNEL)?;
+ offset += mem::size_of::<u32>();
+ }
+
+ let mut ext_hash = KVec::new();
+ for _ in 0..ext_hash_sel_count {
+ ext_hash.push(read_le32(offset)?, GFP_KERNEL)?;
+ offset += mem::size_of::<u32>();
+ }
+
+ let mut resp_alg_struct = KVec::new();
+ for _ in 0..header.param1 {
+ let alg_type = *unvalidated.get(offset).ok_or(EPROTO)?;
+ let alg_count = *unvalidated.get(offset + 1).ok_or(EPROTO)?;
+ let fixed_alg_count = (alg_count & 0xf) as usize;
+ let ext_alg_count = (alg_count >> 4) as usize;
+ offset += 2;
+
+ let mut alg_supported = KVec::new();
+ alg_supported.extend_from_slice(
+ unvalidated
+ .get(offset..offset + fixed_alg_count)
+ .ok_or(EPROTO)?,
+ GFP_KERNEL,
+ )?;
+ offset += fixed_alg_count;
+
+ let mut alg_external = KVec::new();
+ for _ in 0..ext_alg_count {
+ alg_external.push(read_le32(offset)?, GFP_KERNEL)?;
+ offset += mem::size_of::<u32>();
+ }
+
+ resp_alg_struct.push(
+ RespAlgStruct {
+ alg_type,
+ alg_count,
+ alg_supported,
+ alg_external,
+ },
+ GFP_KERNEL,
+ )?;
+ }
+
+ if resp_len != offset as u16 {
+ pr_err!("Incorrect response length reported\n");
+ return Err(EPROTO);
+ }
+
+ Ok(NegotiateAlgsRsp {
+ header,
+ measurement_specification_sel,
+ other_params_sel,
+ measurement_hash_algo,
+ base_asym_sel,
+ base_hash_sel,
+ mel_specification_sel,
+ ext_asym_sel_count,
+ ext_hash_sel_count,
+ ext_asym,
+ ext_hash,
+ resp_alg_struct,
+ length: offset,
+ raw_bytes: unvalidated,
+ })
+ }
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* Re: [PATCH v5 17/23] lib: rspdm: Support SPDM negotiate_algorithms
2026-10-27 5:25 ` [PATCH v5 17/23] lib: rspdm: Support SPDM negotiate_algorithms alistair23
@ 2026-10-27 5:43 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-10-27 5:43 UTC (permalink / raw)
To: alistair23, linux-pci, linux-cxl; +Cc: ojeda, gary
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] rspdm: spdm_exchange() passes full request buffer to SpdmHeader::validate()
- [Medium] rspdm: missing minimum buffer length check in NegotiateAlgsRsp::validate()
- [Medium] rspdm: swapped alg_count nibbles in validate() and from_bytes()
- [Medium] rspdm: missing measurement_specification_sel check in negotiate_algs()
- [Low] rspdm: unguarded algorithm constants and imports break Kconfig builds
--
commit f0e28cd8a1d08f541ce0684d3f1feb7d8f5fafa5
Author: Alistair Francis <alistair.francis@wdc.com>
lib: rspdm: Support SPDM negotiate_algorithms
This commit adds support for the SPDM NEGOTIATE_ALGORITHMS request and
response messages, allowing the requester and responder to negotiate
asymmetric, hash, and measurement hash algorithms.
> diff --git a/lib/rspdm/consts.rs b/lib/rspdm/consts.rs
> index fe5c79e0dd911..7a75bd1e4649c 100644
> --- a/lib/rspdm/consts.rs
> +++ b/lib/rspdm/consts.rs
> @@ -45,6 +45,58 @@
>
> pub(crate) const SPDM_CERT_CAP: u32 = bit_u32(1);
> pub(crate) const SPDM_CHAL_CAP: u32 = bit_u32(2);
> +pub(crate) const SPDM_KEY_EX_CAP: u32 = bit_u32(9);
> +pub(crate) const SPDM_MEAS_RESP_CAP: u32 = bit_u32(3) | bit_u32(4);
>
> pub(crate) const SPDM_REQ_CAPS: u32 = SPDM_CERT_CAP | SPDM_CHAL_CAP;
> pub(crate) const SPDM_RSP_MIN_CAPS: u32 = SPDM_CERT_CAP | SPDM_CHAL_CAP;
> +
> +pub(crate) const SPDM_NEGOTIATE_ALGS: u8 = 0xe3;
> +
> +pub(crate) const SPDM_MEAS_SPEC_DMTF: u8 = bit_u8(0);
> +
> +pub(crate) const SPDM_ASYM_RSASSA_2048: u32 = bit_u32(0);
> +pub(crate) const _SPDM_ASYM_RSAPSS_2048: u32 = bit_u32(1);
> +pub(crate) const SPDM_ASYM_RSASSA_3072: u32 = bit_u32(2);
> +pub(crate) const _SPDM_ASYM_RSAPSS_3072: u32 = bit_u32(3);
> +pub(crate) const SPDM_ASYM_ECDSA_ECC_NIST_P256: u32 = bit_u32(4);
> +pub(crate) const SPDM_ASYM_RSASSA_4096: u32 = bit_u32(5);
> +pub(crate) const _SPDM_ASYM_RSAPSS_4096: u32 = bit_u32(6);
> +pub(crate) const SPDM_ASYM_ECDSA_ECC_NIST_P384: u32 = bit_u32(7);
> +pub(crate) const SPDM_ASYM_ECDSA_ECC_NIST_P521: u32 = bit_u32(8);
> +pub(crate) const _SPDM_ASYM_SM2_ECC_SM2_P256: u32 = bit_u32(9);
> +pub(crate) const _SPDM_ASYM_EDDSA_ED25519: u32 = bit_u32(10);
> +pub(crate) const _SPDM_ASYM_EDDSA_ED448: u32 = bit_u32(11);
> +
> +pub(crate) const SPDM_HASH_SHA_256: u32 = bit_u32(0);
> +pub(crate) const SPDM_HASH_SHA_384: u32 = bit_u32(1);
> +pub(crate) const SPDM_HASH_SHA_512: u32 = bit_u32(2);
> +
> +// If the crypto support isn't enabled don't offer the algorithms
> +// to the responder
> +#[cfg(CONFIG_CRYPTO_RSA)]
> +pub(crate) const SPDM_ASYM_RSA: u32 =
> + SPDM_ASYM_RSASSA_2048 | SPDM_ASYM_RSASSA_3072 | SPDM_ASYM_RSASSA_4096;
> +#[cfg(not(CONFIG_CRYPTO_RSA))]
> +pub(crate) const SPDM_ASYM_RSA: u32 = 0;
> +
> +#[cfg(CONFIG_CRYPTO_ECDSA)]
> +pub(crate) const SPDM_ASYM_ECDSA: u32 =
> + SPDM_ASYM_ECDSA_ECC_NIST_P256 | SPDM_ASYM_ECDSA_ECC_NIST_P384 | SPDM_ASYM_ECDSA_ECC_NIST_P521;
> +#[cfg(not(CONFIG_CRYPTO_ECDSA))]
> +pub(crate) const SPDM_ASYM_ECDSA: u32 = 0;
> +
> +#[cfg(CONFIG_CRYPTO_SHA256)]
> +pub(crate) const SPDM_HASH_SHA2_256: u32 = SPDM_HASH_SHA_256;
> +#[cfg(not(CONFIG_CRYPTO_SHA256))]
> +pub(crate) const SPDM_HASH_SHA2_256: u32 = 0;
> +
> +#[cfg(CONFIG_CRYPTO_SHA512)]
> +pub(crate) const SPDM_HASH_SHA2_384_512: u32 = SPDM_HASH_SHA_384 | SPDM_HASH_SHA_512;
> +#[cfg(not(CONFIG_CRYPTO_SHA512))]
> +pub(crate) const SPDM_HASH_SHA2_384_512: u32 = 0;
[ ... ]
> diff --git a/lib/rspdm/state.rs b/lib/rspdm/state.rs
> index 815f332fabad8..79819c492b07b 100644
> --- a/lib/rspdm/state.rs
> +++ b/lib/rspdm/state.rs
> @@ -15,14 +15,30 @@
> Device, //
> },
> error::Error,
> + hash::Shash,
> + str::CStr,
> validate::Untrusted,
> };
>
> use crate::consts::{
> + SPDM_ASYM_ALGOS,
> + SPDM_ASYM_ECDSA_ECC_NIST_P256,
> + SPDM_ASYM_ECDSA_ECC_NIST_P384,
> + SPDM_ASYM_ECDSA_ECC_NIST_P521,
> + SPDM_ASYM_RSASSA_2048,
> + SPDM_ASYM_RSASSA_3072,
> + SPDM_ASYM_RSASSA_4096,
> SPDM_ERROR,
> SPDM_GET_VERSION_LEN,
> + SPDM_HASH_ALGOS,
> + SPDM_HASH_SHA_256,
> + SPDM_HASH_SHA_384,
> + SPDM_HASH_SHA_512,
[ ... ]
> @@ -239,4 +285,113 @@ pub(crate) fn get_capabilities(&mut self) -> Result<(), Error> {
>
> Ok(())
> }
> +
> + fn update_response_algs(&mut self) -> Result<(), Error> {
> + match self.base_asym_alg {
> + #[cfg(CONFIG_CRYPTO_RSA)]
> + SPDM_ASYM_RSASSA_2048 => {
> + self.sig_len = 256;
> + self.base_asym_enc = c"pkcs1";
> + }
> + #[cfg(CONFIG_CRYPTO_RSA)]
> + SPDM_ASYM_RSASSA_3072 => {
> + self.sig_len = 384;
> + self.base_asym_enc = c"pkcs1";
> + }
> + #[cfg(CONFIG_CRYPTO_RSA)]
> + SPDM_ASYM_RSASSA_4096 => {
> + self.sig_len = 512;
> + self.base_asym_enc = c"pkcs1";
> + }
> + #[cfg(CONFIG_CRYPTO_ECDSA)]
> + SPDM_ASYM_ECDSA_ECC_NIST_P256 => {
> + self.sig_len = 64;
> + self.base_asym_enc = c"p1363";
> + }
> + #[cfg(CONFIG_CRYPTO_ECDSA)]
> + SPDM_ASYM_ECDSA_ECC_NIST_P384 => {
> + self.sig_len = 96;
> + self.base_asym_enc = c"p1363";
> + }
> + #[cfg(CONFIG_CRYPTO_ECDSA)]
> + SPDM_ASYM_ECDSA_ECC_NIST_P521 => {
> + self.sig_len = 132;
> + self.base_asym_enc = c"p1363";
> + }
> + _ => {
> + pr_err!("Unknown asym algorithm\n");
> + return Err(EPROTO);
> + }
> + }
> +
> + let hash_name = match self.base_hash_alg {
> + #[cfg(CONFIG_CRYPTO_SHA256)]
> + SPDM_HASH_SHA_256 => c"sha256",
> + #[cfg(CONFIG_CRYPTO_SHA512)]
> + SPDM_HASH_SHA_384 => c"sha384",
> + #[cfg(CONFIG_CRYPTO_SHA512)]
> + SPDM_HASH_SHA_512 => c"sha512",
> + _ => {
> + pr_err!("Unknown hash algorithm\n");
> + return Err(EPROTO);
> + }
> + };
> +
> + self.crypto = Some(Shash::new(hash_name)?);
> +
> + Ok(())
> + }
[Severity: Low]
Will this fail to build when CONFIG_RSPDM=y and any of CONFIG_CRYPTO_RSA,
CONFIG_CRYPTO_ECDSA, CONFIG_CRYPTO_SHA256, or CONFIG_CRYPTO_SHA512 is not
enabled?
The nine algorithm constants (SPDM_ASYM_RSASSA_2048, SPDM_ASYM_RSASSA_3072,
SPDM_ASYM_RSASSA_4096, SPDM_ASYM_ECDSA_ECC_NIST_P256,
SPDM_ASYM_ECDSA_ECC_NIST_P384, SPDM_ASYM_ECDSA_ECC_NIST_P521,
SPDM_HASH_SHA_256, SPDM_HASH_SHA_384, and SPDM_HASH_SHA_512) are defined in
lib/rspdm/consts.rs and imported in lib/rspdm/state.rs unconditionally, while
their only uses in consts.rs and SpdmState::update_response_algs() are gated
behind #[cfg(CONFIG_CRYPTO_*)] attributes.
Additionally, in SpdmState::update_response_algs(), if neither
CONFIG_CRYPTO_RSA nor CONFIG_CRYPTO_ECDSA is enabled, or if neither
CONFIG_CRYPTO_SHA256 nor CONFIG_CRYPTO_SHA512 is enabled, the corresponding
match expression only retains the diverging wildcard arm that returns
Err(EPROTO), making the statements following the match unreachable and Shash
an unused import under -Dwarnings.
> +
> + pub(crate) fn negotiate_algs(&mut self) -> Result<(), Error> {
> + let mut request = NegotiateAlgsReq::default();
> + request.header.version = self.version;
> +
> + if self.version >= SPDM_VER_12 && (self.rsp_caps & SPDM_KEY_EX_CAP) == SPDM_KEY_EX_CAP {
> + request.other_params_support = SPDM_OPAQUE_DATA_FMT_GENERAL;
> + }
> +
> + let rsp_sz =
> + (NEGOTIATE_ALGS_RSP_SZ as u32 + u32::from(u16::MAX)).min(self.transport_sz) as usize;
> +
> + request.length = NegotiateAlgsReq::WIRE_SIZE as u16;
> +
> + let mut request_buf = request.to_bytes()?;
> +
> + let response_vec: KVec<u8> = KVec::from_elem(0u8, rsp_sz, GFP_KERNEL)?;
> +
> + let response: NegotiateAlgsRsp = self
> + .spdm_exchange(request_buf.as_mut_slice(), response_vec)?
> + .validate(&*self)?;
[Severity: High]
Won't SpdmState::negotiate_algs() always fail with Err(EPROTO) inside
SpdmState::spdm_exchange() before sending the request?
In SpdmState::negotiate_algs(), request.to_bytes() serializes a 32-byte
NegotiateAlgsReq buffer and passes the full slice to
SpdmState::spdm_exchange():
lib/rspdm/state.rs:SpdmState::spdm_exchange() {
let header_size = core::mem::size_of::<SpdmHeader>();
let request: SpdmHeader = Untrusted::new(request_buf).validate(self)?;
...
}
Unlike the response validation below it, which slices
response_buf.get(..header_size), SpdmState::spdm_exchange() passes the entire
32-byte request_buf to SpdmHeader::validate():
lib/rspdm/validator.rs:SpdmHeader::validate() {
SpdmHeader::read_from_bytes(unvalidated).or(Err(EPROTO))
}
Because zerocopy::FromBytes::read_from_bytes() requires the slice length to
exactly equal mem::size_of::<SpdmHeader>() (4 bytes), any request message with
a payload beyond the 4-byte header is rejected with Err(EPROTO). Should
SpdmState::spdm_exchange() slice request_buf.get(..header_size) when
validating the request header?
> +
> + self.base_asym_alg = response.base_asym_sel;
> + self.base_hash_alg = response.base_hash_sel;
> + self.meas_hash_alg = response.measurement_hash_algo;
> +
> + if self.base_asym_alg & SPDM_ASYM_ALGOS == 0 || self.base_hash_alg & SPDM_HASH_ALGOS == 0 {
> + pr_err!("No common supported algorithms\n");
> + return Err(EPROTO);
> + }
> +
> + let meas_hash_valid = if self.rsp_caps & SPDM_MEAS_RESP_CAP != 0
> + && response.measurement_specification_sel != 0
> + {
> + self.meas_hash_alg.count_ones() == 1
> + } else {
> + self.meas_hash_alg == 0
> + };
[Severity: Medium]
Can a responder bypass measurement specification validation here by returning
an unrequested or multi-bit value in response.measurement_specification_sel?
In NegotiateAlgsReq::default(), the requester only offers
SPDM_MEAS_SPEC_DMTF (bit 0). However, SpdmState::negotiate_algs() only checks
whether response.measurement_specification_sel != 0 when SPDM_MEAS_RESP_CAP is
set, and does not verify that response.measurement_specification_sel is 0 when
SPDM_MEAS_RESP_CAP is not set.
If a responder sets an unsupported bit (such as 0x02), sets multiple bits
(such as 0x03), or returns a non-zero measurement_specification_sel without
advertising SPDM_MEAS_RESP_CAP (along with measurement_hash_algo == 0),
negotiate_algs() will accept the response. Should this verify that
response.measurement_specification_sel is either 0 or SPDM_MEAS_SPEC_DMTF
(and 0 when SPDM_MEAS_RESP_CAP is absent)?
[ ... ]
> diff --git a/lib/rspdm/validator.rs b/lib/rspdm/validator.rs
> index 080c2f2c49376..9afcb3f8bcb6c 100644
> --- a/lib/rspdm/validator.rs
> +++ b/lib/rspdm/validator.rs
[ ... ]
> @@ -493,3 +497,225 @@ fn validate(unvalidated: KVec<u8>, context: &'c SpdmState<'c, T>) -> Result<Self
> })
> }
> }
[ ... ]
> +impl<'c, T: Transport> Validate<'c, Untrusted<KVec<u8>>, &'c SpdmState<'c, T>>
> + for NegotiateAlgsRsp
> +{
> + type Err = Error;
> +
> + fn validate(unvalidated: KVec<u8>, context: &'c SpdmState<'c, T>) -> Result<Self, Self::Err> {
[ ... ]
> + let mel_specification_sel = *unvalidated.get(31).ok_or(EPROTO)?;
> + let ext_asym_sel_count = *unvalidated.get(32).ok_or(EPROTO)?;
> + let ext_hash_sel_count = *unvalidated.get(33).ok_or(EPROTO)?;
> +
> + // We currently never request extended algorithms or algorithm structures
> + if header.param1 != 0 || ext_asym_sel_count != 0 || ext_hash_sel_count != 0 {
> + pr_err!("Responder selected unsupported extended algorithms\n");
> + return Err(EPROTO);
> + }
> +
> + let mut offset = NEGOTIATE_ALGS_RSP_SZ;
> +
> + let mut ext_asym = KVec::new();
> + for _ in 0..ext_asym_sel_count {
> + ext_asym.push(read_le32(offset)?, GFP_KERNEL)?;
> + offset += mem::size_of::<u32>();
> + }
> +
> + let mut ext_hash = KVec::new();
> + for _ in 0..ext_hash_sel_count {
> + ext_hash.push(read_le32(offset)?, GFP_KERNEL)?;
> + offset += mem::size_of::<u32>();
> + }
> +
> + let mut resp_alg_struct = KVec::new();
> + for _ in 0..header.param1 {
> + let alg_type = *unvalidated.get(offset).ok_or(EPROTO)?;
> + let alg_count = *unvalidated.get(offset + 1).ok_or(EPROTO)?;
> + let fixed_alg_count = (alg_count & 0xf) as usize;
> + let ext_alg_count = (alg_count >> 4) as usize;
[Severity: Medium]
Are fixed_alg_count and ext_alg_count swapped when decoding alg_count here and
in SupportedAlgorithms::from_bytes()?
Per the DMTF SPDM specification (DSP0274) for ReqAlgStruct and RespAlgStruct,
the 1-byte AlgCount field encodes FixedAlgCount in bits [7:4] (alg_count >> 4)
and ExtAlgCount in bits [3:0] (alg_count & 0xf). Both
NegotiateAlgsRsp::validate() and SupportedAlgorithms::from_bytes() extract
fixed_alg_count from the low nibble and ext_alg_count from the high nibble:
lib/rspdm/validator.rs:SupportedAlgorithms::from_bytes() {
...
for _ in 0..alg_struct_count {
let alg_type = *buf.get(offset).ok_or(EPROTO)?;
let alg_count = *buf.get(offset + 1).ok_or(EPROTO)?;
let fixed_alg_count = (alg_count & 0xf) as usize;
let ext_alg_count = (alg_count >> 4) as usize;
...
}
For a standard 4-byte algorithm structure where alg_count is 0x20 (2 fixed
algorithm bytes and 0 extended dwords), this reads 0 bytes of alg_supported
and 8 bytes of alg_external, causing SupportedAlgorithms::from_bytes() to fail
with Err(EPROTO) when validating SPDM 1.3+ capabilities responses. Also, in
NegotiateAlgsRsp::validate(), the check just above rejects any non-zero
header.param1, ext_asym_sel_count, or ext_hash_sel_count, which makes these
three loops in NegotiateAlgsRsp::validate() unreachable.
> + offset += 2;
> +
> + let mut alg_supported = KVec::new();
> + alg_supported.extend_from_slice(
> + unvalidated
> + .get(offset..offset + fixed_alg_count)
> + .ok_or(EPROTO)?,
> + GFP_KERNEL,
> + )?;
> + offset += fixed_alg_count;
> +
> + let mut alg_external = KVec::new();
> + for _ in 0..ext_alg_count {
> + alg_external.push(read_le32(offset)?, GFP_KERNEL)?;
> + offset += mem::size_of::<u32>();
> + }
> +
> + resp_alg_struct.push(
> + RespAlgStruct {
> + alg_type,
> + alg_count,
> + alg_supported,
> + alg_external,
> + },
> + GFP_KERNEL,
> + )?;
> + }
> +
> + if resp_len != offset as u16 {
> + pr_err!("Incorrect response length reported\n");
> + return Err(EPROTO);
> + }
[Severity: Medium]
What happens if a responder returns a truncated 34- or 35-byte buffer whose
header resp_len field is set to 36 (NEGOTIATE_ALGS_RSP_SZ)?
In NegotiateAlgsRsp::validate(), the last byte accessed from unvalidated is at
index 33 (ext_hash_sel_count), leaving the two reserved bytes at offsets 34
and 35 unread. Because the loops are skipped when counts are zero, offset
remains NEGOTIATE_ALGS_RSP_SZ (36), and the check only compares the untrusted
resp_len field from the packet against offset without verifying that
unvalidated.len() >= offset.
If unvalidated is 34 or 35 bytes long with resp_len == 36, validate() succeeds
and sets NegotiateAlgsRsp.length to 36 while raw_bytes is shorter than 36
bytes, causing NegotiateAlgsRsp::as_bytes() to fail with Err(EPROTO). Should
NegotiateAlgsRsp::validate() check that unvalidated.len() >= offset?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027052536.1722081-1-alistair.francis@wdc.com?part=17
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH v5 18/23] lib: rspdm: Support SPDM get_digests
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (16 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 17/23] lib: rspdm: Support SPDM negotiate_algorithms alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:25 ` [PATCH v5 19/23] lib: rspdm: Support SPDM get_certificate alistair23
` (4 subsequent siblings)
22 siblings, 0 replies; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis, Jonathan Cameron
From: Alistair Francis <alistair.francis@wdc.com>
Support the GET_DIGESTS SPDM command.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
Reviewed-by: Jonathan Cameron <jonathan.cameron@oss.qualcomm.com>
---
lib/rspdm/consts.rs | 5 +-
lib/rspdm/lib.rs | 4 ++
lib/rspdm/state.rs | 61 +++++++++++++++++++++++-
lib/rspdm/validator.rs | 102 ++++++++++++++++++++++++++++++++++++++++-
4 files changed, 169 insertions(+), 3 deletions(-)
diff --git a/lib/rspdm/consts.rs b/lib/rspdm/consts.rs
index 7a75bd1e4649c..68f347a046859 100644
--- a/lib/rspdm/consts.rs
+++ b/lib/rspdm/consts.rs
@@ -18,10 +18,11 @@
pub(crate) const SPDM_VER_10: u8 = 0x10;
pub(crate) const SPDM_VER_11: u8 = 0x11;
pub(crate) const SPDM_VER_12: u8 = 0x12;
-#[expect(dead_code)]
pub(crate) const SPDM_VER_13: u8 = 0x13;
pub(crate) const SPDM_VER_14: u8 = 0x14;
+pub(crate) const SPDM_SLOTS: usize = 8;
+
pub(crate) const SPDM_MIN_VER: u8 = SPDM_VER_10;
pub(crate) const SPDM_MAX_VER: u8 = SPDM_VER_14;
@@ -72,6 +73,8 @@
pub(crate) const SPDM_HASH_SHA_384: u32 = bit_u32(1);
pub(crate) const SPDM_HASH_SHA_512: u32 = bit_u32(2);
+pub(crate) const SPDM_GET_DIGESTS: u8 = 0x81;
+
// 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 4c219b32098ab..ee7e9be5b41c1 100644
--- a/lib/rspdm/lib.rs
+++ b/lib/rspdm/lib.rs
@@ -270,6 +270,10 @@ pub extern "C" fn spdm_authenticate(state_ptr: *mut spdm_state) -> c_int {
return e.to_errno() as c_int;
}
+ if let Err(e) = state.get_digests() {
+ return e.to_errno() as c_int;
+ }
+
-(EPROTONOSUPPORT as i32)
}
diff --git a/lib/rspdm/state.rs b/lib/rspdm/state.rs
index 79819c492b07b..73c39847d7335 100644
--- a/lib/rspdm/state.rs
+++ b/lib/rspdm/state.rs
@@ -41,13 +41,17 @@
SPDM_OPAQUE_DATA_FMT_GENERAL,
SPDM_REQ,
SPDM_RSP_MIN_CAPS,
+ SPDM_SLOTS,
SPDM_VER_10,
SPDM_VER_11,
- SPDM_VER_12, //
+ SPDM_VER_12,
+ SPDM_VER_13, //
};
use crate::validator::{
GetCapabilitiesReq,
GetCapabilitiesRsp,
+ GetDigestsReq,
+ GetDigestsRsp,
GetVersionReq,
GetVersionRsp,
NegotiateAlgsReq,
@@ -119,6 +123,12 @@ pub(crate) struct SpdmState<'a, T: Transport> {
/// Hash algorithm for measurement blocks. Selected by responder during
/// NEGOTIATE_ALGORITHMS exchange.
pub(crate) meas_hash_alg: u32,
+ /// Bitmask of responder's supported certificate slots. Received during
+ /// GET_DIGESTS exchange (from SPDM 1.3).
+ pub(crate) supported_slots: u8,
+ /// Bitmask of responder's provisioned certificate slots. Received during
+ /// GET_DIGESTS exchange.
+ pub(crate) provisioned_slots: u8,
/// Human-readable name of `base_asym_alg`'s signature encoding. Passed
/// to the crypto subsystem when calling `verify_signature()`.
@@ -129,6 +139,10 @@ pub(crate) struct SpdmState<'a, T: Transport> {
/// Hash transformation for `base_hash_alg` computation.
crypto: Option<Shash>,
+
+ /// Certificate chain in each of the 8 slots. Empty `KVec` if a slot is
+ /// not populated. Prefixed by the 4 + H header per SPDM 1.0.0 table 15.
+ pub(crate) certs: [KVec<u8>; SPDM_SLOTS],
}
impl<'a, T: Transport> SpdmState<'a, T> {
@@ -148,12 +162,23 @@ pub(crate) fn new(
base_asym_alg: 0,
base_hash_alg: 0,
meas_hash_alg: 0,
+ supported_slots: 0,
+ provisioned_slots: 0,
base_asym_enc: c"",
sig_len: 0,
crypto: None,
+ certs: [const { KVec::new() }; SPDM_SLOTS],
}
}
+ /// Hash length of `base_hash_alg` (in bytes). H in SPDM specification.
+ pub(crate) fn hash_len(&self) -> usize {
+ self.crypto
+ .as_ref()
+ .map(|hash| hash.hash_len())
+ .unwrap_or(0)
+ }
+
/// Start a SPDM exchange
///
/// The data in `request_buf` is sent to the device and the response
@@ -394,4 +419,38 @@ pub(crate) fn negotiate_algs(&mut self) -> Result<(), Error> {
Ok(())
}
+
+ pub(crate) fn get_digests(&mut self) -> Result<(), Error> {
+ let mut request = GetDigestsReq::default();
+ request.header.version = self.version;
+
+ let rsp_sz = core::mem::size_of::<SpdmHeader>() + SPDM_SLOTS * self.hash_len();
+
+ let mut request_buf = request.to_bytes()?;
+
+ let response_vec: KVec<u8> = KVec::from_elem(0u8, rsp_sz, GFP_KERNEL)?;
+
+ let response: GetDigestsRsp = self
+ .spdm_exchange(request_buf.as_mut_slice(), response_vec)?
+ .validate(&*self)?;
+
+ let mut deprovisioned_slots = self.provisioned_slots & !response.header.param2;
+ while (deprovisioned_slots.trailing_zeros() as usize) < SPDM_SLOTS {
+ let slot = deprovisioned_slots.trailing_zeros() as usize;
+ self.certs[slot].clear();
+ deprovisioned_slots &= !(1 << slot);
+ }
+
+ self.provisioned_slots = response.header.param2;
+
+ let supported_slots = if self.version >= SPDM_VER_13 {
+ response.header.param1
+ } else {
+ 0xFF
+ };
+
+ self.supported_slots = supported_slots;
+
+ Ok(())
+ }
}
diff --git a/lib/rspdm/validator.rs b/lib/rspdm/validator.rs
index 9afcb3f8bcb6c..f7ad46aef1b88 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_DIGESTS,
SPDM_GET_VERSION,
SPDM_HASH_ALGOS,
SPDM_MEAS_SPEC_DMTF,
@@ -19,9 +20,11 @@
SPDM_MIN_VER,
SPDM_NEGOTIATE_ALGS,
SPDM_REQ_CAPS,
+ SPDM_SLOTS,
SPDM_VER_10,
SPDM_VER_11,
- SPDM_VER_12, //
+ SPDM_VER_12,
+ SPDM_VER_13, //
};
use crate::{
SpdmErrorCode,
@@ -719,3 +722,100 @@ fn validate(unvalidated: KVec<u8>, context: &'c SpdmState<'c, T>) -> Result<Self
})
}
}
+
+pub(crate) struct GetDigestsReq {
+ pub(crate) header: SpdmHeader,
+}
+
+impl GetDigestsReq {
+ pub(crate) fn to_bytes(&self) -> Result<KVec<u8>> {
+ self.header.to_bytes()
+ }
+}
+
+impl Default for GetDigestsReq {
+ fn default() -> Self {
+ GetDigestsReq {
+ header: SpdmHeader::new(SPDM_GET_DIGESTS),
+ }
+ }
+}
+
+#[expect(dead_code)]
+pub(crate) struct GetDigestsRsp {
+ pub(crate) header: SpdmHeader,
+
+ pub(crate) digests: [KVec<u8>; SPDM_SLOTS],
+
+ // KeyPairIDs, added in 1.3
+
+ // CertificateInfo, added in 1.3
+
+ // KeyUsageMask, added in 1.3
+ /// 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 GetDigestsRsp {
+ #[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 GetDigestsRsp {
+ 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_DIGESTS - 0x80 {
+ return Err(EPROTO);
+ }
+
+ if header.version != context.version {
+ pr_err!("Invalid version response\n");
+ return Err(EPROTO);
+ }
+
+ if header.param2 == 0 {
+ pr_err!("No certificates provisioned\n");
+ return Err(EPROTO);
+ }
+
+ let mut digests: [KVec<u8>; SPDM_SLOTS] = [const { KVec::new() }; SPDM_SLOTS];
+ let mut offset = mem::size_of::<SpdmHeader>();
+ let mut slot_mask = header.param2;
+
+ while (slot_mask.trailing_zeros() as usize) < SPDM_SLOTS {
+ let slot = slot_mask.trailing_zeros() as usize;
+
+ digests[slot].extend_from_slice(
+ unvalidated
+ .get(offset..(offset + context.hash_len()))
+ .ok_or(EPROTO)?,
+ GFP_KERNEL,
+ )?;
+ offset += context.hash_len();
+
+ slot_mask &= !(1 << slot);
+ }
+
+ if context.version >= SPDM_VER_13 && (header.param2 & !header.param1 != 0) {
+ pr_err!("Malformed digests response\n");
+ return Err(EPROTO);
+ }
+
+ Ok(GetDigestsRsp {
+ header,
+ digests,
+ length: offset,
+ raw_bytes: unvalidated,
+ })
+ }
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* [PATCH v5 19/23] lib: rspdm: Support SPDM get_certificate
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (17 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 18/23] lib: rspdm: Support SPDM get_digests alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:25 ` [PATCH v5 20/23] rust: public_key: Add crypto public key bindings alistair23
` (3 subsequent siblings)
22 siblings, 0 replies; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Alistair Francis <alistair.francis@wdc.com>
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@wdc.com>
---
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
^ permalink raw reply related [flat|nested] 37+ messages in thread* [PATCH v5 20/23] rust: public_key: Add crypto public key bindings
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (18 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 19/23] lib: rspdm: Support SPDM get_certificate alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:41 ` sashiko-bot
2026-10-27 5:25 ` [PATCH v5 21/23] rust: x509: Add X509 certificate bindings alistair23
` (2 subsequent siblings)
22 siblings, 1 reply; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Alistair Francis <alistair.francis@wdc.com>
Initial support for the kernel crypto public key (asymmetric-keys) bindings,
which will be used by the SPDM library.
This is just a few functions that the Rust SPDM implementation will use
in the next few commits.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
rust/bindings/bindings_helper.h | 1 +
rust/kernel/lib.rs | 2 +
rust/kernel/public_key.rs | 69 +++++++++++++++++++++++++++++++++
3 files changed, 72 insertions(+)
create mode 100644 rust/kernel/public_key.rs
diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h
index a2512fda77e1f..da8855899c530 100644
--- a/rust/bindings/bindings_helper.h
+++ b/rust/bindings/bindings_helper.h
@@ -29,6 +29,7 @@
#include <linux/hrtimer_types.h>
#include <crypto/hash.h>
+#include <crypto/public_key.h>
#include <linux/acpi.h>
#include <linux/gpu_buddy.h>
#include <drm/drm_device.h>
diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
index f7db5975e7621..e8b652faa3b49 100644
--- a/rust/kernel/lib.rs
+++ b/rust/kernel/lib.rs
@@ -122,6 +122,8 @@
pub mod print;
pub mod processor;
pub mod ptr;
+#[cfg(CONFIG_CRYPTO)]
+pub mod public_key;
#[cfg(CONFIG_RUST_PWM_ABSTRACTIONS)]
pub mod pwm;
pub mod ratelimit;
diff --git a/rust/kernel/public_key.rs b/rust/kernel/public_key.rs
new file mode 100644
index 0000000000000..e1d51333c1179
--- /dev/null
+++ b/rust/kernel/public_key.rs
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-2.0
+
+// Copyright (C) 2026 Western Digital
+
+//! Wrappers around the Linux kernel crypto public key APIs.
+//!
+//! C headers: [`include/crypto/public_key.h`](srctree/include/crypto/public_key.h).
+//!
+//! Reference: <https://www.kernel.org/doc/html/latest/crypto/asymmetric-keys.html>
+
+use core::ptr::NonNull;
+use kernel::prelude::*;
+use kernel::{
+ bindings,
+ error::to_result,
+ str::CStr, //
+};
+
+/// Owned `struct public_key` wrapper.
+pub struct PublicKey {
+ key: NonNull<bindings::public_key>,
+}
+
+impl PublicKey {
+ /// Create a Rust `PublicKey` from a valid C `struct public_key` pointer.
+ pub fn from_raw(key: NonNull<bindings::public_key>) -> Self {
+ Self { key }
+ }
+
+ /// Verify a signature using this public key.
+ pub fn verify_signature(
+ &self,
+ signature: &[u8],
+ encoding: &'static CStr,
+ hash_algo: &'static CStr,
+ message: &[u8],
+ ) -> Result {
+ let sig = bindings::public_key_signature {
+ s: signature.as_ptr().cast_mut(),
+ s_size: signature.len() as u32,
+ encoding: encoding.as_char_ptr().cast::<u8>(),
+ hash_algo: hash_algo.as_char_ptr().cast::<u8>(),
+ m: message.as_ptr().cast_mut(),
+ m_size: message.len() as u32,
+ ..Default::default()
+ };
+
+ // SAFETY: `self.key` is a valid public key handle, and `sig` points to
+ // valid signature/message data owned by the caller for the duration of
+ // this call.
+ unsafe {
+ to_result(bindings::public_key_verify_signature(
+ self.key.as_ptr(),
+ &sig,
+ ))
+ }
+ }
+}
+
+impl Drop for PublicKey {
+ fn drop(&mut self) {
+ // SAFETY: `self.key` was either returned by `x509_cert_parse()` or
+ // manually transferred from an `x509_certificate` and has not been
+ // freed yet.
+ unsafe {
+ bindings::public_key_free(self.key.as_ptr());
+ }
+ }
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* Re: [PATCH v5 20/23] rust: public_key: Add crypto public key bindings
2026-10-27 5:25 ` [PATCH v5 20/23] rust: public_key: Add crypto public key bindings alistair23
@ 2026-10-27 5:41 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-10-27 5:41 UTC (permalink / raw)
To: alistair23, linux-pci, linux-cxl; +Cc: ojeda, gary
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] rust: public_key: missing unsafe on PublicKey::from_raw()
- [Low] rust: public_key: wrong Kconfig guard on public_key module in lib.rs
- [Low] rust: public_key: missing inline attributes and split imports
--
commit d3b51b7112cb9e69d8cc7083f31a2a45bf366492
Author: Alistair Francis <alistair.francis@wdc.com>
rust: public_key: Add crypto public key bindings
This commit adds initial Rust bindings and a PublicKey wrapper around
the kernel crypto public key (asymmetric-keys) APIs, including
PublicKey::from_raw(), PublicKey::verify_signature(), and Drop cleanup via
public_key_free().
> diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
> index f7db5975e7621..e8b652faa3b49 100644
> --- a/rust/kernel/lib.rs
> +++ b/rust/kernel/lib.rs
> @@ -122,6 +122,8 @@
> pub mod print;
> pub mod processor;
> pub mod ptr;
> +#[cfg(CONFIG_CRYPTO)]
> +pub mod public_key;
[Severity: Low]
Will gating the public_key module on CONFIG_CRYPTO break the build when
CONFIG_RUST=y and CONFIG_CRYPTO=y, while
CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE is n or m?
In rust/kernel/public_key.rs, PublicKey::verify_signature() calls
bindings::public_key_verify_signature() and PublicKey::drop() calls
bindings::public_key_free(). Both functions are defined in
crypto/asymmetric_keys/public_key.c, which is built under the tristate
symbol CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE rather than CONFIG_CRYPTO:
crypto/asymmetric_keys/Makefile:
obj-$(CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE) += public_key.o
When CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE=n, include/crypto/public_key.h
defines public_key_verify_signature() as a static inline stub:
include/crypto/public_key.h:
#if IS_REACHABLE(CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE)
int public_key_verify_signature(const struct public_key *pkey,
const struct public_key_signature *sig);
#else
static inline
int public_key_verify_signature(const struct public_key *pkey,
const struct public_key_signature *sig)
{
return -EINVAL;
}
#endif
Without a Rust helper wrapper, bindgen does not generate
bindings::public_key_verify_signature, and public_key_free() has no stub at
all. Additionally, when CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE=m, built-in
rust/kernel.o cannot link against public_key_verify_signature() and
public_key_free() in the loadable public_key.ko module.
Should this module instead be gated on
#[cfg(CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE = "y")]?
> diff --git a/rust/kernel/public_key.rs b/rust/kernel/public_key.rs
> new file mode 100644
> index 0000000000000..e1d51333c1179
> --- /dev/null
> +++ b/rust/kernel/public_key.rs
> @@ -0,0 +1,69 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +// Copyright (C) 2026 Western Digital
> +
> +//! Wrappers around the Linux kernel crypto public key APIs.
> +//!
> +//! C headers: [`include/crypto/public_key.h`](srctree/include/crypto/public_key.h).
> +//!
> +//! Reference: <https://www.kernel.org/doc/html/latest/crypto/asymmetric-keys.html>
> +
> +use core::ptr::NonNull;
> +use kernel::prelude::*;
> +use kernel::{
> + bindings,
> + error::to_result,
> + str::CStr, //
> +};
> +
> +/// Owned `struct public_key` wrapper.
> +pub struct PublicKey {
> + key: NonNull<bindings::public_key>,
> +}
> +
> +impl PublicKey {
> + /// Create a Rust `PublicKey` from a valid C `struct public_key` pointer.
> + pub fn from_raw(key: NonNull<bindings::public_key>) -> Self {
> + Self { key }
> + }
[Severity: Medium]
Should PublicKey::from_raw() be marked unsafe?
NonNull<bindings::public_key> is Copy and can be constructed or duplicated
in safe Rust (for example via NonNull::dangling() or passing the same
NonNull pointer twice). However, PublicKey::verify_signature() passes
self.key.as_ptr() to bindings::public_key_verify_signature(), which
dereferences the pointer and its internal fields, and PublicKey::drop()
unconditionally passes self.key.as_ptr() to bindings::public_key_free(),
which calls kfree_sensitive(key->key), kfree(key->params), and kfree(key).
Although Certificate::take_public_key() in a later commit in this series
passes a valid pointer and clears cert->pub_, leaving PublicKey::from_raw()
as a safe public function makes the abstraction unsound for safe callers.
Could PublicKey document its type invariants in an Invariants section, and
could PublicKey::from_raw() be made an unsafe fn with a Safety section and
an invariant comment?
> +
> + /// Verify a signature using this public key.
> + pub fn verify_signature(
> + &self,
> + signature: &[u8],
> + encoding: &'static CStr,
> + hash_algo: &'static CStr,
> + message: &[u8],
> + ) -> Result {
> + let sig = bindings::public_key_signature {
> + s: signature.as_ptr().cast_mut(),
> + s_size: signature.len() as u32,
> + encoding: encoding.as_char_ptr().cast::<u8>(),
> + hash_algo: hash_algo.as_char_ptr().cast::<u8>(),
> + m: message.as_ptr().cast_mut(),
> + m_size: message.len() as u32,
> + ..Default::default()
> + };
> +
> + // SAFETY: `self.key` is a valid public key handle, and `sig` points to
> + // valid signature/message data owned by the caller for the duration of
> + // this call.
> + unsafe {
> + to_result(bindings::public_key_verify_signature(
> + self.key.as_ptr(),
> + &sig,
> + ))
> + }
> + }
> +}
> +
> +impl Drop for PublicKey {
> + fn drop(&mut self) {
> + // SAFETY: `self.key` was either returned by `x509_cert_parse()` or
> + // manually transferred from an `x509_certificate` and has not been
> + // freed yet.
> + unsafe {
> + bindings::public_key_free(self.key.as_ptr());
> + }
> + }
> +}
[Severity: Low]
This isn't a bug, but per the Rust coding guidelines for the kernel crate,
could PublicKey::from_raw(), PublicKey::verify_signature(), and
PublicKey::drop() be annotated with #[inline], and could the two
use kernel::... blocks at the top of rust/kernel/public_key.rs be merged
into a single vertical import block (noting that CStr is already exported by
kernel::prelude::*)?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027052536.1722081-1-alistair.francis@wdc.com?part=20
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH v5 21/23] rust: x509: Add X509 certificate bindings
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (19 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 20/23] rust: public_key: Add crypto public key bindings alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:45 ` sashiko-bot
2026-10-27 5:25 ` [PATCH v5 22/23] lib: rspdm: Support SPDM certificate validation alistair23
2026-10-27 5:25 ` [PATCH v5 23/23] lib: rspdm: Support SPDM challenge alistair23
22 siblings, 1 reply; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Alistair Francis <alistair.francis@wdc.com>
Initial support for the kernel x509 parser bindings,
which will be used by the SPDM library.
This is just a few functions that the Rust SPDM implementation will use
in the next few commits.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
rust/bindings/bindings_helper.h | 2 +
rust/kernel/lib.rs | 2 +
rust/kernel/x509.rs | 118 ++++++++++++++++++++++++++++++++
3 files changed, 122 insertions(+)
create mode 100644 rust/kernel/x509.rs
diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h
index da8855899c530..66e18819d52b0 100644
--- a/rust/bindings/bindings_helper.h
+++ b/rust/bindings/bindings_helper.h
@@ -39,6 +39,8 @@
#include <drm/drm_gem_shmem_helper.h>
#include <drm/drm_gpuvm.h>
#include <drm/drm_ioctl.h>
+#include <keys/asymmetric-type.h>
+#include <keys/x509-parser.h>
#include <kunit/test.h>
#include <linux/auxiliary_bus.h>
#include <linux/bitmap.h>
diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
index e8b652faa3b49..aa95aafff8c49 100644
--- a/rust/kernel/lib.rs
+++ b/rust/kernel/lib.rs
@@ -153,6 +153,8 @@
pub mod usb;
pub mod validate;
pub mod workqueue;
+#[cfg(CONFIG_CRYPTO)]
+pub mod x509;
pub mod xarray;
#[doc(hidden)]
diff --git a/rust/kernel/x509.rs b/rust/kernel/x509.rs
new file mode 100644
index 0000000000000..1b5bc2a99c91c
--- /dev/null
+++ b/rust/kernel/x509.rs
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: GPL-2.0
+
+// Copyright (C) 2026 Western Digital
+
+//! Wrappers around the Linux kernel X509 parser APIs.
+//!
+//! C headers: [`include/keys/x509-parser.h`](srctree/include/keys/x509-parser.h).
+
+use core::ffi::c_void;
+use core::ptr::{self, NonNull};
+use kernel::prelude::*;
+use kernel::{
+ bindings,
+ error::{
+ from_err_ptr,
+ to_result, //
+ },
+ public_key::PublicKey,
+};
+
+/// Owned `struct x509_certificate` wrapper.
+pub struct Certificate {
+ cert: NonNull<bindings::x509_certificate>,
+}
+
+impl Certificate {
+ /// Parses a DER certificate from `data` and returns it with the length
+ /// of the certificate in bytes.
+ pub fn parse(data: &[u8]) -> Result<(Self, usize)> {
+ // SAFETY: `data.as_ptr()` points to at least `data.len()` valid bytes.
+ let len = unsafe { bindings::x509_get_certificate_length(data.as_ptr(), data.len()) };
+ let len = usize::try_from(len).map_err(|_| {
+ pr_err!("Invalid certificate length\n");
+ EPROTO
+ })?;
+
+ if len > data.len() {
+ pr_err!("Certificate length is larger then the buffer\n");
+ return Err(EPROTO);
+ }
+
+ // SAFETY: `data.as_ptr()` points to `data.len()` valid bytes and the
+ // caller has already checked that the DER certificate length fits in
+ // that buffer.
+ let cert = from_err_ptr(unsafe {
+ bindings::x509_cert_parse(data.as_ptr().cast::<c_void>(), len)
+ })?;
+ let cert = NonNull::new(cert).ok_or(EINVAL)?;
+
+ Ok((Self { cert }, len))
+ }
+
+ /// Returns the raw pointer for the X.509 certificate.
+ /// This is what is returned from the C function x509_cert_parse()
+ pub fn raw(&self) -> *mut bindings::x509_certificate {
+ self.cert.as_ptr()
+ }
+
+ /// Check that the certificate is not unsupported or blacklisted.
+ pub fn check_accepted(&self) -> Result {
+ // SAFETY: `self.cert` was returned by `x509_cert_parse()` and remains
+ // valid until dropped.
+ let cert = unsafe { &*self.cert.as_ptr() };
+ if cert.unsupported_sig || cert.blacklisted {
+ pr_err!("Certificate was rejected\n");
+ return Err(EKEYREJECTED);
+ }
+
+ Ok(())
+ }
+
+ /// Verify that `child`'s signature is signed by this cert's public/private key.
+ pub fn verify_signed(&self, child: &Self) -> Result {
+ // SAFETY: both certificate pointers were produced by `x509_cert_parse()`
+ // and remain valid until dropped.
+ let (prev, child) = unsafe { (&*self.cert.as_ptr(), &*child.cert.as_ptr()) };
+
+ let prev_pub = NonNull::new(prev.pub_).ok_or(EINVAL)?;
+ let child_sig = NonNull::new(child.sig).ok_or(EINVAL)?;
+
+ // SAFETY: `prev_pub` and `child_sig` are valid for at least the duration
+ // of this call.
+ unsafe {
+ to_result(bindings::public_key_verify_signature(
+ prev_pub.as_ptr(),
+ child_sig.as_ptr(),
+ ))
+ }
+ }
+
+ /// Transfers the public key out of the certificate so the certificate can be
+ /// dropped without freeing the public key.
+ pub fn take_public_key(&mut self) -> Option<PublicKey> {
+ // SAFETY: `self.cert` is a valid certificate pointer.
+ let raw = unsafe { (*self.cert.as_ptr()).pub_ };
+ let key = NonNull::new(raw)?;
+
+ // SAFETY: `self.cert` is a valid certificate pointer. Clearing `pub_`
+ // transfers ownership of the public key to the returned `PublicKey`.
+ // The `pub_` field will not be freed by `x509_free_certificate()` in
+ // Drop
+ unsafe {
+ (*self.cert.as_ptr()).pub_ = ptr::null_mut();
+ }
+
+ Some(PublicKey::from_raw(key))
+ }
+}
+
+impl Drop for Certificate {
+ fn drop(&mut self) {
+ // SAFETY: `self.cert` was produced by `x509_cert_parse()` and has
+ // not been freed yet.
+ unsafe {
+ bindings::x509_free_certificate(self.cert.as_ptr());
+ }
+ }
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* Re: [PATCH v5 21/23] rust: x509: Add X509 certificate bindings
2026-10-27 5:25 ` [PATCH v5 21/23] rust: x509: Add X509 certificate bindings alistair23
@ 2026-10-27 5:45 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-10-27 5:45 UTC (permalink / raw)
To: alistair23, linux-pci, linux-cxl; +Cc: ojeda, gary
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] rust: x509: build and link failure when CONFIG_X509_CERTIFICATE_PARSER != y
- [Medium] rust: bindings: build failure in bindings_helper.h when CONFIG_KEYS=n
- [Low] rust: x509: missing inline attributes, import style, and log typo
--
commit bd0a2a1272842d031d7bfad2c6e145a2a0b663dc
Author: Alistair Francis <alistair.francis@wdc.com>
rust: x509: Add X509 certificate bindings
This commit adds initial Rust bindings and a Certificate wrapper around
the kernel X.509 certificate parser APIs for use by the Rust SPDM
implementation.
> diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h
> index da8855899c530..66e18819d52b0 100644
> --- a/rust/bindings/bindings_helper.h
> +++ b/rust/bindings/bindings_helper.h
> @@ -39,6 +39,8 @@
> #include <drm/drm_gem_shmem_helper.h>
> #include <drm/drm_gpuvm.h>
> #include <drm/drm_ioctl.h>
> +#include <keys/asymmetric-type.h>
> +#include <keys/x509-parser.h>
[Severity: Medium]
Does including keys/asymmetric-type.h and keys/x509-parser.h
unconditionally break bindgen when CONFIG_KEYS is disabled?
In include/linux/key.h, struct key is only defined under #ifdef CONFIG_KEYS
and is otherwise only forward-declared:
include/linux/key.h:
struct key;
struct net;
#ifdef CONFIG_KEYS
However, include/keys/asymmetric-type.h defines inline functions outside
any #ifdef CONFIG_KEYS guard that dereference const struct key *:
include/keys/asymmetric-type.h:
static inline
const struct asymmetric_key_ids *asymmetric_key_ids(const struct key *key)
{
return key->payload.data[asym_key_ids];
}
When building with CONFIG_RUST=y and CONFIG_KEYS=n, clang fails during
bindgen generation with an incomplete type error on struct key. Should
these headers be guarded or should struct key handling in
include/keys/asymmetric-type.h account for !CONFIG_KEYS?
> diff --git a/rust/kernel/lib.rs b/rust/kernel/lib.rs
> index e8b652faa3b49..aa95aafff8c49 100644
> --- a/rust/kernel/lib.rs
> +++ b/rust/kernel/lib.rs
> @@ -153,6 +153,8 @@
> pub mod usb;
> pub mod validate;
> pub mod workqueue;
> +#[cfg(CONFIG_CRYPTO)]
> +pub mod x509;
[Severity: Medium]
Should the x509 module be gated on
#[cfg(CONFIG_X509_CERTIFICATE_PARSER = "y")] instead of
#[cfg(CONFIG_CRYPTO)]?
Because rust/kernel.o is always built-in when CONFIG_RUST=y, non-generic
methods in rust/kernel/x509.rs such as Certificate::parse(),
Certificate::verify_signed(), and Certificate::drop() are compiled directly
into vmlinux whenever CONFIG_CRYPTO is enabled.
In Certificate::parse() and Certificate::drop(), the code calls
bindings::x509_get_certificate_length(), bindings::x509_cert_parse(), and
bindings::x509_free_certificate(), which are only built under
CONFIG_X509_CERTIFICATE_PARSER in crypto/asymmetric_keys/Makefile:
obj-$(CONFIG_X509_CERTIFICATE_PARSER) += x509_key_parser.o
x509_key_parser-y := \
x509.asn1.o \
x509_akid.asn1.o \
x509_cert_parser.o \
x509_loader.o \
x509_public_key.o
Similarly, Certificate::verify_signed() calls
bindings::public_key_verify_signature(), which is built under
CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE (selected by
CONFIG_X509_CERTIFICATE_PARSER) and falls back to a static inline stub in
include/crypto/public_key.h when disabled, which bindgen does not expose
without a rust_helper_* wrapper.
If CONFIG_CRYPTO is enabled while CONFIG_X509_CERTIFICATE_PARSER is n or m,
won't this fail to compile or fail to link vmlinux with undefined references
from rust/kernel.o?
> diff --git a/rust/kernel/x509.rs b/rust/kernel/x509.rs
> new file mode 100644
> index 0000000000000..1b5bc2a99c91c
> --- /dev/null
> +++ b/rust/kernel/x509.rs
> @@ -0,0 +1,118 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +// Copyright (C) 2026 Western Digital
> +
> +//! Wrappers around the Linux kernel X509 parser APIs.
> +//!
> +//! C headers: [`include/keys/x509-parser.h`](srctree/include/keys/x509-parser.h).
> +
> +use core::ffi::c_void;
> +use core::ptr::{self, NonNull};
> +use kernel::prelude::*;
> +use kernel::{
> + bindings,
> + error::{
> + from_err_ptr,
> + to_result, //
> + },
> + public_key::PublicKey,
> +};
> +
> +/// Owned `struct x509_certificate` wrapper.
> +pub struct Certificate {
> + cert: NonNull<bindings::x509_certificate>,
> +}
> +
> +impl Certificate {
> + /// Parses a DER certificate from `data` and returns it with the length
> + /// of the certificate in bytes.
> + pub fn parse(data: &[u8]) -> Result<(Self, usize)> {
> + // SAFETY: `data.as_ptr()` points to at least `data.len()` valid bytes.
> + let len = unsafe { bindings::x509_get_certificate_length(data.as_ptr(), data.len()) };
> + let len = usize::try_from(len).map_err(|_| {
> + pr_err!("Invalid certificate length\n");
> + EPROTO
> + })?;
> +
> + if len > data.len() {
> + pr_err!("Certificate length is larger then the buffer\n");
> + return Err(EPROTO);
> + }
> +
> + // SAFETY: `data.as_ptr()` points to `data.len()` valid bytes and the
> + // caller has already checked that the DER certificate length fits in
> + // that buffer.
> + let cert = from_err_ptr(unsafe {
> + bindings::x509_cert_parse(data.as_ptr().cast::<c_void>(), len)
> + })?;
> + let cert = NonNull::new(cert).ok_or(EINVAL)?;
> +
> + Ok((Self { cert }, len))
> + }
> +
> + /// Returns the raw pointer for the X.509 certificate.
> + /// This is what is returned from the C function x509_cert_parse()
> + pub fn raw(&self) -> *mut bindings::x509_certificate {
> + self.cert.as_ptr()
> + }
> +
> + /// Check that the certificate is not unsupported or blacklisted.
> + pub fn check_accepted(&self) -> Result {
> + // SAFETY: `self.cert` was returned by `x509_cert_parse()` and remains
> + // valid until dropped.
> + let cert = unsafe { &*self.cert.as_ptr() };
> + if cert.unsupported_sig || cert.blacklisted {
> + pr_err!("Certificate was rejected\n");
> + return Err(EKEYREJECTED);
> + }
> +
> + Ok(())
> + }
> +
> + /// Verify that `child`'s signature is signed by this cert's public/private key.
> + pub fn verify_signed(&self, child: &Self) -> Result {
> + // SAFETY: both certificate pointers were produced by `x509_cert_parse()`
> + // and remain valid until dropped.
> + let (prev, child) = unsafe { (&*self.cert.as_ptr(), &*child.cert.as_ptr()) };
> +
> + let prev_pub = NonNull::new(prev.pub_).ok_or(EINVAL)?;
> + let child_sig = NonNull::new(child.sig).ok_or(EINVAL)?;
> +
> + // SAFETY: `prev_pub` and `child_sig` are valid for at least the duration
> + // of this call.
> + unsafe {
> + to_result(bindings::public_key_verify_signature(
> + prev_pub.as_ptr(),
> + child_sig.as_ptr(),
> + ))
> + }
> + }
> +
> + /// Transfers the public key out of the certificate so the certificate can be
> + /// dropped without freeing the public key.
> + pub fn take_public_key(&mut self) -> Option<PublicKey> {
> + // SAFETY: `self.cert` is a valid certificate pointer.
> + let raw = unsafe { (*self.cert.as_ptr()).pub_ };
> + let key = NonNull::new(raw)?;
> +
> + // SAFETY: `self.cert` is a valid certificate pointer. Clearing `pub_`
> + // transfers ownership of the public key to the returned `PublicKey`.
> + // The `pub_` field will not be freed by `x509_free_certificate()` in
> + // Drop
> + unsafe {
> + (*self.cert.as_ptr()).pub_ = ptr::null_mut();
> + }
> +
> + Some(PublicKey::from_raw(key))
> + }
> +}
[Severity: Low]
This isn't a bug, but there are a few minor style and coding guideline items
in rust/kernel/x509.rs:
1. At the top of rust/kernel/x509.rs, core::ptr::{self, NonNull} should use
vertical import formatting, and core::ffi::c_void is already provided by
kernel::prelude::*.
2. In Certificate::parse(), the error message has a typo ("larger then"
instead of "larger than").
3. Small abstraction methods in the kernel crate such as Certificate::raw(),
Certificate::check_accepted(), Certificate::verify_signed(), and
Certificate::take_public_key() should be annotated with #[inline].
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027052536.1722081-1-alistair.francis@wdc.com?part=21
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH v5 22/23] lib: rspdm: Support SPDM certificate validation
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (20 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 21/23] rust: x509: Add X509 certificate bindings alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:25 ` [PATCH v5 23/23] lib: rspdm: Support SPDM challenge alistair23
22 siblings, 0 replies; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Alistair Francis <alistair.francis@wdc.com>
Support validating the SPDM certificate chain. This only performs basic
sanity checks on the chain before we continue on. This does not ensure
that the root CA is trusted, we leave that for userspace to check and
enforce. Instead we just make sure that the chain is correct, uses
supported signatures and that it isn't blacklisted in the kernel.
We then store the first leaf certificate for use later.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
lib/rspdm/lib.rs | 12 ++++++++++
lib/rspdm/state.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 66 insertions(+), 2 deletions(-)
diff --git a/lib/rspdm/lib.rs b/lib/rspdm/lib.rs
index 6b0be0a0303fa..5277fc8c56fb8 100644
--- a/lib/rspdm/lib.rs
+++ b/lib/rspdm/lib.rs
@@ -289,6 +289,18 @@ pub extern "C" fn spdm_authenticate(state_ptr: *mut spdm_state) -> c_int {
provisioned_slots &= !(1 << slot);
}
+ 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.validate_cert_chain(slot) {
+ pr_err!("Certificate in slot {slot} failed to verify: {e:?}\n");
+ 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 7799d4ac70c3f..aa36d4ce7b41a 100644
--- a/lib/rspdm/state.rs
+++ b/lib/rspdm/state.rs
@@ -14,10 +14,15 @@
Bound,
Device, //
},
- error::Error,
+ error::{
+ to_result,
+ Error, //
+ },
hash::Shash,
+ public_key::PublicKey,
str::CStr,
validate::Untrusted,
+ x509::Certificate,
};
use crate::consts::{
@@ -97,7 +102,6 @@ fn exchange(
///
/// `T` is data and an exchange function owned by the caller,
/// so the borrow checker enforces that the transport data outlives `SpdmState`.
-#[expect(dead_code)]
pub(crate) struct SpdmState<'a, T: Transport> {
/// Responder device. Used for error reporting and passed to `transport`.
pub(crate) dev: &'a Device<Bound>,
@@ -148,6 +152,9 @@ pub(crate) struct SpdmState<'a, T: Transport> {
/// Certificate chain in each of the 8 slots. Empty `KVec` if a slot is
/// not populated. Prefixed by the 4 + H header per SPDM 1.0.0 table 15.
pub(crate) certs: [KVec<u8>; SPDM_SLOTS],
+ /// Public key portion of leaf certificate against which to check
+ /// responder's signatures.
+ pub(crate) leaf_key: Option<PublicKey>,
}
impl<'a, T: Transport> SpdmState<'a, T> {
@@ -173,6 +180,7 @@ pub(crate) fn new(
sig_len: 0,
crypto: None,
certs: [const { KVec::new() }; SPDM_SLOTS],
+ leaf_key: None,
}
}
@@ -240,6 +248,9 @@ pub(crate) fn get_version(&mut self) -> Result<(), Error> {
request.header.version = SPDM_MIN_VER;
self.version = SPDM_MIN_VER;
+ // Clear the leaf cert as we are re-authenticating
+ self.leaf_key = None;
+
let mut request_buf = request.to_bytes()?;
let response_vec: KVec<u8> = KVec::from_elem(0u8, SPDM_GET_VERSION_LEN, GFP_KERNEL)?;
@@ -551,4 +562,45 @@ pub(crate) fn get_certificate(&mut self, slot: u8) -> Result<(), Error> {
Ok(())
}
+
+ pub(crate) fn validate_cert_chain(&mut self, slot: u8) -> Result<(), Error> {
+ let cert_chain_buf = &self.certs[slot as usize];
+ let cert_chain_len = cert_chain_buf.len();
+ // We skip over the RootHash
+ let mut offset = SPDM_CERT_CHAIN_HDR_SZ + self.hash_len();
+
+ if offset >= cert_chain_len {
+ return Err(EPROTO);
+ }
+
+ let mut prev_cert: Option<Certificate> = None;
+
+ while offset < cert_chain_len {
+ let (cert, cert_len) = Certificate::parse(&cert_chain_buf[offset..])?;
+ cert.check_accepted()?;
+
+ if let Some(prev) = &prev_cert {
+ prev.verify_signed(&cert)?;
+ }
+
+ let _ = prev_cert.replace(cert);
+ offset += cert_len;
+ }
+
+ if let Some(mut cert) = prev_cert.take() {
+ if let Some(validate) = self.validate {
+ // SAFETY: Call the `validate` function provided.
+ let rc = unsafe { validate(self.dev.as_raw(), slot, cert.raw()) };
+ to_result(rc)?;
+ }
+
+ // The leaf key is the same for all slots (MULT_KEY_CAP == 0),
+ // so just store the first one.
+ if self.leaf_key.is_none() {
+ self.leaf_key = cert.take_public_key();
+ }
+ }
+
+ Ok(())
+ }
}
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* [PATCH v5 23/23] lib: rspdm: Support SPDM challenge
2026-10-27 5:25 [PATCH v5 00/23] lib: Rust implementation of SPDM alistair23
` (21 preceding siblings ...)
2026-10-27 5:25 ` [PATCH v5 22/23] lib: rspdm: Support SPDM certificate validation alistair23
@ 2026-10-27 5:25 ` alistair23
2026-10-27 5:43 ` sashiko-bot
22 siblings, 1 reply; 37+ messages in thread
From: alistair23 @ 2026-10-27 5:25 UTC (permalink / raw)
To: akpm, bhelgaas, lukas, linux-pci, linux-kernel, linux-cxl,
alistair, rust-for-linux, jic23
Cc: ojeda, aliceryhl, a.hindborg, alistair23, wilfred.mallawa,
bjorn3_gh, gregkh, boqun.feng, benno.lossin, gary, tmgross,
alex.gaynor, Alistair Francis
From: Alistair Francis <alistair.francis@wdc.com>
Support the CHALLENGE SPDM command.
Signed-off-by: Alistair Francis <alistair.francis@wdc.com>
---
lib/rspdm/consts.rs | 6 ++
lib/rspdm/lib.rs | 10 ++-
lib/rspdm/state.rs | 188 +++++++++++++++++++++++++++++++++++++++--
lib/rspdm/validator.rs | 156 ++++++++++++++++++++++++++++++++--
4 files changed, 347 insertions(+), 13 deletions(-)
diff --git a/lib/rspdm/consts.rs b/lib/rspdm/consts.rs
index 964ae2a21c6da..f2cb04d82d529 100644
--- a/lib/rspdm/consts.rs
+++ b/lib/rspdm/consts.rs
@@ -77,6 +77,8 @@
pub(crate) const SPDM_GET_CERTIFICATE: u8 = 0x82;
+pub(crate) const SPDM_CHALLENGE: u8 = 0x83;
+
// If the crypto support isn't enabled don't offer the algorithms
// to the responder
#[cfg(CONFIG_CRYPTO_RSA)]
@@ -105,3 +107,7 @@
pub(crate) const SPDM_HASH_ALGOS: u32 = SPDM_HASH_SHA2_256 | SPDM_HASH_SHA2_384_512;
pub(crate) const SPDM_OPAQUE_DATA_FMT_GENERAL: u8 = bit_u8(1);
+
+pub(crate) const SPDM_PREFIX_SZ: usize = 64;
+pub(crate) const SPDM_COMBINED_PREFIX_SZ: usize = 100;
+pub(crate) const SPDM_MAX_OPAQUE_DATA: usize = 1024;
diff --git a/lib/rspdm/lib.rs b/lib/rspdm/lib.rs
index 5277fc8c56fb8..3d03f9dee8f33 100644
--- a/lib/rspdm/lib.rs
+++ b/lib/rspdm/lib.rs
@@ -11,8 +11,7 @@
//! from other subsytems.
use crate::bindings::{
- spdm_state,
- EPROTONOSUPPORT, //
+ spdm_state, //
};
use core::ffi::{
c_int,
@@ -301,7 +300,12 @@ pub extern "C" fn spdm_authenticate(state_ptr: *mut spdm_state) -> c_int {
provisioned_slots &= !(1 << slot);
}
- -(EPROTONOSUPPORT as i32)
+ let provisioned_slots = state.provisioned_slots.trailing_zeros();
+ if let Err(e) = state.challenge(provisioned_slots as u8) {
+ return e.to_errno() as c_int;
+ }
+
+ 0
}
/// Destroys an SPDM session.
diff --git a/lib/rspdm/state.rs b/lib/rspdm/state.rs
index aa36d4ce7b41a..993d19cd60935 100644
--- a/lib/rspdm/state.rs
+++ b/lib/rspdm/state.rs
@@ -21,6 +21,7 @@
hash::Shash,
public_key::PublicKey,
str::CStr,
+ str::CString,
validate::Untrusted,
x509::Certificate,
};
@@ -33,6 +34,7 @@
SPDM_ASYM_RSASSA_2048,
SPDM_ASYM_RSASSA_3072,
SPDM_ASYM_RSASSA_4096,
+ SPDM_COMBINED_PREFIX_SZ,
SPDM_ERROR,
SPDM_GET_VERSION_LEN,
SPDM_HASH_ALGOS,
@@ -40,10 +42,12 @@
SPDM_HASH_SHA_384,
SPDM_HASH_SHA_512,
SPDM_KEY_EX_CAP,
+ SPDM_MAX_OPAQUE_DATA,
SPDM_MAX_VER,
SPDM_MEAS_RESP_CAP,
SPDM_MIN_VER,
SPDM_OPAQUE_DATA_FMT_GENERAL,
+ SPDM_PREFIX_SZ,
SPDM_REQ,
SPDM_RSP_MIN_CAPS,
SPDM_SLOTS,
@@ -53,6 +57,8 @@
SPDM_VER_13, //
};
use crate::validator::{
+ ChallengeReq,
+ ChallengeRsp,
GetCapabilitiesReq,
GetCapabilitiesRsp,
GetCertificateReq,
@@ -72,6 +78,8 @@
SPDM_CERT_CHAIN_HDR_SZ, //
};
+const SPDM_CONTEXT: &str = "responder-challenge_auth signing";
+
/// Transport for exchanging SPDM messages with a responder device.
///
/// This trait abstracts the bus-specific message transport (for example
@@ -144,7 +152,7 @@ pub(crate) struct SpdmState<'a, T: Transport> {
base_asym_enc: &'static CStr,
/// Signature length of `base_asym_alg` (in bytes). S or SigLen in SPDM
/// specification.
- sig_len: usize,
+ pub(crate) sig_len: usize,
/// Hash transformation for `base_hash_alg` computation.
crypto: Option<Shash>,
@@ -155,6 +163,16 @@ pub(crate) struct SpdmState<'a, T: Transport> {
/// Public key portion of leaf certificate against which to check
/// responder's signatures.
pub(crate) leaf_key: Option<PublicKey>,
+
+ /// Concatenation of all SPDM messages exchanged during an authentication
+ /// or measurement sequence. Used to verify the signature, as it is
+ /// computed over the hashed transcript.
+ transcript: VVec<u8>,
+
+ /// Requester nonce to be used for the next authentication sequence.
+ /// Populated from user space through sysfs. If user space does not
+ /// provide a nonce, the kernel uses a random one.
+ pub(crate) next_nonce: KVec<u8>,
}
impl<'a, T: Transport> SpdmState<'a, T> {
@@ -181,6 +199,8 @@ pub(crate) fn new(
crypto: None,
certs: [const { KVec::new() }; SPDM_SLOTS],
leaf_key: None,
+ transcript: VVec::new(),
+ next_nonce: KVec::new(),
}
}
@@ -197,12 +217,14 @@ pub(crate) fn hash_len(&self) -> usize {
/// The data in `request_buf` is sent to the device and the response
/// is returned as untrusted data to be validated by the caller.
pub(crate) fn spdm_exchange(
- &self,
+ &mut self,
request_buf: &[u8],
mut response_buf: KVec<u8>,
) -> Result<Untrusted<KVec<u8>>, Error> {
let header_size = core::mem::size_of::<SpdmHeader>();
- let request: SpdmHeader = Untrusted::new(request_buf).validate(self)?;
+ let request: SpdmHeader = Untrusted::new(request_buf).validate(&*self)?;
+
+ self.transcript.extend_from_slice(request_buf, GFP_KERNEL)?;
let length = self
.transport
@@ -220,11 +242,11 @@ pub(crate) fn spdm_exchange(
response_buf.truncate(length);
let response: SpdmHeader =
- Untrusted::new(response_buf.get(..header_size).ok_or(EPROTO)?).validate(self)?;
+ Untrusted::new(response_buf.get(..header_size).ok_or(EPROTO)?).validate(&*self)?;
if response.code == SPDM_ERROR {
let error_rsp: SpdmErrorRsp =
- Untrusted::new(response_buf.get(..header_size).ok_or(EPROTO)?).validate(self)?;
+ Untrusted::new(response_buf.get(..header_size).ok_or(EPROTO)?).validate(&*self)?;
pr_err!("Error response: {error_rsp:?}\n");
return Err(error_rsp.error_code.into());
}
@@ -248,6 +270,8 @@ pub(crate) fn get_version(&mut self) -> Result<(), Error> {
request.header.version = SPDM_MIN_VER;
self.version = SPDM_MIN_VER;
+ self.transcript.clear();
+
// Clear the leaf cert as we are re-authenticating
self.leaf_key = None;
@@ -259,6 +283,9 @@ pub(crate) fn get_version(&mut self) -> Result<(), Error> {
.spdm_exchange(request_buf.as_mut_slice(), response_vec)?
.validate(&*self)?;
+ self.transcript
+ .extend_from_slice(response.as_bytes()?, GFP_KERNEL)?;
+
let mut foundver = false;
for &entry in response.version_number_entries.iter() {
let alpha_version = (entry & 0xF) as u8;
@@ -309,7 +336,11 @@ pub(crate) fn get_capabilities(&mut self) -> Result<(), Error> {
.spdm_exchange(request_buf.as_mut_slice(), response_vec)?
.validate(&*self)?;
+ self.transcript
+ .extend_from_slice(response.as_bytes()?, GFP_KERNEL)?;
+
self.rsp_caps = response.flags;
+
if (self.rsp_caps & SPDM_RSP_MIN_CAPS) != SPDM_RSP_MIN_CAPS {
pr_err!(
"{:#x} capabilities are supported, which don't meet required {:#x}\n",
@@ -404,6 +435,9 @@ pub(crate) fn negotiate_algs(&mut self) -> Result<(), Error> {
.spdm_exchange(request_buf.as_mut_slice(), response_vec)?
.validate(&*self)?;
+ self.transcript
+ .extend_from_slice(response.as_bytes()?, GFP_KERNEL)?;
+
self.base_asym_alg = response.base_asym_sel;
self.base_hash_alg = response.base_hash_sel;
self.meas_hash_alg = response.measurement_hash_algo;
@@ -450,6 +484,9 @@ pub(crate) fn get_digests(&mut self) -> Result<(), Error> {
.spdm_exchange(request_buf.as_mut_slice(), response_vec)?
.validate(&*self)?;
+ self.transcript
+ .extend_from_slice(response.as_bytes()?, GFP_KERNEL)?;
+
let mut deprovisioned_slots = self.provisioned_slots & !response.header.param2;
while (deprovisioned_slots.trailing_zeros() as usize) < SPDM_SLOTS {
let slot = deprovisioned_slots.trailing_zeros() as usize;
@@ -488,6 +525,9 @@ pub(crate) fn get_certificate(&mut self, slot: u8) -> Result<(), Error> {
.spdm_exchange(request_buf.as_mut_slice(), response_vec)?
.validate(&*self)?;
+ self.transcript
+ .extend_from_slice(response.as_bytes()?, GFP_KERNEL)?;
+
if (response.header.param1 & 0xF) != slot {
pr_err!("Invalid slot response\n");
return Err(EPROTO);
@@ -521,6 +561,9 @@ pub(crate) fn get_certificate(&mut self, slot: u8) -> Result<(), Error> {
.spdm_exchange(request_buf.as_mut_slice(), response_vec)?
.validate(&*self)?;
+ self.transcript
+ .extend_from_slice(response.as_bytes()?, GFP_KERNEL)?;
+
let portion_length = response.portion_length;
let rem_length = response.remainder_length;
@@ -603,4 +646,139 @@ pub(crate) fn validate_cert_chain(&mut self, slot: u8) -> Result<(), Error> {
Ok(())
}
+
+ pub(crate) fn challenge_rsp_len(&mut self, nonce_len: usize, opaque_len: usize) -> usize {
+ // No measurement summary hash requested (MSHLength == 0)
+ let mut length =
+ core::mem::size_of::<SpdmHeader>() + self.hash_len() + nonce_len + opaque_len + 2;
+
+ if self.version >= SPDM_VER_13 {
+ length += 8;
+ }
+
+ length + self.sig_len
+ }
+
+ fn verify_signature(&mut self, signature: &mut [u8]) -> Result<(), Error> {
+ let crypto = self.crypto.as_ref().ok_or(EPROTO)?;
+ let leaf_key = self.leaf_key.as_ref().ok_or(EPROTO)?;
+ let hash_len = crypto.hash_len();
+ let sig_len = self.sig_len;
+
+ if self.transcript.len() <= sig_len {
+ return Err(EPROTO);
+ }
+
+ let mut m: KVec<u8> = KVec::new();
+ m.extend_with(SPDM_COMBINED_PREFIX_SZ + hash_len, 0, GFP_KERNEL)?;
+
+ // Generate the `unverified_message_hash`, which is the hash of the
+ // `unverified_data` (Chapter 16 - "Signature Verification").
+ crypto.digest(
+ &self.transcript[..(self.transcript.len() - sig_len)],
+ &mut m[SPDM_COMBINED_PREFIX_SZ..],
+ )?;
+
+ // Must be in scope whenever `sig` is used
+ let mut mhash: KVec<u8> = KVec::new();
+
+ let message = if self.version <= SPDM_VER_11 {
+ &m[SPDM_COMBINED_PREFIX_SZ..]
+ } else {
+ // `M` shall be the concatenation of the `combined_spdm_prefix`
+ // and `unverified_message_hash`. We use `M` for pub/priv
+ // signature verification below.
+ // (Chapter 16 - "Signature Verification")
+
+ let major = self.version >> 4;
+ let minor = self.version & 0xF;
+
+ // Generate the `spdm_prefix` (Chapter 15 - "Signature generation")
+ let prefix = CString::try_from_fmt(fmt!("dmtf-spdm-v{major:x}.{minor:x}.*dmtf-spdm-v{major:x}.{minor:x}.*dmtf-spdm-v{major:x}.{minor:x}.*dmtf-spdm-v{major:x}.{minor:x}.*"))?;
+
+ if prefix.to_bytes().len() != SPDM_PREFIX_SZ {
+ pr_err!("spdm_prefix length is incorrect\n");
+ return Err(EPROTO);
+ }
+
+ // Build the `combined_spdm_prefix` (Chapter 15 - "Signature
+ // generation") directly in `m`: the 64 byte `spdm_prefix`, zero
+ // padding, followed by the 36 byte `spdm_context`. `m` was zero
+ // initialised above with the `unverified_message_hash` copied
+ // in at offset SPDM_COMBINED_PREFIX_SZ, so only the two
+ // non-zero portions need to be written.
+ m[..SPDM_PREFIX_SZ].copy_from_slice(prefix.to_bytes());
+ m[SPDM_COMBINED_PREFIX_SZ - SPDM_CONTEXT.len()..SPDM_COMBINED_PREFIX_SZ]
+ .copy_from_slice(SPDM_CONTEXT.as_bytes());
+
+ // Generate a hash of `M` and use that for signature
+ // verification (RSA or ECDSA)
+ mhash.extend_with(hash_len, 0, GFP_KERNEL)?;
+ crypto.digest(m.as_slice(), mhash.as_mut_slice())?;
+
+ mhash.as_slice()
+ };
+
+ // Finally verify the generated signature against the public key
+ leaf_key.verify_signature(signature, self.base_asym_enc, crypto.name(), message)
+ }
+
+ pub(crate) fn challenge(&mut self, slot: u8) -> Result<(), Error> {
+ let mut request = ChallengeReq::default();
+ request.header.version = self.version;
+ request.header.param1 = slot;
+
+ let nonce_len = request.nonce.len();
+
+ if !self.next_nonce.is_empty() {
+ let request_nonce_len = request.nonce.len();
+
+ if self.next_nonce.len() == request_nonce_len {
+ request
+ .nonce
+ .copy_from_slice(&self.next_nonce[..request_nonce_len]);
+ } else {
+ return Err(EINVAL);
+ }
+
+ self.next_nonce.clear();
+ } else {
+ // SAFETY: `request.nonce` is a valid buffer of `nonce_len` length.
+ unsafe {
+ bindings::get_random_bytes(
+ core::ptr::from_mut(&mut request.nonce).cast::<c_void>(),
+ nonce_len,
+ )
+ };
+ }
+
+ let rsp_sz = self.challenge_rsp_len(nonce_len, SPDM_MAX_OPAQUE_DATA);
+
+ let mut request_buf = request.to_bytes()?;
+
+ let response_vec: KVec<u8> = KVec::from_elem(0u8, rsp_sz, GFP_KERNEL)?;
+
+ let mut response: ChallengeRsp = self
+ .spdm_exchange(request_buf.as_mut_slice(), response_vec)?
+ .validate(&*self)?;
+
+ self.transcript
+ .extend_from_slice(response.as_bytes()?, GFP_KERNEL)?;
+
+ if request.context != response.context {
+ pr_err!("Mismatched challenge context response\n");
+ return Err(EPROTO);
+ }
+
+ match self.verify_signature(&mut response.signature) {
+ Ok(()) => {
+ pr_info!("Authenticated with certificate slot {slot}\n");
+ Ok(())
+ }
+ Err(e) => {
+ pr_err!("Cannot verify challenge_auth signature: {e:?}\n");
+ Err(EPROTO)
+ }
+ }
+ }
}
diff --git a/lib/rspdm/validator.rs b/lib/rspdm/validator.rs
index 469a5722368f2..4ec0db6ffb59c 100644
--- a/lib/rspdm/validator.rs
+++ b/lib/rspdm/validator.rs
@@ -10,12 +10,14 @@
use crate::consts::{
SPDM_ASYM_ALGOS,
SPDM_CAP_SUPPORTED_ALGORITHMS,
+ SPDM_CHALLENGE,
SPDM_CTEXPONENT,
SPDM_GET_CAPABILITIES,
SPDM_GET_CERTIFICATE,
SPDM_GET_DIGESTS,
SPDM_GET_VERSION,
SPDM_HASH_ALGOS,
+ SPDM_MAX_OPAQUE_DATA,
SPDM_MEAS_SPEC_DMTF,
SPDM_MIN_DATA_TRANSFER_SIZE,
SPDM_MIN_VER,
@@ -144,7 +146,6 @@ pub(crate) struct GetVersionRsp {
}
impl GetVersionRsp {
- #[expect(dead_code)]
pub(crate) fn as_bytes(&self) -> Result<&[u8], Error> {
self.raw_bytes.get(..self.length).ok_or(EPROTO)
}
@@ -401,7 +402,6 @@ pub(crate) struct GetCapabilitiesRsp {
}
impl GetCapabilitiesRsp {
- #[expect(dead_code)]
pub(crate) fn as_bytes(&self) -> Result<&[u8], Error> {
self.raw_bytes.get(..self.length).ok_or(EPROTO)
}
@@ -592,7 +592,6 @@ pub(crate) struct NegotiateAlgsRsp {
}
impl NegotiateAlgsRsp {
- #[expect(dead_code)]
pub(crate) fn as_bytes(&self) -> Result<&[u8], Error> {
self.raw_bytes.get(..self.length).ok_or(EPROTO)
}
@@ -762,7 +761,6 @@ pub(crate) struct GetDigestsRsp {
}
impl GetDigestsRsp {
- #[expect(dead_code)]
pub(crate) fn as_bytes(&self) -> Result<&[u8], Error> {
self.raw_bytes.get(..self.length).ok_or(EPROTO)
}
@@ -924,7 +922,6 @@ pub(crate) struct GetCertificateRsp {
}
impl GetCertificateRsp {
- #[expect(dead_code)]
pub(crate) fn as_bytes(&self) -> Result<&[u8], Error> {
self.raw_bytes.get(..self.length).ok_or(EPROTO)
}
@@ -981,3 +978,152 @@ fn validate(unvalidated: KVec<u8>, context: &'c SpdmState<'c, T>) -> Result<Self
})
}
}
+
+pub(crate) struct ChallengeReq {
+ pub(crate) header: SpdmHeader,
+
+ pub(crate) nonce: [u8; 32],
+ pub(crate) context: [u8; 8],
+}
+
+impl ChallengeReq {
+ pub(crate) fn to_bytes(&self) -> Result<KVec<u8>> {
+ let mut out = self.header.to_bytes()?;
+
+ out.extend_from_slice(&self.nonce, GFP_KERNEL)?;
+
+ if self.header.version > SPDM_VER_12 {
+ out.extend_from_slice(&self.context, GFP_KERNEL)?;
+ }
+
+ Ok(out)
+ }
+}
+
+impl Default for ChallengeReq {
+ fn default() -> Self {
+ ChallengeReq {
+ header: SpdmHeader::new(SPDM_CHALLENGE),
+
+ nonce: [0; 32],
+ context: [0; 8],
+ }
+ }
+}
+
+#[expect(dead_code)]
+pub(crate) struct ChallengeRsp {
+ pub(crate) header: SpdmHeader,
+
+ pub(crate) cert_chain_hash: KVec<u8>,
+ pub(crate) nonce: [u8; 32],
+
+ pub(crate) opaque_data_len: u16,
+ pub(crate) opaque_data: KVec<u8>,
+
+ /// `RequesterContext`, only present in SPDM 1.3 and later, zeroed otherwise.
+ pub(crate) context: [u8; 8],
+
+ pub(crate) signature: 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 ChallengeRsp {
+ 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 ChallengeRsp {
+ 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_CHALLENGE - 0x80 {
+ return Err(EPROTO);
+ }
+
+ if header.version != context.version {
+ pr_err!("Invalid version response\n");
+ return Err(EPROTO);
+ }
+
+ let hash_len = context.hash_len();
+
+ let nonce_offset = mem::size_of::<SpdmHeader>() + hash_len;
+ // MSHLength (H) is always zero as we don't support it
+ let opaque_len_offset = nonce_offset + 32;
+ let opaque_offset = opaque_len_offset + 2;
+
+ let mut cert_chain_hash = KVec::new();
+ cert_chain_hash.extend_from_slice(
+ unvalidated
+ .get(mem::size_of::<SpdmHeader>()..nonce_offset)
+ .ok_or(EPROTO)?,
+ GFP_KERNEL,
+ )?;
+
+ let mut nonce = [0u8; 32];
+ nonce.copy_from_slice(
+ unvalidated
+ .get(nonce_offset..nonce_offset + 32)
+ .ok_or(EPROTO)?,
+ );
+
+ let opaque_data_len = u16::from_le_bytes(
+ unvalidated
+ .get(opaque_len_offset..opaque_len_offset + 2)
+ .ok_or(EPROTO)?
+ .try_into()
+ .map_err(|_| EPROTO)?,
+ );
+
+ if opaque_data_len > SPDM_MAX_OPAQUE_DATA as u16 {
+ return Err(EPROTO);
+ }
+
+ let opaque_end = opaque_offset + opaque_data_len as usize;
+ let mut opaque_data = KVec::new();
+ opaque_data.extend_from_slice(
+ unvalidated.get(opaque_offset..opaque_end).ok_or(EPROTO)?,
+ GFP_KERNEL,
+ )?;
+
+ // `RequesterContext` (8 bytes) is only present in SPDM 1.3 and later.
+ let mut req_context = [0u8; 8];
+ let context_end;
+ if context.version >= SPDM_VER_13 {
+ context_end = opaque_end + 8;
+ req_context.copy_from_slice(unvalidated.get(opaque_end..context_end).ok_or(EPROTO)?);
+ } else {
+ context_end = opaque_end;
+ }
+
+ let signature_end = context_end + context.sig_len;
+ let mut signature = KVec::new();
+ signature.extend_from_slice(
+ unvalidated.get(context_end..signature_end).ok_or(EPROTO)?,
+ GFP_KERNEL,
+ )?;
+
+ Ok(ChallengeRsp {
+ header,
+ cert_chain_hash,
+ nonce,
+ opaque_data_len,
+ opaque_data,
+ context: req_context,
+ signature,
+ length: signature_end,
+ raw_bytes: unvalidated,
+ })
+ }
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 37+ messages in thread* Re: [PATCH v5 23/23] lib: rspdm: Support SPDM challenge
2026-10-27 5:25 ` [PATCH v5 23/23] lib: rspdm: Support SPDM challenge alistair23
@ 2026-10-27 5:43 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-10-27 5:43 UTC (permalink / raw)
To: alistair23, linux-pci, linux-cxl; +Cc: ojeda, gary
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] lib: rspdm: unsliced request_buf in spdm_exchange() fails validation
--
commit b875413583bbf6bfc09ae38ff772d19acd98e3d0
Author: Alistair Francis <alistair.francis@wdc.com>
lib: rspdm: Support SPDM challenge
This commit adds support for the SPDM CHALLENGE command, including message
transcript tracking, signature verification against the responder leaf
certificate, and challenge response validation.
> diff --git a/lib/rspdm/state.rs b/lib/rspdm/state.rs
> index aa36d4ce7b41a..993d19cd60935 100644
> --- a/lib/rspdm/state.rs
> +++ b/lib/rspdm/state.rs
[ ... ]
> @@ -197,12 +217,14 @@ pub(crate) fn hash_len(&self) -> usize {
> /// The data in `request_buf` is sent to the device and the response
> /// is returned as untrusted data to be validated by the caller.
> pub(crate) fn spdm_exchange(
> - &self,
> + &mut self,
> request_buf: &[u8],
> mut response_buf: KVec<u8>,
> ) -> Result<Untrusted<KVec<u8>>, Error> {
> let header_size = core::mem::size_of::<SpdmHeader>();
> - let request: SpdmHeader = Untrusted::new(request_buf).validate(self)?;
> + let request: SpdmHeader = Untrusted::new(request_buf).validate(&*self)?;
[Severity: High]
Does validating the full request_buf as an SpdmHeader cause SpdmState::challenge()
(and other requests with payloads beyond the 4-byte header) to fail with EPROTO?
In lib/rspdm/state.rs, spdm_authenticate() calls SpdmState::challenge(), which
serializes a 36-byte (or 44-byte for SPDM 1.3+) ChallengeReq via
ChallengeReq::to_bytes() and passes the buffer to SpdmState::spdm_exchange():
lib/rspdm/state.rs:SpdmState::challenge() {
...
let mut request_buf = request.to_bytes()?;
...
let mut response: ChallengeRsp = self
.spdm_exchange(request_buf.as_mut_slice(), response_vec)?
.validate(&*self)?;
}
In SpdmState::spdm_exchange(), request_buf is passed unsliced to
Untrusted::new(request_buf).validate(&*self), which invokes
SpdmHeader::validate() in lib/rspdm/validator.rs:
lib/rspdm/validator.rs:SpdmHeader::validate() {
SpdmHeader::read_from_bytes(unvalidated).or(Err(EPROTO))
}
Because zerocopy::FromBytes::read_from_bytes() requires the input slice length
to exactly equal size_of::<SpdmHeader>() (4 bytes) via Ref::sized_from() in
rust/zerocopy/src/ref.rs, any request buffer larger than 4 bytes is rejected
with EPROTO before the request can be added to the transcript or sent over the
transport.
Should request_buf be sliced to ..header_size before validating the request
header, matching how response_buf.get(..header_size) is validated later in
SpdmState::spdm_exchange()?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027052536.1722081-1-alistair.francis@wdc.com?part=23
^ permalink raw reply [flat|nested] 37+ messages in thread