Skip to content

Commit e1faaf4

Browse files
committed
fix(ra-tls): stabilize derive_dh_secret encoding
1 parent cce5ff2 commit e1faaf4

File tree

1 file changed

+117
-2
lines changed

1 file changed

+117
-2
lines changed

ra-tls/src/kdf.rs

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,49 @@ use ring::{
1212
};
1313
use rustls_pki_types::PrivateKeyDer;
1414

15+
// PKCS#8 PrivateKeyInfo template for a P-256 key generated by p256/pkcs8 0.10,
16+
// without the EC public key. This prefix is stable because all lengths and
17+
// algorithm identifiers are constant for prime256v1.
18+
//
19+
// Structure:
20+
// PrivateKeyInfo ::= SEQUENCE {
21+
// version INTEGER (0),
22+
// privateKeyAlgorithm AlgorithmIdentifier {
23+
// id-ecPublicKey, prime256v1
24+
// },
25+
// privateKey OCTET STRING (ECPrivateKey)
26+
// }
27+
//
28+
// ECPrivateKey ::= SEQUENCE {
29+
// version INTEGER (1),
30+
// privateKey OCTET STRING (32 bytes),
31+
// publicKey [1] BIT STRING OPTIONAL
32+
// }
33+
//
34+
// The remaining suffix encodes the [1] publicKey BIT STRING header; the
35+
// actual SEC1-encoded uncompressed point (65 bytes) is appended after it.
36+
const P256_PKCS8_PREFIX: [u8; 36] = [
37+
0x30, 0x81, 0x87, // SEQUENCE, len 0x87
38+
0x02, 0x01, 0x00, // version = 0
39+
0x30, 0x13, // SEQUENCE (AlgorithmIdentifier)
40+
0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // id-ecPublicKey
41+
0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, // prime256v1
42+
0x04, 0x6d, // OCTET STRING, len 0x6d (ECPrivateKey)
43+
0x30, 0x6b, // SEQUENCE (ECPrivateKey), len 0x6b
44+
0x02, 0x01, 0x01, // version = 1
45+
0x04, 0x20, // OCTET STRING, len 32 (private key)
46+
];
47+
48+
// Context-specific [1] publicKey BIT STRING wrapper; the 65-byte SEC1
49+
// uncompressed point (0x04 || X || Y) is appended after this header.
50+
const P256_PKCS8_PUBLIC_KEY_PREFIX: [u8; 5] = [
51+
0xa1, 0x44, // [1] constructed, len 0x44
52+
0x03, 0x42, // BIT STRING, len 0x42
53+
0x00, // number of unused bits
54+
];
55+
56+
const P256_PKCS8_TOTAL_LEN: usize = 138;
57+
1558
struct AnySizeKey(usize);
1659
impl KeyType for AnySizeKey {
1760
fn len(&self) -> usize {
@@ -66,9 +109,66 @@ fn sha256(data: &[u8]) -> [u8; 32] {
66109
}
67110

68111
/// Derives a X25519 secret from a given key pair.
112+
///
113+
/// Historically this was implemented as:
114+
/// 1. derive a P-256 key pair from `from` using HKDF
115+
/// 2. hash `rcgen::KeyPair::serialized_der()` with SHA-256
116+
///
117+
/// That made the result sensitive to library-level changes in PKCS#8
118+
/// encoding. To avoid this, we now:
119+
/// - derive the same P-256 scalar as before
120+
/// - encode it using a fixed, Dstack-defined PKCS#8 layout
121+
/// - hash that encoding with SHA-256
69122
pub fn derive_dh_secret(from: &KeyPair, context_data: &[&[u8]]) -> Result<[u8; 32]> {
70-
let key_pair = derive_p256_key_pair(from, context_data)?;
71-
let derived_secret = sha256(key_pair.serialized_der());
123+
use p256::elliptic_curve::sec1::ToEncodedPoint;
124+
125+
// 1. Decode the root CA key from rcgen::KeyPair into a P-256 scalar.
126+
let der_bytes = from.serialized_der();
127+
let sk =
128+
p256::SecretKey::from_pkcs8_der(der_bytes).context("failed to decode root secret key")?;
129+
let sk_bytes = sk.as_scalar_primitive().to_bytes();
130+
131+
// 2. Derive the same 32-byte scalar as before using HKDF.
132+
let derived_sk_bytes = derive_key(sk_bytes.as_slice(), context_data, 32)
133+
.or(Err(anyhow!("failed to derive key")))?;
134+
let derived_sk_bytes: [u8; 32] = derived_sk_bytes
135+
.as_slice()
136+
.try_into()
137+
.map_err(|_| anyhow!("unexpected length for derived key"))?;
138+
139+
// 3. Compute the corresponding P-256 public key (uncompressed SEC1).
140+
let derived_sk = p256::SecretKey::from_slice(&derived_sk_bytes)
141+
.context("failed to decode derived secret key")?;
142+
let public_key = derived_sk.public_key();
143+
let encoded_point = public_key.to_encoded_point(false);
144+
let public_key_bytes = encoded_point.as_bytes(); // 0x04 || X || Y (65 bytes)
145+
146+
// 4. Build a fixed PKCS#8 PrivateKeyInfo encoding matching the previous
147+
// rcgen/pkcs8 output for prime256v1 keys.
148+
assert_eq!(
149+
public_key_bytes.len(),
150+
65,
151+
"unexpected P-256 public key length"
152+
);
153+
154+
let mut pkcs8 = [0u8; P256_PKCS8_TOTAL_LEN];
155+
// Prefix up to the private key OCTET STRING contents.
156+
pkcs8[..P256_PKCS8_PREFIX.len()].copy_from_slice(&P256_PKCS8_PREFIX);
157+
158+
// 32-byte private key.
159+
let mut offset = P256_PKCS8_PREFIX.len();
160+
pkcs8[offset..offset + 32].copy_from_slice(&derived_sk_bytes);
161+
offset += 32;
162+
163+
// [1] BIT STRING public key header.
164+
pkcs8[offset..offset + P256_PKCS8_PUBLIC_KEY_PREFIX.len()]
165+
.copy_from_slice(&P256_PKCS8_PUBLIC_KEY_PREFIX);
166+
offset += P256_PKCS8_PUBLIC_KEY_PREFIX.len();
167+
168+
// SEC1-encoded uncompressed public key bytes.
169+
pkcs8[offset..offset + public_key_bytes.len()].copy_from_slice(public_key_bytes);
170+
171+
let derived_secret = sha256(&pkcs8);
72172
Ok(derived_secret)
73173
}
74174

@@ -95,4 +195,19 @@ mod tests {
95195
let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
96196
let _derived_key = derive_p256_key_pair(&key, &[b"context one"]).unwrap();
97197
}
198+
199+
#[test]
200+
fn test_derive_dh_secret_compatible_with_previous_encoding() {
201+
let root_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
202+
let context = [b"context one".as_ref(), b"context two".as_ref()];
203+
204+
// New implementation under test.
205+
let new_secret = derive_dh_secret(&root_key, &context).unwrap();
206+
207+
// Previous behaviour: derive P-256 key pair with HKDF, then hash PKCS#8 DER.
208+
let old_key_pair = derive_p256_key_pair(&root_key, &context).unwrap();
209+
let old_secret = sha256(old_key_pair.serialized_der());
210+
211+
assert_eq!(new_secret, old_secret);
212+
}
98213
}

0 commit comments

Comments
 (0)