@@ -65,10 +65,29 @@ fn sha256(data: &[u8]) -> [u8; 32] {
6565 hasher. finalize ( ) . into ( )
6666}
6767
68+ /// Returns the canonical PKCS#8 DER encoding of a P-256 key pair.
69+ ///
70+ /// This uses `p256::SecretKey::to_pkcs8_der()` directly instead of
71+ /// `rcgen::KeyPair::serialized_der()` to decouple the encoding from the
72+ /// rcgen library version. The p256 crate produces canonical ASN.1 DER which
73+ /// is deterministic and identical to rcgen's current output.
74+ ///
75+ /// This matters because `derive_dh_secret` hashes the PKCS#8 DER bytes
76+ /// (including the public key) to produce a secret — a historical design
77+ /// choice that we must preserve for backward compatibility.
78+ fn p256_keypair_to_pkcs8_der ( key_pair : & KeyPair ) -> Result < Vec < u8 > > {
79+ let sk = p256:: SecretKey :: from_pkcs8_der ( key_pair. serialized_der ( ) )
80+ . context ( "failed to decode secret key" ) ?;
81+ let pkcs8_der = sk
82+ . to_pkcs8_der ( )
83+ . context ( "failed to encode secret key to PKCS#8 DER" ) ?;
84+ Ok ( pkcs8_der. as_bytes ( ) . to_vec ( ) )
85+ }
86+
6887/// Derives a X25519 secret from a given key pair.
6988pub fn derive_dh_secret ( from : & KeyPair , context_data : & [ & [ u8 ] ] ) -> Result < [ u8 ; 32 ] > {
7089 let key_pair = derive_p256_key_pair ( from, context_data) ?;
71- let derived_secret = sha256 ( key_pair . serialized_der ( ) ) ;
90+ let derived_secret = sha256 ( & p256_keypair_to_pkcs8_der ( & key_pair ) ? ) ;
7291 Ok ( derived_secret)
7392}
7493
@@ -95,4 +114,35 @@ mod tests {
95114 let key = KeyPair :: generate_for ( & PKCS_ECDSA_P256_SHA256 ) . unwrap ( ) ;
96115 let _derived_key = derive_p256_key_pair ( & key, & [ b"context one" ] ) . unwrap ( ) ;
97116 }
117+
118+ #[ test]
119+ fn test_derive_dh_secret_stable_output ( ) {
120+ // Fixed test vector generated from the original rcgen-based implementation.
121+ // If this test fails after a dependency upgrade, the PKCS#8 encoding has
122+ // changed and deployed secrets would be silently broken.
123+ // Do NOT update the expected value — fix the encoding instead.
124+ let root_der = hex:: decode (
125+ "308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b02\
126+ 01010420f57527cea4ab7ffb49af99b158cdc0e3ec06398f528349ea236b7d2a\
127+ fe19cec1a1440342000491f50522407ce29dce3ed7d31a15d80c1c42f13a2355\
128+ 2d2b33a0ce09ee11e47bce95936f3e7f80d195f879e28e1b144ef37ac9ab8e36\
129+ a690cbf930b775897b27",
130+ )
131+ . unwrap ( ) ;
132+ let expected_secret = "663afd58820be8ad645f9c035e93199d114ab16f738db62393bc1d7d623e8813" ;
133+
134+ let root_key = KeyPair :: from_der_and_sign_algo (
135+ & PrivateKeyDer :: try_from ( root_der. as_slice ( ) ) . unwrap ( ) ,
136+ & PKCS_ECDSA_P256_SHA256 ,
137+ )
138+ . unwrap ( ) ;
139+ let context = [ b"context one" . as_ref ( ) , b"context two" . as_ref ( ) ] ;
140+ let secret = derive_dh_secret ( & root_key, & context) . unwrap ( ) ;
141+
142+ assert_eq ! (
143+ hex:: encode( secret) ,
144+ expected_secret,
145+ "derive_dh_secret output changed — this would break existing deployments"
146+ ) ;
147+ }
98148}
0 commit comments