diff --git a/.github/workflows/ghpages.yml b/.github/workflows/ghpages.yml index 118cb52..6db1cfd 100644 --- a/.github/workflows/ghpages.yml +++ b/.github/workflows/ghpages.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Checkout" - uses: actions/checkout@v2 + uses: actions/checkout@v6 - name: "Cache Setup" id: cache-setup @@ -30,7 +30,7 @@ jobs: date -u "+::set-output name=date::%FT%T" - name: "Cache References" - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: | ${{ steps.cache-setup.outputs.path }} @@ -53,7 +53,7 @@ jobs: token: ${{ github.token }} - name: "Archive Built Drafts" - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v6 with: path: | draft-*.html diff --git a/reference-implementation/src/lib.rs b/reference-implementation/src/lib.rs index 3f81b1e..f7ee98e 100644 --- a/reference-implementation/src/lib.rs +++ b/reference-implementation/src/lib.rs @@ -36,9 +36,13 @@ pub mod header; /// Cipher agility layer pub mod cipher; +/// Per-SSRC stream key derivation for RTP +pub mod per_ssrc_key_derivation; + pub use crate::{ cipher::{new_cipher, Cipher, CipherSuite, SFrameIntermediateValues}, header::{Counter, Header, KeyId}, + per_ssrc_key_derivation::derive_ssrc_key, }; use std::collections::HashMap; diff --git a/reference-implementation/src/per_ssrc_key_derivation.rs b/reference-implementation/src/per_ssrc_key_derivation.rs new file mode 100644 index 0000000..7c451d8 --- /dev/null +++ b/reference-implementation/src/per_ssrc_key_derivation.rs @@ -0,0 +1,38 @@ +use hkdf::Hkdf; +use hmac::SimpleHmac; +use sha2::{Sha256, Sha512}; + +/// Derive the per-SSRC stream key for a given SFrame cipher suite as defined in Section 7 of draft-ietf-avtcore-rtp-sframe +/// +/// This implements the RTP stream key derivation from the SFrame RTP payload format draft: +/// ```text +/// PRK = HKDF-Extract(salt=SSRC_big_endian_4_bytes, ikm=base_key) +/// stream_key = HKDF-Expand(PRK, info="SFrame 1.0 RTP Stream", L=Nh) +/// ``` +/// +/// Hash function and output length (Nh) are determined by cipher suite: +/// - Suites 0x0001–0x0004: SHA-256, Nh = 32 +/// - Suites 0x0005–0x0008: SHA-512, Nh = 64 +/// +/// # Panics +/// Panics if `cipher_suite` is not in the range 0x0001–0x0008. +pub fn derive_ssrc_key(ssrc: u32, base_key: &[u8], cipher_suite: u16) -> Vec { + static INFO: &[u8] = b"SFrame 1.0 RTP Stream"; + let ssrc_salt = ssrc.to_be_bytes(); + + match cipher_suite { + 0x0001..=0x0004 => { + let (_, h) = Hkdf::::extract(Some(&ssrc_salt), base_key); + let mut key = vec![0u8; 32]; + h.expand(INFO, &mut key).expect("HKDF-Expand failed"); + key + } + 0x0005..=0x0008 => { + let (_, h) = Hkdf::>::extract(Some(&ssrc_salt), base_key); + let mut key = vec![0u8; 64]; + h.expand(INFO, &mut key).expect("HKDF-Expand failed"); + key + } + _ => panic!("Unsupported SFrame cipher suite: 0x{:04x}", cipher_suite), + } +} diff --git a/reference-implementation/tests/per_ssrc_key_derivation.rs b/reference-implementation/tests/per_ssrc_key_derivation.rs new file mode 100644 index 0000000..cc0feb3 --- /dev/null +++ b/reference-implementation/tests/per_ssrc_key_derivation.rs @@ -0,0 +1,57 @@ +// Test harness for SFrame per-SSRC stream key derivation vectors. +// Loads per-ssrc-key-derivation.json from the project's test-vectors directory +// and verifies that the Rust implementation produces identical keys. + +use serde::Deserialize; +use sframe_reference::derive_ssrc_key; + +#[derive(Deserialize)] +struct VectorEntry { + cipher_suite: u16, + base_key: String, + ssrc_a: String, + ssrc_b: String, + stream_key_a: String, + stream_key_b: String, +} + +fn hex_to_bytes(s: &str) -> Vec { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("invalid hex")) + .collect() +} + +fn parse_ssrc(s: &str) -> u32 { + u32::from_str_radix(s.trim_start_matches("0x"), 16).expect("invalid ssrc") +} + +#[test] +fn per_ssrc_stream_key_vectors() { + let json = include_str!("../../test-vectors/test-vectors-per-ssrc-key-derivation.json"); + let entries: Vec = serde_json::from_str(json).expect("failed to parse vectors"); + + assert!(!entries.is_empty(), "Expected at least one vector entry"); + + for entry in &entries { + let base_key = hex_to_bytes(&entry.base_key); + let ssrc_a = parse_ssrc(&entry.ssrc_a); + let ssrc_b = parse_ssrc(&entry.ssrc_b); + + let computed_a = derive_ssrc_key(ssrc_a, &base_key, entry.cipher_suite); + let computed_b = derive_ssrc_key(ssrc_b, &base_key, entry.cipher_suite); + + assert_eq!( + computed_a, + hex_to_bytes(&entry.stream_key_a), + "stream_key_a mismatch for cipher_suite 0x{:04x}", + entry.cipher_suite + ); + assert_eq!( + computed_b, + hex_to_bytes(&entry.stream_key_b), + "stream_key_b mismatch for cipher_suite 0x{:04x}", + entry.cipher_suite + ); + } +} diff --git a/test-vectors/test-vectors-per-ssrc-key-derivation.json b/test-vectors/test-vectors-per-ssrc-key-derivation.json new file mode 100644 index 0000000..60e872a --- /dev/null +++ b/test-vectors/test-vectors-per-ssrc-key-derivation.json @@ -0,0 +1,66 @@ +[ + { + "cipher_suite": 1, + "base_key": "000102030405060708090a0b0c0d0e0f", + "ssrc_a": "0x12345678", + "ssrc_b": "0x98765432", + "stream_key_a": "f309f118aecdacfbc165cc080022b297100bbb55e483b002d762000a0faa684d", + "stream_key_b": "06e5d1b8c6e01f10b592bd2e13ff4b37182b895ec8330d5cc6ec690704043b5f" + }, + { + "cipher_suite": 2, + "base_key": "000102030405060708090a0b0c0d0e0f", + "ssrc_a": "0x12345678", + "ssrc_b": "0x98765432", + "stream_key_a": "f309f118aecdacfbc165cc080022b297100bbb55e483b002d762000a0faa684d", + "stream_key_b": "06e5d1b8c6e01f10b592bd2e13ff4b37182b895ec8330d5cc6ec690704043b5f" + }, + { + "cipher_suite": 3, + "base_key": "000102030405060708090a0b0c0d0e0f", + "ssrc_a": "0x12345678", + "ssrc_b": "0x98765432", + "stream_key_a": "f309f118aecdacfbc165cc080022b297100bbb55e483b002d762000a0faa684d", + "stream_key_b": "06e5d1b8c6e01f10b592bd2e13ff4b37182b895ec8330d5cc6ec690704043b5f" + }, + { + "cipher_suite": 4, + "base_key": "000102030405060708090a0b0c0d0e0f", + "ssrc_a": "0x12345678", + "ssrc_b": "0x98765432", + "stream_key_a": "f309f118aecdacfbc165cc080022b297100bbb55e483b002d762000a0faa684d", + "stream_key_b": "06e5d1b8c6e01f10b592bd2e13ff4b37182b895ec8330d5cc6ec690704043b5f" + }, + { + "cipher_suite": 5, + "base_key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "ssrc_a": "0x12345678", + "ssrc_b": "0x98765432", + "stream_key_a": "4715e2a3e3ecb408c83135a30b8e660c75a3488dbd4d9dd3553a01b39df76bc43a5a0b130159edd90353b09c810e849db7398182ac7c1dea072a2c1b6bac7dda", + "stream_key_b": "f7891c6b18d88a93ba7638875b87146df4cb9693ca3837b480e9221539e86c1c98391cb1273423b33f94abe7f67c1070997a588084a54391ebda18b3d6ff92f1" + }, + { + "cipher_suite": 6, + "base_key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "ssrc_a": "0x12345678", + "ssrc_b": "0x98765432", + "stream_key_a": "4715e2a3e3ecb408c83135a30b8e660c75a3488dbd4d9dd3553a01b39df76bc43a5a0b130159edd90353b09c810e849db7398182ac7c1dea072a2c1b6bac7dda", + "stream_key_b": "f7891c6b18d88a93ba7638875b87146df4cb9693ca3837b480e9221539e86c1c98391cb1273423b33f94abe7f67c1070997a588084a54391ebda18b3d6ff92f1" + }, + { + "cipher_suite": 7, + "base_key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "ssrc_a": "0x12345678", + "ssrc_b": "0x98765432", + "stream_key_a": "4715e2a3e3ecb408c83135a30b8e660c75a3488dbd4d9dd3553a01b39df76bc43a5a0b130159edd90353b09c810e849db7398182ac7c1dea072a2c1b6bac7dda", + "stream_key_b": "f7891c6b18d88a93ba7638875b87146df4cb9693ca3837b480e9221539e86c1c98391cb1273423b33f94abe7f67c1070997a588084a54391ebda18b3d6ff92f1" + }, + { + "cipher_suite": 8, + "base_key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "ssrc_a": "0x12345678", + "ssrc_b": "0x98765432", + "stream_key_a": "4715e2a3e3ecb408c83135a30b8e660c75a3488dbd4d9dd3553a01b39df76bc43a5a0b130159edd90353b09c810e849db7398182ac7c1dea072a2c1b6bac7dda", + "stream_key_b": "f7891c6b18d88a93ba7638875b87146df4cb9693ca3837b480e9221539e86c1c98391cb1273423b33f94abe7f67c1070997a588084a54391ebda18b3d6ff92f1" + } +]