Skip to content

Commit 4f9abad

Browse files
authored
IGVMfilegen: Sign hash of ID block with temporary key (#3832)
1 parent 332a2a1 commit 4f9abad

11 files changed

Lines changed: 647 additions & 11 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3738,7 +3738,9 @@ name = "igvmfilegen"
37383738
version = "0.0.0"
37393739
dependencies = [
37403740
"anyhow",
3741+
"base64 0.22.1",
37413742
"clap",
3743+
"crypto",
37423744
"fs-err",
37433745
"hex",
37443746
"hvdef",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! ECDSA stub for macOS. ECDSA is not yet implemented for the macOS native
5+
//! backend; all operations return an error.
6+
7+
use super::EcdsaCurve;
8+
use super::EcdsaError;
9+
10+
pub enum EcdsaKeyPairInner {}
11+
12+
impl EcdsaKeyPairInner {
13+
pub fn generate(_curve: EcdsaCurve) -> Result<Self, EcdsaError> {
14+
Err(EcdsaError(crate::BackendError::Null(
15+
"ECDSA not implemented for macOS native backend",
16+
)))
17+
}
18+
19+
pub fn sign_prehash(&self, _hash: &[u8]) -> Result<Vec<u8>, EcdsaError> {
20+
match *self {}
21+
}
22+
23+
pub fn public_key_bytes(&self) -> Result<Vec<u8>, EcdsaError> {
24+
match *self {}
25+
}
26+
}

support/crypto/src/ecdsa/mod.rs

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! ECDSA cryptographic operations (key generation, signing, public key export).
5+
6+
// The ecdsa module is available on backends that support it: OpenSSL (Linux glibc),
7+
// SymCrypt (Linux musl), and BCrypt (Windows). On macOS, ECDSA is not yet implemented.
8+
#![cfg(any(
9+
openssl,
10+
symcrypt,
11+
all(native, windows),
12+
all(native, target_os = "macos")
13+
))]
14+
15+
#[cfg(openssl)]
16+
mod ossl;
17+
#[cfg(openssl)]
18+
use ossl as sys;
19+
20+
#[cfg(all(native, windows))]
21+
mod win;
22+
#[cfg(all(native, windows))]
23+
use win as sys;
24+
25+
#[cfg(symcrypt)]
26+
mod symcrypt_stub;
27+
#[cfg(symcrypt)]
28+
use symcrypt_stub as sys;
29+
30+
// macOS stub: provides the types so the module compiles under clippy,
31+
// but all operations return an error at runtime.
32+
#[cfg(all(native, target_os = "macos"))]
33+
mod mac_stub;
34+
#[cfg(all(native, target_os = "macos"))]
35+
use mac_stub as sys;
36+
37+
use thiserror::Error;
38+
39+
/// An error for ECDSA operations.
40+
#[derive(Debug, Error)]
41+
#[error("ECDSA error")]
42+
pub struct EcdsaError(#[source] pub(crate) super::BackendError);
43+
44+
/// The ECC curve to use.
45+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46+
pub enum EcdsaCurve {
47+
/// NIST P-384 (secp384r1)
48+
P384,
49+
}
50+
51+
impl EcdsaCurve {
52+
/// The size of a single coordinate or scalar for this curve, in bytes.
53+
pub fn key_size(self) -> usize {
54+
match self {
55+
EcdsaCurve::P384 => 48,
56+
}
57+
}
58+
}
59+
60+
/// An ECDSA key pair (private + public key).
61+
pub struct EcdsaKeyPair(sys::EcdsaKeyPairInner);
62+
63+
impl EcdsaKeyPair {
64+
/// Generate a new random ECDSA key pair for the given curve.
65+
pub fn generate(curve: EcdsaCurve) -> Result<Self, EcdsaError> {
66+
sys::EcdsaKeyPairInner::generate(curve).map(Self)
67+
}
68+
69+
/// Sign a pre-computed hash value. Returns the signature as `r || s`
70+
/// in big-endian, each component `curve.key_size()` bytes.
71+
pub fn sign_prehash(&self, hash: &[u8]) -> Result<Vec<u8>, EcdsaError> {
72+
self.0.sign_prehash(hash)
73+
}
74+
75+
/// Export the public key as `Qx || Qy` in big-endian, each component
76+
/// `curve.key_size()` bytes.
77+
pub fn public_key_bytes(&self) -> Result<Vec<u8>, EcdsaError> {
78+
self.0.public_key_bytes()
79+
}
80+
}
81+
82+
#[cfg(all(test, not(all(native, target_os = "macos"))))]
83+
mod tests {
84+
use super::*;
85+
86+
#[test]
87+
fn generate_p384_key_pair() {
88+
let key = EcdsaKeyPair::generate(EcdsaCurve::P384).unwrap();
89+
let pub_key = key.public_key_bytes().unwrap();
90+
// P-384 public key is Qx || Qy, each 48 bytes.
91+
assert_eq!(pub_key.len(), 96);
92+
}
93+
94+
#[test]
95+
fn sign_prehash_p384_produces_correct_size() {
96+
let key = EcdsaKeyPair::generate(EcdsaCurve::P384).unwrap();
97+
// SHA-384 hash (48 bytes)
98+
let hash = [0xABu8; 48];
99+
let sig = key.sign_prehash(&hash).unwrap();
100+
// P-384 signature is r || s, each 48 bytes.
101+
assert_eq!(sig.len(), 96);
102+
}
103+
104+
#[test]
105+
fn sign_prehash_p384_is_non_deterministic() {
106+
let key = EcdsaKeyPair::generate(EcdsaCurve::P384).unwrap();
107+
let hash = [0x42u8; 48];
108+
let sig1 = key.sign_prehash(&hash).unwrap();
109+
let sig2 = key.sign_prehash(&hash).unwrap();
110+
// ECDSA uses a random nonce, so two signatures of the same hash
111+
// should differ (with overwhelming probability).
112+
assert_ne!(sig1, sig2);
113+
}
114+
115+
#[test]
116+
fn two_keys_produce_different_public_keys() {
117+
let key1 = EcdsaKeyPair::generate(EcdsaCurve::P384).unwrap();
118+
let key2 = EcdsaKeyPair::generate(EcdsaCurve::P384).unwrap();
119+
assert_ne!(
120+
key1.public_key_bytes().unwrap(),
121+
key2.public_key_bytes().unwrap()
122+
);
123+
}
124+
125+
#[test]
126+
fn public_key_is_stable_across_exports() {
127+
let key = EcdsaKeyPair::generate(EcdsaCurve::P384).unwrap();
128+
let pk1 = key.public_key_bytes().unwrap();
129+
let pk2 = key.public_key_bytes().unwrap();
130+
assert_eq!(pk1, pk2);
131+
}
132+
133+
/// Verify that the signature components (r, s) are valid big-endian
134+
/// integers — i.e., they are not all zeros and not larger than the
135+
/// curve order.
136+
#[test]
137+
fn signature_components_are_valid() {
138+
let key = EcdsaKeyPair::generate(EcdsaCurve::P384).unwrap();
139+
let hash = [0x01u8; 48];
140+
let sig = key.sign_prehash(&hash).unwrap();
141+
142+
let r = &sig[..48];
143+
let s = &sig[48..];
144+
145+
// Neither component should be all zeros.
146+
assert_ne!(r, &[0u8; 48][..]);
147+
assert_ne!(s, &[0u8; 48][..]);
148+
149+
// P-384 order n (big-endian):
150+
// FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
151+
// C7634D81 F4372DDF 581A0DB2 48B0A77A ECEC196A CCC52973
152+
let n: [u8; 48] = [
153+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
154+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0x63, 0x4D, 0x81,
155+
0xF4, 0x37, 0x2D, 0xDF, 0x58, 0x1A, 0x0D, 0xB2, 0x48, 0xB0, 0xA7, 0x7A, 0xEC, 0xEC,
156+
0x19, 0x6A, 0xCC, 0xC5, 0x29, 0x73,
157+
];
158+
159+
// r and s must be < n (valid ECDSA signature).
160+
assert!(r < &n[..], "r must be less than curve order");
161+
assert!(s < &n[..], "s must be less than curve order");
162+
}
163+
164+
/// Verify signature using OpenSSL (roundtrip test). This test exercises
165+
/// the full flow: generate key → sign → export public key → verify with
166+
/// the exported public key using the openssl crate directly.
167+
#[cfg(openssl)]
168+
#[test]
169+
fn roundtrip_sign_verify_with_openssl() {
170+
use openssl::bn::BigNum;
171+
use openssl::bn::BigNumContext;
172+
use openssl::ec::EcGroup;
173+
use openssl::ec::EcKey;
174+
use openssl::ec::EcPoint;
175+
use openssl::ecdsa::EcdsaSig;
176+
use openssl::nid::Nid;
177+
178+
let key = EcdsaKeyPair::generate(EcdsaCurve::P384).unwrap();
179+
let hash = [0xDE, 0xAD, 0xBE, 0xEF].repeat(12); // 48 bytes
180+
181+
let sig_bytes = key.sign_prehash(&hash).unwrap();
182+
let pub_bytes = key.public_key_bytes().unwrap();
183+
184+
// Reconstruct the public key using OpenSSL.
185+
let group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap();
186+
let mut ctx = BigNumContext::new().unwrap();
187+
let x = BigNum::from_slice(&pub_bytes[..48]).unwrap();
188+
let y = BigNum::from_slice(&pub_bytes[48..]).unwrap();
189+
let mut pub_point = EcPoint::new(&group).unwrap();
190+
pub_point
191+
.set_affine_coordinates_gfp(&group, &x, &y, &mut ctx)
192+
.unwrap();
193+
let ec_pub = EcKey::from_public_key(&group, &pub_point).unwrap();
194+
195+
// Reconstruct the ECDSA signature.
196+
let r = BigNum::from_slice(&sig_bytes[..48]).unwrap();
197+
let s = BigNum::from_slice(&sig_bytes[48..]).unwrap();
198+
let sig = EcdsaSig::from_private_components(r, s).unwrap();
199+
200+
// Verify.
201+
assert!(sig.verify(&hash, &ec_pub).unwrap());
202+
}
203+
}

support/crypto/src/ecdsa/ossl.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! ECDSA implementation using OpenSSL.
5+
6+
use super::EcdsaCurve;
7+
use super::EcdsaError;
8+
9+
fn err(e: openssl::error::ErrorStack, op: &'static str) -> EcdsaError {
10+
EcdsaError(crate::BackendError(e, op))
11+
}
12+
13+
pub struct EcdsaKeyPairInner {
14+
pkey: openssl::pkey::PKey<openssl::pkey::Private>,
15+
curve: EcdsaCurve,
16+
}
17+
18+
impl EcdsaKeyPairInner {
19+
pub fn generate(curve: EcdsaCurve) -> Result<Self, EcdsaError> {
20+
let nid = match curve {
21+
EcdsaCurve::P384 => openssl::nid::Nid::SECP384R1,
22+
};
23+
let ec_group =
24+
openssl::ec::EcGroup::from_curve_name(nid).map_err(|e| err(e, "creating EC group"))?;
25+
let ec_key =
26+
openssl::ec::EcKey::generate(&ec_group).map_err(|e| err(e, "generating EC key"))?;
27+
let pkey = openssl::pkey::PKey::from_ec_key(ec_key)
28+
.map_err(|e| err(e, "converting EC key to PKey"))?;
29+
Ok(Self { pkey, curve })
30+
}
31+
32+
pub fn sign_prehash(&self, hash: &[u8]) -> Result<Vec<u8>, EcdsaError> {
33+
let ec_key = self
34+
.pkey
35+
.ec_key()
36+
.map_err(|e| err(e, "getting EC key from PKey"))?;
37+
let sig =
38+
openssl::ecdsa::EcdsaSig::sign(hash, &ec_key).map_err(|e| err(e, "ECDSA sign"))?;
39+
40+
let key_size = self.curve.key_size();
41+
let r_bytes = sig
42+
.r()
43+
.to_vec_padded(key_size as i32)
44+
.map_err(|e| err(e, "padding r"))?;
45+
let s_bytes = sig
46+
.s()
47+
.to_vec_padded(key_size as i32)
48+
.map_err(|e| err(e, "padding s"))?;
49+
50+
let mut result = Vec::with_capacity(key_size * 2);
51+
result.extend_from_slice(&r_bytes);
52+
result.extend_from_slice(&s_bytes);
53+
Ok(result)
54+
}
55+
56+
pub fn public_key_bytes(&self) -> Result<Vec<u8>, EcdsaError> {
57+
let ec_key = self
58+
.pkey
59+
.ec_key()
60+
.map_err(|e| err(e, "getting EC key from PKey"))?;
61+
let group = ec_key.group();
62+
let pub_key = ec_key.public_key();
63+
64+
let mut ctx =
65+
openssl::bn::BigNumContext::new().map_err(|e| err(e, "creating BigNumContext"))?;
66+
let mut x = openssl::bn::BigNum::new().map_err(|e| err(e, "creating BigNum for x"))?;
67+
let mut y = openssl::bn::BigNum::new().map_err(|e| err(e, "creating BigNum for y"))?;
68+
69+
pub_key
70+
.affine_coordinates_gfp(group, &mut x, &mut y, &mut ctx)
71+
.map_err(|e| err(e, "getting affine coordinates"))?;
72+
73+
let key_size = self.curve.key_size();
74+
let x_bytes = x
75+
.to_vec_padded(key_size as i32)
76+
.map_err(|e| err(e, "padding x coordinate"))?;
77+
let y_bytes = y
78+
.to_vec_padded(key_size as i32)
79+
.map_err(|e| err(e, "padding y coordinate"))?;
80+
81+
let mut result = Vec::with_capacity(key_size * 2);
82+
result.extend_from_slice(&x_bytes);
83+
result.extend_from_slice(&y_bytes);
84+
Ok(result)
85+
}
86+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! ECDSA implementation using SymCrypt.
5+
6+
use super::EcdsaCurve;
7+
use super::EcdsaError;
8+
9+
fn err(e: symcrypt::errors::SymCryptError, op: &'static str) -> EcdsaError {
10+
EcdsaError(crate::BackendError::SymCrypt(e, op))
11+
}
12+
13+
pub struct EcdsaKeyPairInner {
14+
key: symcrypt::ecc::EcKey,
15+
}
16+
17+
impl EcdsaKeyPairInner {
18+
pub fn generate(curve: EcdsaCurve) -> Result<Self, EcdsaError> {
19+
let curve_type = match curve {
20+
EcdsaCurve::P384 => symcrypt::ecc::CurveType::NistP384,
21+
};
22+
let key =
23+
symcrypt::ecc::EcKey::generate_key_pair(curve_type, symcrypt::ecc::EcKeyUsage::EcDsa)
24+
.map_err(|e| err(e, "generating ECDSA key pair"))?;
25+
Ok(Self { key })
26+
}
27+
28+
pub fn sign_prehash(&self, hash: &[u8]) -> Result<Vec<u8>, EcdsaError> {
29+
self.key.ecdsa_sign(hash).map_err(|e| err(e, "ECDSA sign"))
30+
}
31+
32+
pub fn public_key_bytes(&self) -> Result<Vec<u8>, EcdsaError> {
33+
self.key
34+
.export_public_key()
35+
.map_err(|e| err(e, "exporting public key"))
36+
}
37+
}

0 commit comments

Comments
 (0)