Skip to content

Commit a2cc7a7

Browse files
committed
Add export_keying_material
1 parent 7cc1c33 commit a2cc7a7

4 files changed

Lines changed: 158 additions & 0 deletions

File tree

bindings/bindings.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ functions = [
3939
"SSL_ConfigServerCert",
4040
"SSL_ConfigServerSessionIDCache",
4141
"SSL_DestroyResumptionTokenInfo",
42+
"SSL_ExportKeyingMaterial",
4243
"SSL_GetChannelInfo",
4344
"SSL_GetExperimentalAPI",
4445
"SSL_GetImplementedCiphers",

src/agent.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,51 @@ impl SecretAgent {
819819
CertificateInfo::new(self.fd)
820820
}
821821

822+
/// Export keying material per RFC 8446 Section 7.5.
823+
///
824+
/// This can only be called after the handshake is complete.
825+
/// In TLS 1.3, there is no distinction between no context and an empty
826+
/// context, so the caller passes `&[u8]` instead of `Option<&[u8]>`.
827+
///
828+
/// # Errors
829+
///
830+
/// Returns error if handshake is not complete or export fails.
831+
pub fn export_keying_material(
832+
&self,
833+
label: &[u8],
834+
context: &[u8],
835+
out_len: usize,
836+
) -> Res<Vec<u8>> {
837+
if !self.state.is_connected() {
838+
return Err(Error::InvalidState);
839+
}
840+
841+
if out_len == 0 {
842+
return Err(Error::InvalidState);
843+
}
844+
let mut out = vec![0u8; out_len];
845+
846+
let has_context = !context.is_empty();
847+
secstatus_to_res(unsafe {
848+
ssl::SSL_ExportKeyingMaterial(
849+
self.fd,
850+
label.as_ptr().cast(),
851+
c_uint::try_from(label.len())?,
852+
PRBool::from(has_context),
853+
if has_context {
854+
context.as_ptr()
855+
} else {
856+
null()
857+
},
858+
c_uint::try_from(context.len())?,
859+
out.as_mut_ptr(),
860+
c_uint::try_from(out_len)?,
861+
)
862+
})?;
863+
864+
Ok(out)
865+
}
866+
822867
/// Return any fatal alert that the TLS stack might have sent.
823868
#[must_use]
824869
pub fn alert(&self) -> Option<Alert> {

src/err.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ pub enum Error {
8484
InvalidCertificateCompressionID,
8585
#[error("Invalid input")]
8686
InvalidInput,
87+
#[error("Invalid state for this operation")]
88+
InvalidState,
8789
#[error("Mixed handshake method")]
8890
MixedHandshakeMethod,
8991
#[error("No data available")]

tests/agent.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,3 +826,113 @@ fn connection_fails_encoder_returned_too_long() {
826826

827827
connect_fail(&mut client, &mut server);
828828
}
829+
830+
fn connected_pair() -> (Client, Server) {
831+
fixture_init();
832+
let mut client = Client::new("server.example", true).expect("should create client");
833+
let mut server = Server::new(&["key"]).expect("should create server");
834+
connect(&mut client, &mut server);
835+
(client, server)
836+
}
837+
838+
#[test]
839+
fn export_keying_material_basic() {
840+
let (client, _server) = connected_pair();
841+
842+
let material = client
843+
.export_keying_material(b"EXPORTER-test", &[], 32)
844+
.expect("should export keying material");
845+
assert_eq!(material.len(), 32);
846+
}
847+
848+
#[test]
849+
fn export_keying_material_differs_across_connections() {
850+
let label = b"EXPORTER-test";
851+
let context = b"context-data";
852+
853+
let (client1, _server1) = connected_pair();
854+
let material1 = client1
855+
.export_keying_material(label, context, 32)
856+
.expect("first connection export");
857+
858+
let (client2, _server2) = connected_pair();
859+
let material2 = client2
860+
.export_keying_material(label, context, 32)
861+
.expect("second connection export");
862+
863+
assert_ne!(
864+
material1, material2,
865+
"Different connections should produce different keying material"
866+
);
867+
}
868+
869+
#[test]
870+
fn export_keying_material_different_labels() {
871+
let (client, _server) = connected_pair();
872+
873+
let material1 = client
874+
.export_keying_material(b"EXPORTER-test1", &[], 32)
875+
.expect("export with label1");
876+
let material2 = client
877+
.export_keying_material(b"EXPORTER-test2", &[], 32)
878+
.expect("export with label2");
879+
880+
assert_eq!(material1.len(), 32);
881+
assert_eq!(material2.len(), 32);
882+
assert_ne!(
883+
material1, material2,
884+
"Different labels should produce different output"
885+
);
886+
}
887+
888+
#[test]
889+
fn export_keying_material_different_contexts() {
890+
let (client, _server) = connected_pair();
891+
892+
let material1 = client
893+
.export_keying_material(b"EXPORTER-test", b"context1", 32)
894+
.expect("export with context1");
895+
let material2 = client
896+
.export_keying_material(b"EXPORTER-test", b"context2", 32)
897+
.expect("export with context2");
898+
899+
assert_eq!(material1.len(), 32);
900+
assert_eq!(material2.len(), 32);
901+
assert_ne!(
902+
material1, material2,
903+
"Different contexts should produce different output"
904+
);
905+
}
906+
907+
#[test]
908+
fn export_keying_material_before_handshake() {
909+
fixture_init();
910+
let client = Client::new("server.example", true).expect("should create client");
911+
912+
assert_eq!(
913+
client
914+
.export_keying_material(b"EXPORTER-test", &[], 32)
915+
.unwrap_err(),
916+
Error::InvalidState
917+
);
918+
}
919+
920+
#[test]
921+
fn export_keying_material_same_for_both_sides() {
922+
let (client, server) = connected_pair();
923+
924+
let label = b"EXPORTER-test";
925+
let context = b"shared-context";
926+
927+
let client_material = client
928+
.export_keying_material(label, context, 32)
929+
.expect("client export");
930+
let server_material = server
931+
.export_keying_material(label, context, 32)
932+
.expect("server export");
933+
934+
assert_eq!(
935+
client_material, server_material,
936+
"Both sides should export identical material"
937+
);
938+
}

0 commit comments

Comments
 (0)