diff --git a/ra-tls/src/kdf.rs b/ra-tls/src/kdf.rs index a6e07a32..3b4e3b4d 100644 --- a/ra-tls/src/kdf.rs +++ b/ra-tls/src/kdf.rs @@ -12,6 +12,49 @@ use ring::{ }; use rustls_pki_types::PrivateKeyDer; +// PKCS#8 PrivateKeyInfo template for a P-256 key generated by p256/pkcs8 0.10, +// without the EC public key. This prefix is stable because all lengths and +// algorithm identifiers are constant for prime256v1. +// +// Structure: +// PrivateKeyInfo ::= SEQUENCE { +// version INTEGER (0), +// privateKeyAlgorithm AlgorithmIdentifier { +// id-ecPublicKey, prime256v1 +// }, +// privateKey OCTET STRING (ECPrivateKey) +// } +// +// ECPrivateKey ::= SEQUENCE { +// version INTEGER (1), +// privateKey OCTET STRING (32 bytes), +// publicKey [1] BIT STRING OPTIONAL +// } +// +// The remaining suffix encodes the [1] publicKey BIT STRING header; the +// actual SEC1-encoded uncompressed point (65 bytes) is appended after it. +const P256_PKCS8_PREFIX: [u8; 36] = [ + 0x30, 0x81, 0x87, // SEQUENCE, len 0x87 + 0x02, 0x01, 0x00, // version = 0 + 0x30, 0x13, // SEQUENCE (AlgorithmIdentifier) + 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // id-ecPublicKey + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, // prime256v1 + 0x04, 0x6d, // OCTET STRING, len 0x6d (ECPrivateKey) + 0x30, 0x6b, // SEQUENCE (ECPrivateKey), len 0x6b + 0x02, 0x01, 0x01, // version = 1 + 0x04, 0x20, // OCTET STRING, len 32 (private key) +]; + +// Context-specific [1] publicKey BIT STRING wrapper; the 65-byte SEC1 +// uncompressed point (0x04 || X || Y) is appended after this header. +const P256_PKCS8_PUBLIC_KEY_PREFIX: [u8; 5] = [ + 0xa1, 0x44, // [1] constructed, len 0x44 + 0x03, 0x42, // BIT STRING, len 0x42 + 0x00, // number of unused bits +]; + +const P256_PKCS8_TOTAL_LEN: usize = 138; + struct AnySizeKey(usize); impl KeyType for AnySizeKey { fn len(&self) -> usize { @@ -66,9 +109,65 @@ fn sha256(data: &[u8]) -> [u8; 32] { } /// Derives a X25519 secret from a given key pair. +/// +/// Historically this was implemented as: +/// 1. derive a P-256 key pair from `from` using HKDF +/// 2. hash `rcgen::KeyPair::serialized_der()` with SHA-256 +/// +/// That made the result sensitive to library-level changes in PKCS#8 +/// encoding. To avoid this, we now: +/// - derive the same P-256 scalar as before +/// - encode it using a fixed, Dstack-defined PKCS#8 layout +/// - hash that encoding with SHA-256 pub fn derive_dh_secret(from: &KeyPair, context_data: &[&[u8]]) -> Result<[u8; 32]> { - let key_pair = derive_p256_key_pair(from, context_data)?; - let derived_secret = sha256(key_pair.serialized_der()); + use p256::elliptic_curve::sec1::ToEncodedPoint; + + // 1. Decode the root CA key from rcgen::KeyPair into a P-256 scalar. + let der_bytes = from.serialized_der(); + let sk = + p256::SecretKey::from_pkcs8_der(der_bytes).context("failed to decode root secret key")?; + let sk_bytes = sk.as_scalar_primitive().to_bytes(); + + // 2. Derive the same 32-byte scalar as before using HKDF. + let derived_sk_bytes = derive_key(sk_bytes.as_slice(), context_data, 32) + .or(Err(anyhow!("failed to derive key")))?; + let derived_sk_bytes: [u8; 32] = derived_sk_bytes + .as_slice() + .try_into() + .map_err(|_| anyhow!("unexpected length for derived key"))?; + + // 3. Compute the corresponding P-256 public key (uncompressed SEC1). + let derived_sk = p256::SecretKey::from_slice(&derived_sk_bytes) + .context("failed to decode derived secret key")?; + let public_key = derived_sk.public_key(); + let encoded_point = public_key.to_encoded_point(false); + let public_key_bytes = encoded_point.as_bytes(); // 0x04 || X || Y (65 bytes) + + // 4. Build a fixed PKCS#8 PrivateKeyInfo encoding matching the previous + // rcgen/pkcs8 output for prime256v1 keys. + anyhow::ensure!( + public_key_bytes.len() == 65, + "unexpected P-256 public key length" + ); + + let mut pkcs8 = [0u8; P256_PKCS8_TOTAL_LEN]; + // Prefix up to the private key OCTET STRING contents. + pkcs8[..P256_PKCS8_PREFIX.len()].copy_from_slice(&P256_PKCS8_PREFIX); + + // 32-byte private key. + let mut offset = P256_PKCS8_PREFIX.len(); + pkcs8[offset..offset + 32].copy_from_slice(&derived_sk_bytes); + offset += 32; + + // [1] BIT STRING public key header. + pkcs8[offset..offset + P256_PKCS8_PUBLIC_KEY_PREFIX.len()] + .copy_from_slice(&P256_PKCS8_PUBLIC_KEY_PREFIX); + offset += P256_PKCS8_PUBLIC_KEY_PREFIX.len(); + + // SEC1-encoded uncompressed public key bytes. + pkcs8[offset..offset + public_key_bytes.len()].copy_from_slice(public_key_bytes); + + let derived_secret = sha256(&pkcs8); Ok(derived_secret) } @@ -95,4 +194,19 @@ mod tests { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); let _derived_key = derive_p256_key_pair(&key, &[b"context one"]).unwrap(); } + + #[test] + fn test_derive_dh_secret_compatible_with_previous_encoding() { + let root_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); + let context = [b"context one".as_ref(), b"context two".as_ref()]; + + // New implementation under test. + let new_secret = derive_dh_secret(&root_key, &context).unwrap(); + + // Previous behaviour: derive P-256 key pair with HKDF, then hash PKCS#8 DER. + let old_key_pair = derive_p256_key_pair(&root_key, &context).unwrap(); + let old_secret = sha256(old_key_pair.serialized_der()); + + assert_eq!(new_secret, old_secret); + } }