@@ -12,6 +12,49 @@ use ring::{
1212} ;
1313use 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+
1558struct AnySizeKey ( usize ) ;
1659impl 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
69122pub 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