Skip to content

Commit e320ffa

Browse files
authored
Add export_keying_material (#41)
1 parent 7cc1c33 commit e320ffa

4 files changed

Lines changed: 164 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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,40 @@ 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::InvalidState` if the handshake is not complete,
831+
/// `Error::InvalidInput` if `out` is empty, or an NSS error if the
832+
/// export fails.
833+
pub fn export_keying_material(&self, label: &[u8], context: &[u8], out: &mut [u8]) -> Res<()> {
834+
if !self.state.is_connected() {
835+
return Err(Error::InvalidState);
836+
}
837+
838+
if out.is_empty() {
839+
return Err(Error::InvalidInput);
840+
}
841+
842+
secstatus_to_res(unsafe {
843+
ssl::SSL_ExportKeyingMaterial(
844+
self.fd,
845+
label.as_ptr().cast(),
846+
c_uint::try_from(label.len())?,
847+
PRBool::from(!context.is_empty()),
848+
context.as_ptr(),
849+
c_uint::try_from(context.len())?,
850+
out.as_mut_ptr(),
851+
c_uint::try_from(out.len())?,
852+
)
853+
})
854+
}
855+
822856
/// Return any fatal alert that the TLS stack might have sent.
823857
#[must_use]
824858
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: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,3 +826,130 @@ 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 mut material = vec![0u8; 32];
843+
client
844+
.export_keying_material(b"EXPORTER-test", &[], &mut material)
845+
.expect("should export keying material");
846+
assert_ne!(material, vec![0u8; 32]);
847+
}
848+
849+
#[test]
850+
fn export_keying_material_differs_across_connections() {
851+
let label = b"EXPORTER-test";
852+
let context = b"context-data";
853+
854+
let (client1, _server1) = connected_pair();
855+
let mut material1 = vec![0u8; 32];
856+
client1
857+
.export_keying_material(label, context, &mut material1)
858+
.expect("first connection export");
859+
860+
let (client2, _server2) = connected_pair();
861+
let mut material2 = vec![0u8; 32];
862+
client2
863+
.export_keying_material(label, context, &mut material2)
864+
.expect("second connection export");
865+
866+
assert_ne!(
867+
material1, material2,
868+
"Different connections should produce different keying material"
869+
);
870+
}
871+
872+
#[test]
873+
fn export_keying_material_different_labels() {
874+
let (client, _server) = connected_pair();
875+
876+
let mut material1 = vec![0u8; 32];
877+
client
878+
.export_keying_material(b"EXPORTER-test1", &[], &mut material1)
879+
.expect("export with label1");
880+
let mut material2 = vec![0u8; 32];
881+
client
882+
.export_keying_material(b"EXPORTER-test2", &[], &mut material2)
883+
.expect("export with label2");
884+
885+
assert_ne!(
886+
material1, material2,
887+
"Different labels should produce different output"
888+
);
889+
}
890+
891+
#[test]
892+
fn export_keying_material_different_contexts() {
893+
let (client, _server) = connected_pair();
894+
895+
let mut material1 = vec![0u8; 32];
896+
client
897+
.export_keying_material(b"EXPORTER-test", b"context1", &mut material1)
898+
.expect("export with context1");
899+
let mut material2 = vec![0u8; 32];
900+
client
901+
.export_keying_material(b"EXPORTER-test", b"context2", &mut material2)
902+
.expect("export with context2");
903+
904+
assert_ne!(
905+
material1, material2,
906+
"Different contexts should produce different output"
907+
);
908+
}
909+
910+
#[test]
911+
fn export_keying_material_before_handshake() {
912+
fixture_init();
913+
let client = Client::new("server.example", true).expect("should create client");
914+
915+
assert_eq!(
916+
client
917+
.export_keying_material(b"EXPORTER-test", &[], &mut [0u8; 32])
918+
.unwrap_err(),
919+
Error::InvalidState
920+
);
921+
}
922+
923+
#[test]
924+
fn export_keying_material_zero_length() {
925+
let (client, _server) = connected_pair();
926+
927+
assert_eq!(
928+
client
929+
.export_keying_material(b"EXPORTER-test", &[], &mut [])
930+
.unwrap_err(),
931+
Error::InvalidInput
932+
);
933+
}
934+
935+
#[test]
936+
fn export_keying_material_same_for_both_sides() {
937+
let (client, server) = connected_pair();
938+
939+
let label = b"EXPORTER-test";
940+
let context = b"shared-context";
941+
942+
let mut client_material = vec![0u8; 32];
943+
client
944+
.export_keying_material(label, context, &mut client_material)
945+
.expect("client export");
946+
let mut server_material = vec![0u8; 32];
947+
server
948+
.export_keying_material(label, context, &mut server_material)
949+
.expect("server export");
950+
951+
assert_eq!(
952+
client_material, server_material,
953+
"Both sides should export identical material"
954+
);
955+
}

0 commit comments

Comments
 (0)