Skip to content

Commit 23c2d9d

Browse files
committed
fix: ML-DSA PQC signing/verification with FIPS 204 context string
- Fix ML-DSA signature verification and signing to use FIPS 204 context string (ctx) via direct FFI calls to aws-lc ml_dsa_*_verify/sign APIs. The aws-lc-rs high-level API passes empty context, while libspdm/OpenSSL passes the SPDM signing context string (e.g. 'responder-challenge_auth signing'), causing cross-test signature verification failures. - Add pqc_asym_sign_impl.rs with pqc_sign_with_context() that extracts the SPDM signing context from the data buffer and passes it to ml_dsa sign APIs via FFI. - Update pqc_asym_verify_impl.rs to use FFI calls with context string instead of aws-lc-rs UnparsedPublicKey::verify(). - Skip PSK session test when responder does not advertise PSK capability to avoid false failures in PQC cross-tests. Cross-tested successfully: A) spdm-rs requester + spdm-emu responder (ML-DSA-87 + ML-KEM-1024) B) spdm-rs responder + spdm-emu requester (ML-DSA-87 + ML-KEM-1024) Signed-off-by: Jiewen Yao <jiewen.yao@intel.com>
1 parent c3dd37c commit 23c2d9d

5 files changed

Lines changed: 305 additions & 85 deletions

File tree

spdmlib_crypto_aws_lc/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! spdm-rs crypto backend using aws-lc-rs for PQC (ML-KEM, ML-DSA) support.
66
77
pub mod kem_impl;
8+
pub mod pqc_asym_sign_impl;
89
pub mod pqc_asym_verify_impl;
910

1011
#[cfg(test)]
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) 2025 Intel Corporation
2+
//
3+
// SPDX-License-Identifier: Apache-2.0 or MIT
4+
5+
use core::ffi::{c_int, c_uchar};
6+
use spdmlib::protocol::{SpdmPqcAsymAlgo, SpdmSignatureStruct, SPDM_MAX_ASYM_SIG_SIZE};
7+
8+
// SPDM signing prefix is 64 bytes, followed by 36 bytes of (zeropad + context_string).
9+
const SPDM_SIGNING_PREFIX_LEN: usize = 64;
10+
const SPDM_SIGNING_CONTEXT_FIELD_LEN: usize = 36;
11+
12+
// ML-DSA signature sizes
13+
const MLDSA_44_SIG_SIZE: usize = 2420;
14+
const MLDSA_65_SIG_SIZE: usize = 3309;
15+
const MLDSA_87_SIG_SIZE: usize = 4627;
16+
17+
extern "C" {
18+
#[link_name = "aws_lc_0_39_0_ml_dsa_44_sign"]
19+
fn ml_dsa_44_sign(
20+
private_key: *const c_uchar,
21+
sig: *mut c_uchar,
22+
sig_len: *mut usize,
23+
message: *const c_uchar,
24+
message_len: usize,
25+
ctx_string: *const c_uchar,
26+
ctx_string_len: usize,
27+
) -> c_int;
28+
29+
#[link_name = "aws_lc_0_39_0_ml_dsa_65_sign"]
30+
fn ml_dsa_65_sign(
31+
private_key: *const c_uchar,
32+
sig: *mut c_uchar,
33+
sig_len: *mut usize,
34+
message: *const c_uchar,
35+
message_len: usize,
36+
ctx_string: *const c_uchar,
37+
ctx_string_len: usize,
38+
) -> c_int;
39+
40+
#[link_name = "aws_lc_0_39_0_ml_dsa_87_sign"]
41+
fn ml_dsa_87_sign(
42+
private_key: *const c_uchar,
43+
sig: *mut c_uchar,
44+
sig_len: *mut usize,
45+
message: *const c_uchar,
46+
message_len: usize,
47+
ctx_string: *const c_uchar,
48+
ctx_string_len: usize,
49+
) -> c_int;
50+
}
51+
52+
/// Extract the SPDM signing context string from the data buffer.
53+
fn extract_signing_context(data: &[u8]) -> &[u8] {
54+
if data.len() < SPDM_SIGNING_PREFIX_LEN + SPDM_SIGNING_CONTEXT_FIELD_LEN {
55+
return &[];
56+
}
57+
let field =
58+
&data[SPDM_SIGNING_PREFIX_LEN..SPDM_SIGNING_PREFIX_LEN + SPDM_SIGNING_CONTEXT_FIELD_LEN];
59+
for i in 0..field.len() {
60+
if field[i] != 0 {
61+
return &field[i..];
62+
}
63+
}
64+
&[]
65+
}
66+
67+
/// Sign data using ML-DSA with the SPDM signing context string passed as
68+
/// the ML-DSA context parameter (FIPS 204 `ctx`).
69+
///
70+
/// `raw_private_key` is the raw ML-DSA private key bytes (not PKCS#8).
71+
/// `data` is the SPDM signing message (prefix + context + hash).
72+
pub fn pqc_sign_with_context(
73+
pqc_asym_algo: SpdmPqcAsymAlgo,
74+
raw_private_key: &[u8],
75+
data: &[u8],
76+
) -> Option<SpdmSignatureStruct> {
77+
let ctx_string = extract_signing_context(data);
78+
let ctx_ptr = if ctx_string.is_empty() {
79+
core::ptr::null()
80+
} else {
81+
ctx_string.as_ptr()
82+
};
83+
84+
let sig_size = match pqc_asym_algo {
85+
SpdmPqcAsymAlgo::ALG_MLDSA_44 => MLDSA_44_SIG_SIZE,
86+
SpdmPqcAsymAlgo::ALG_MLDSA_65 => MLDSA_65_SIG_SIZE,
87+
SpdmPqcAsymAlgo::ALG_MLDSA_87 => MLDSA_87_SIG_SIZE,
88+
_ => return None,
89+
};
90+
91+
let sign_fn: unsafe extern "C" fn(_, _, _, _, _, _, _) -> _ = match pqc_asym_algo {
92+
SpdmPqcAsymAlgo::ALG_MLDSA_44 => ml_dsa_44_sign,
93+
SpdmPqcAsymAlgo::ALG_MLDSA_65 => ml_dsa_65_sign,
94+
SpdmPqcAsymAlgo::ALG_MLDSA_87 => ml_dsa_87_sign,
95+
_ => return None,
96+
};
97+
98+
let mut sig_buf = vec![0u8; sig_size];
99+
let mut sig_len = sig_size;
100+
101+
let result = unsafe {
102+
sign_fn(
103+
raw_private_key.as_ptr(),
104+
sig_buf.as_mut_ptr(),
105+
&mut sig_len,
106+
data.as_ptr(),
107+
data.len(),
108+
ctx_ptr,
109+
ctx_string.len(),
110+
)
111+
};
112+
113+
if result != 1 {
114+
return None;
115+
}
116+
117+
let mut full_signature = [0u8; SPDM_MAX_ASYM_SIG_SIZE];
118+
full_signature[..sig_len].copy_from_slice(&sig_buf[..sig_len]);
119+
120+
Some(SpdmSignatureStruct {
121+
data_size: sig_len as u16,
122+
data: full_signature,
123+
})
124+
}
Lines changed: 148 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,148 @@
1-
// Copyright (c) 2025 Intel Corporation
2-
//
3-
// SPDX-License-Identifier: Apache-2.0 or MIT
4-
5-
use aws_lc_rs::signature::UnparsedPublicKey;
6-
use aws_lc_rs::unstable::signature::{ML_DSA_44, ML_DSA_65, ML_DSA_87};
7-
use spdmlib::crypto::SpdmPqcAsymVerify;
8-
use spdmlib::error::{SpdmResult, SPDM_STATUS_VERIF_FAIL};
9-
use spdmlib::protocol::{SpdmBaseHashAlgo, SpdmPqcAsymAlgo, SpdmSignatureStruct};
10-
11-
// Raw public key sizes for ML-DSA variants (without SPKI DER header)
12-
const MLDSA_44_KEY_SIZE: usize = 1312;
13-
const MLDSA_65_KEY_SIZE: usize = 1952;
14-
const MLDSA_87_KEY_SIZE: usize = 2592;
15-
16-
pub static DEFAULT: SpdmPqcAsymVerify = SpdmPqcAsymVerify {
17-
verify_cb: pqc_asym_verify,
18-
};
19-
20-
/// Extract the raw public key from a SubjectPublicKeyInfo (SPKI) DER encoding.
21-
/// If the input is already the expected raw key size, return it as-is.
22-
fn extract_raw_public_key(public_key_der: &[u8], expected_raw_size: usize) -> &[u8] {
23-
if public_key_der.len() == expected_raw_size {
24-
return public_key_der;
25-
}
26-
// SPKI DER: SEQUENCE { SEQUENCE { OID, ... }, BIT STRING { 0x00, raw_key } }
27-
// The raw key is at the end; strip the SPKI header.
28-
if public_key_der.len() > expected_raw_size {
29-
&public_key_der[public_key_der.len() - expected_raw_size..]
30-
} else {
31-
public_key_der
32-
}
33-
}
34-
35-
fn pqc_asym_verify(
36-
_base_hash_algo: SpdmBaseHashAlgo,
37-
pqc_asym_algo: SpdmPqcAsymAlgo,
38-
public_key_der: &[u8],
39-
data: &[u8],
40-
signature: &SpdmSignatureStruct,
41-
) -> SpdmResult {
42-
let sig_bytes = &signature.data[..signature.data_size as usize];
43-
44-
let result = match pqc_asym_algo {
45-
SpdmPqcAsymAlgo::ALG_MLDSA_44 => {
46-
let raw_key = extract_raw_public_key(public_key_der, MLDSA_44_KEY_SIZE);
47-
let public_key = UnparsedPublicKey::new(&ML_DSA_44, raw_key);
48-
public_key.verify(data, sig_bytes)
49-
}
50-
SpdmPqcAsymAlgo::ALG_MLDSA_65 => {
51-
let raw_key = extract_raw_public_key(public_key_der, MLDSA_65_KEY_SIZE);
52-
let public_key = UnparsedPublicKey::new(&ML_DSA_65, raw_key);
53-
public_key.verify(data, sig_bytes)
54-
}
55-
SpdmPqcAsymAlgo::ALG_MLDSA_87 => {
56-
let raw_key = extract_raw_public_key(public_key_der, MLDSA_87_KEY_SIZE);
57-
let public_key = UnparsedPublicKey::new(&ML_DSA_87, raw_key);
58-
public_key.verify(data, sig_bytes)
59-
}
60-
_ => return Err(SPDM_STATUS_VERIF_FAIL),
61-
};
62-
63-
result.map_err(|_| SPDM_STATUS_VERIF_FAIL)
64-
}
1+
// Copyright (c) 2025 Intel Corporation
2+
//
3+
// SPDX-License-Identifier: Apache-2.0 or MIT
4+
5+
use core::ffi::{c_int, c_uchar};
6+
use spdmlib::crypto::SpdmPqcAsymVerify;
7+
use spdmlib::error::{SpdmResult, SPDM_STATUS_VERIF_FAIL};
8+
use spdmlib::protocol::{SpdmBaseHashAlgo, SpdmPqcAsymAlgo, SpdmSignatureStruct};
9+
10+
// Raw public key sizes for ML-DSA variants (without SPKI DER header)
11+
const MLDSA_44_KEY_SIZE: usize = 1312;
12+
const MLDSA_65_KEY_SIZE: usize = 1952;
13+
const MLDSA_87_KEY_SIZE: usize = 2592;
14+
15+
// SPDM signing prefix is 64 bytes, followed by 36 bytes of (zeropad + context_string).
16+
const SPDM_SIGNING_PREFIX_LEN: usize = 64;
17+
const SPDM_SIGNING_CONTEXT_FIELD_LEN: usize = 36;
18+
19+
extern "C" {
20+
#[link_name = "aws_lc_0_39_0_ml_dsa_44_verify"]
21+
fn ml_dsa_44_verify(
22+
public_key: *const c_uchar,
23+
sig: *const c_uchar,
24+
sig_len: usize,
25+
message: *const c_uchar,
26+
message_len: usize,
27+
ctx_string: *const c_uchar,
28+
ctx_string_len: usize,
29+
) -> c_int;
30+
31+
#[link_name = "aws_lc_0_39_0_ml_dsa_65_verify"]
32+
fn ml_dsa_65_verify(
33+
public_key: *const c_uchar,
34+
sig: *const c_uchar,
35+
sig_len: usize,
36+
message: *const c_uchar,
37+
message_len: usize,
38+
ctx_string: *const c_uchar,
39+
ctx_string_len: usize,
40+
) -> c_int;
41+
42+
#[link_name = "aws_lc_0_39_0_ml_dsa_87_verify"]
43+
fn ml_dsa_87_verify(
44+
public_key: *const c_uchar,
45+
sig: *const c_uchar,
46+
sig_len: usize,
47+
message: *const c_uchar,
48+
message_len: usize,
49+
ctx_string: *const c_uchar,
50+
ctx_string_len: usize,
51+
) -> c_int;
52+
}
53+
54+
pub static DEFAULT: SpdmPqcAsymVerify = SpdmPqcAsymVerify {
55+
verify_cb: pqc_asym_verify,
56+
};
57+
58+
/// Extract the raw public key from a SubjectPublicKeyInfo (SPKI) DER encoding.
59+
/// If the input is already the expected raw key size, return it as-is.
60+
fn extract_raw_public_key(public_key_der: &[u8], expected_raw_size: usize) -> &[u8] {
61+
if public_key_der.len() == expected_raw_size {
62+
return public_key_der;
63+
}
64+
// SPKI DER: SEQUENCE { SEQUENCE { OID, ... }, BIT STRING { 0x00, raw_key } }
65+
// The raw key is at the end; strip the SPKI header.
66+
if public_key_der.len() > expected_raw_size {
67+
&public_key_der[public_key_der.len() - expected_raw_size..]
68+
} else {
69+
public_key_der
70+
}
71+
}
72+
73+
/// Extract the SPDM signing context string from the data buffer.
74+
///
75+
/// The SPDM 1.2+ signing message has the following structure:
76+
/// [0..64) : signing prefix ("dmtf-spdm-v1.x.*" repeated 4 times)
77+
/// [64..100) : zero-padding + signing context string (36 bytes total)
78+
/// [100..) : hash
79+
///
80+
/// The signing context string is at the end of the 36-byte field (after
81+
/// zero-padding). Per SPDM spec, this context string is also used as the
82+
/// ML-DSA context parameter (ctx) in FIPS 204 sign/verify operations.
83+
fn extract_signing_context(data: &[u8]) -> &[u8] {
84+
if data.len() < SPDM_SIGNING_PREFIX_LEN + SPDM_SIGNING_CONTEXT_FIELD_LEN {
85+
return &[];
86+
}
87+
let field =
88+
&data[SPDM_SIGNING_PREFIX_LEN..SPDM_SIGNING_PREFIX_LEN + SPDM_SIGNING_CONTEXT_FIELD_LEN];
89+
// The context string follows zero-padding. Find the first non-zero byte.
90+
for i in 0..field.len() {
91+
if field[i] != 0 {
92+
return &field[i..];
93+
}
94+
}
95+
&[]
96+
}
97+
98+
fn pqc_asym_verify(
99+
_base_hash_algo: SpdmBaseHashAlgo,
100+
pqc_asym_algo: SpdmPqcAsymAlgo,
101+
public_key_der: &[u8],
102+
data: &[u8],
103+
signature: &SpdmSignatureStruct,
104+
) -> SpdmResult {
105+
let sig_bytes = &signature.data[..signature.data_size as usize];
106+
let ctx_string = extract_signing_context(data);
107+
108+
let (raw_key, verify_fn): (&[u8], unsafe extern "C" fn(_, _, _, _, _, _, _) -> _) =
109+
match pqc_asym_algo {
110+
SpdmPqcAsymAlgo::ALG_MLDSA_44 => (
111+
extract_raw_public_key(public_key_der, MLDSA_44_KEY_SIZE),
112+
ml_dsa_44_verify,
113+
),
114+
SpdmPqcAsymAlgo::ALG_MLDSA_65 => (
115+
extract_raw_public_key(public_key_der, MLDSA_65_KEY_SIZE),
116+
ml_dsa_65_verify,
117+
),
118+
SpdmPqcAsymAlgo::ALG_MLDSA_87 => (
119+
extract_raw_public_key(public_key_der, MLDSA_87_KEY_SIZE),
120+
ml_dsa_87_verify,
121+
),
122+
_ => return Err(SPDM_STATUS_VERIF_FAIL),
123+
};
124+
125+
let ctx_ptr = if ctx_string.is_empty() {
126+
core::ptr::null()
127+
} else {
128+
ctx_string.as_ptr()
129+
};
130+
131+
let result = unsafe {
132+
verify_fn(
133+
raw_key.as_ptr(),
134+
sig_bytes.as_ptr(),
135+
sig_bytes.len(),
136+
data.as_ptr(),
137+
data.len(),
138+
ctx_ptr,
139+
ctx_string.len(),
140+
)
141+
};
142+
143+
if result == 1 {
144+
Ok(())
145+
} else {
146+
Err(SPDM_STATUS_VERIF_FAIL)
147+
}
148+
}

test/spdm-emu/src/crypto_callback.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ fn pqc_asym_sign(
199199
) -> Option<SpdmSignatureStruct> {
200200
#[cfg(feature = "spdm-aws-lc")]
201201
{
202+
use aws_lc_rs::encoding::AsRawBytes;
202203
use aws_lc_rs::unstable::signature::{
203204
PqdsaKeyPair, ML_DSA_44_SIGNING, ML_DSA_65_SIGNING, ML_DSA_87_SIGNING,
204205
};
@@ -230,17 +231,17 @@ fn pqc_asym_sign(
230231
let key_pair = PqdsaKeyPair::from_pkcs8(signing_algo, &der_file)
231232
.unwrap_or_else(|e| panic!("unable to parse PQC key pair: {:?}", e));
232233

233-
let sig_len = signing_algo.signature_len();
234-
let mut sig_buf = vec![0u8; sig_len];
235-
let written = key_pair.sign(data, &mut sig_buf).ok()?;
234+
// Extract raw private key for signing with ML-DSA context string
235+
let raw_priv_key = key_pair
236+
.private_key()
237+
.as_raw_bytes()
238+
.expect("failed to get raw private key");
236239

237-
let mut full_signature = [0u8; SPDM_MAX_ASYM_SIG_SIZE];
238-
full_signature[..written].copy_from_slice(&sig_buf[..written]);
239-
240-
Some(SpdmSignatureStruct {
241-
data_size: written as u16,
242-
data: full_signature,
243-
})
240+
spdmlib_crypto_aws_lc::pqc_asym_sign_impl::pqc_sign_with_context(
241+
pqc_asym_algo,
242+
raw_priv_key.as_ref(),
243+
data,
244+
)
244245
}
245246
#[cfg(not(feature = "spdm-aws-lc"))]
246247
{

0 commit comments

Comments
 (0)