Skip to content

Commit 45e3efc

Browse files
committed
Add export_keying_material
1 parent 7cc1c33 commit 45e3efc

4 files changed

Lines changed: 169 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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,45 @@ 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(
834+
&self,
835+
label: &[u8],
836+
context: &[u8],
837+
out: &mut [u8],
838+
) -> Res<()> {
839+
if !self.state.is_connected() {
840+
return Err(Error::InvalidState);
841+
}
842+
843+
if out.is_empty() {
844+
return Err(Error::InvalidInput);
845+
}
846+
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(!context.is_empty()),
853+
context.as_ptr(),
854+
c_uint::try_from(context.len())?,
855+
out.as_mut_ptr(),
856+
c_uint::try_from(out.len())?,
857+
)
858+
})
859+
}
860+
822861
/// Return any fatal alert that the TLS stack might have sent.
823862
#[must_use]
824863
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)