Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 116 additions & 2 deletions ra-tls/src/kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}

Expand All @@ -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);
}
}
Loading