Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ff01f11
dsc: add dscsigner
diegonieto Nov 17, 2025
f3035d0
dsc: add dscverifier element
diegonieto Nov 18, 2025
7f114ab
dsc: add dsc tests
diegonieto Nov 19, 2025
2da7eb1
dsc: move to video folder
diegonieto Nov 20, 2025
07524d9
dsc: sign and verify raw frames within a gop instead of sign frame by…
diegonieto Nov 20, 2025
7164f1d
dsc: hash only the selected NALUs instead of the whole bitstream
diegonieto Nov 22, 2025
1448b60
dsc: use metadata cert uri instead of verifier one
diegonieto Dec 1, 2025
054dc90
dsc: format fix
diegonieto Dec 1, 2025
9594cb3
dsc: signaturemeta format fix
diegonieto Dec 1, 2025
b479436
dsc: nal_parser format fix
diegonieto Dec 1, 2025
cec387e
dsc: add module descriptions
diegonieto Dec 1, 2025
13d4c15
dsc: update signature meta test
diegonieto Dec 1, 2025
5803e3c
dsc: update nal parser test
diegonieto Dec 1, 2025
0f4ca4d
dsc: fix dsc_substream format
diegonieto Dec 1, 2025
d486794
dsc: update dsc test
diegonieto Dec 1, 2025
b681464
dsc: update README.md
diegonieto Dec 1, 2025
8fa5ea0
dsc: update cargo
diegonieto Dec 1, 2025
9069685
dsc: update plugin description
diegonieto Dec 1, 2025
fd8a0b4
dsc: update commit README.md
diegonieto Dec 1, 2025
da7cd4b
dsc: add dscinserter skeleton
diegonieto Dec 9, 2025
da42ee4
dsc: wip dscinserter
diegonieto Dec 15, 2025
e63db64
dscverifier: make dscverifier compatible with VVC/VTM one
diegonieto Jan 14, 2026
d5bb640
dscverifier: polish implementation
diegonieto Jan 14, 2026
e4dd0af
dscsigner: make dscsigner compatible with VVC/VTM one
diegonieto Jan 15, 2026
0946c67
dscverifier: clean traces
diegonieto Jan 22, 2026
3a189eb
dsc: remove dependency not needed
diegonieto Jan 22, 2026
3bbb3a5
dsc: update dsc nal parser debug name
diegonieto Jan 22, 2026
df3e377
dsc: dsc nal parser. Update traces
diegonieto Jan 22, 2026
dc8a377
dscverifier: refactor transform_ip
diegonieto Jan 23, 2026
88fdb57
dscverifier: refactor verification process
diegonieto Jan 23, 2026
eadf6a9
dscverifier: post a message with verification result
diegonieto Jan 23, 2026
db4e6d0
dsc: update README.md
diegonieto Jan 23, 2026
0c08d63
dsc: update tests
diegonieto Jan 23, 2026
3e19688
dsc: update dsc tests
diegonieto Jan 26, 2026
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
2,513 changes: 1,383 additions & 1,130 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ members = [
"video/cdg",
"video/closedcaption",
"video/dav1d",
"video/dsc",
"video/ffv1",
"video/gif",
"video/gtk4",
Expand Down
42 changes: 42 additions & 0 deletions video/dsc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[package]
name = "gst-plugin-dsc"
version = "0.1.0"
authors = ["Diego Nieto <dnieto@fluendo.com>"]
edition = "2021"
description = "GStreamer Digital Signed Content plugin"
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
license = "MPL-2.0"

[dependencies]
hound = "3"
anyhow = "1"
glib.workspace = true
gst = { workspace = true, features = ["v1_20"] }
gst-base = { workspace = true, features = ["v1_20"] }
gst-video = { workspace = true, features = ["v1_30"] }
human_bytes = { version = "0.4", default-features = false }
atomic_refcell = "0.1"

openssl = "0.10"
rsa = "0.9"
rand = "0.9.2"
sha2 = "0.10"
base64ct = "1.7.0"
once_cell = "1"
hex = "0.4"

[lib]
name = "gstdsc"
crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"

[build-dependencies]
gst-plugin-version-helper = { git = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" }

[features]
capi = []
static = []
doc = ["gst/v1_16"]

[dev-dependencies]
gst-check = { workspace = true, features = ["v1_20"] }
373 changes: 373 additions & 0 deletions video/dsc/LICENSE-MPL-2.0

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions video/dsc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# GStreamer DSC Plugin

A GStreamer plugin for Digitaly Signed Content (DSC) that provides cryptographic signing and verification for some encoded video data based on OpenSSL library. DSC allows to verify the authenticity of the videos.
They are mechanisms for trustworthy authentication and verification of video content have recently been developed by JVET for inclusion into these video coding standards. This has been realized by three new supplemental enhancement information (SEI) messages which enable to attach cryptographic signatures to flexible chunks of data of a video stream at the network abstraction layer (NAL) unit level.

The following are the references used for this implementation:
* Paper: https://www.hhi.fraunhofer.de/fileadmin/Events/2025/IBC_2025/IBC2025PaperAuthentication_HHI.pdf
* JVET Specs: https://www.jvet-experts.org/doc_end_user/documents/40_Geneva/wg11/JVET-AN1019-v1.zip
* DSC implementation in VVC VTM: https://vcgit.hhi.fraunhofer.de/jvet/VVCSoftware_VTM/-/releases/VTM-23.13

## Elements

- **DSC Signer**: Signs data packets based on the specified hash method
- **DSC Verifier**: Verifies the signed data

## Generate certificate samples
### Create CA
```bash
openssl genrsa -out example_ca.key 4096
openssl genrsa -aes256 -out example_ca.key 4096
openssl req -x509 -new -nodes -key example_ca.key -sha256 -days 1826 -out example_ca.crt
openssl x509 -in example_ca.crt -noout -pubkey -out example_ca.pub
```

### Create Content Provider certificate
```bash
openssl genrsa -out example_content.key 4096
openssl req -new -key example_content.key -out example_content.csr
openssl x509 -req -in example_content.csr -CA example_ca.crt -CAkey example_ca.key -out example_content.crt -days 730 -sha256
openssl x509 -in example_content.crt -noout -pubkey -out example_content.pub
```

## Example
```bash
gst-launch-1.0 videotestsrc pattern=ball num-buffers=30 ! "video/x-raw,framerate=30/1" ! videoconvert ! x264enc key-int-max=5 ! dscsigner private-key-path= ./example_content.key public-key-uri= ./example_content.crt substream-length=5 ! dscverifier key-store-path= `pwd` ! h264parse ! avdec_h264 ! videoconvert ! autovideosink
```
12 changes: 12 additions & 0 deletions video/dsc/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (C) 2025, Fluendo S.A.
// Author: Diego Nieto <dnieto@fluendo.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0

fn main() {
gst_plugin_version_helper::info()
}
75 changes: 75 additions & 0 deletions video/dsc/src/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (C) 2025, Fluendo S.A.
// Author: Diego Nieto <dnieto@fluendo.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0

use openssl::hash::MessageDigest;

/// Hash method enum shared between signer and verifier
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashMethod {
Sha1,
Sha224,
Sha256,
Sha384,
Sha512,
}

impl HashMethod {
pub fn to_openssl(self) -> MessageDigest {
match self {
HashMethod::Sha1 => MessageDigest::sha1(),
HashMethod::Sha224 => MessageDigest::sha224(),
HashMethod::Sha256 => MessageDigest::sha256(),
HashMethod::Sha384 => MessageDigest::sha384(),
HashMethod::Sha512 => MessageDigest::sha512(),
}
}
}

impl Default for HashMethod {
fn default() -> Self {
HashMethod::Sha256
}
}

impl ToString for HashMethod {
fn to_string(&self) -> String {
match self {
HashMethod::Sha1 => "sha1",
HashMethod::Sha224 => "sha224",
HashMethod::Sha256 => "sha256",
HashMethod::Sha384 => "sha384",
HashMethod::Sha512 => "sha512",
}.to_string()
}
}

impl From<u8> for HashMethod {
fn from(value: u8) -> Self {
match value {
0 => HashMethod::Sha1,
1 => HashMethod::Sha224,
2 => HashMethod::Sha256,
3 => HashMethod::Sha384,
4 => HashMethod::Sha512,
_ => HashMethod::Sha256, // default
}
}
}

impl Into<u8> for HashMethod {
fn into(self) -> u8 {
match self {
HashMethod::Sha1 => 0,
HashMethod::Sha224 => 1,
HashMethod::Sha256 => 2,
HashMethod::Sha384 => 3,
HashMethod::Sha512 => 4,
}
}
}
207 changes: 207 additions & 0 deletions video/dsc/src/dsc_substream.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// Copyright (C) 2025, Fluendo S.A.
// Author: Diego Nieto <dnieto@fluendo.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at
// <https://mozilla.org/MPL/2.0/>.
//
// SPDX-License-Identifier: MPL-2.0

use anyhow::{Result, anyhow};
use openssl::hash::Hasher;
use std::sync::LazyLock;

static CAT: LazyLock<gst::DebugCategory> = LazyLock::new(|| {
gst::DebugCategory::new(
"dsc-substream",
gst::DebugColorFlags::empty(),
Some("DSC Substream Manager")
)
});

#[derive(Clone)]
pub struct DscSubstream {
hasher: Option<Hasher>,
}

impl DscSubstream {
pub fn new(hash_method: openssl::hash::MessageDigest) -> Result<Self> {
let hasher = Hasher::new(hash_method)?;
Ok(Self {
hasher: Some(hasher),
})
}

pub fn add_to_substream(&mut self, data: &[u8]) -> Result<()> {
if self.hasher.is_none() {
return Err(anyhow!("Substream hasher not initialized"));
}

if let Some(ref mut hasher) = self.hasher {
gst::trace!(*CAT, "DscSubstream::add_to_substream - adding {} bytes", data.len());
gst::trace!(*CAT, " First 32 bytes: {:02x?}", &data[..std::cmp::min(32, data.len())]);
gst::trace!(*CAT, " Last 32 bytes: {:02x?}", &data[data.len().saturating_sub(32)..]);

gst::debug!(*CAT, " → Hashing {} bytes: {:02x?}...", data.len(), &data[..std::cmp::min(16, data.len())]);

hasher.update(data)?;
}

Ok(())
}

pub fn finalize(&mut self) -> Result<Vec<u8>> {
if let Some(mut hasher) = self.hasher.take() {
let digest = hasher.finish()?;
gst::debug!(*CAT, "Finalized substream digest: {} bytes", digest.len());
gst::debug!(*CAT, " Full digest: {:02x?}", digest.as_ref());
Ok(digest.to_vec())
} else {
Err(anyhow!("Substream already finalized"))
}
}
}

pub struct DscSubstreamManager {
hash_method_byte: u8,
content_uuid: Option<[u8; 16]>,

substreams: Vec<Option<DscSubstream>>,

last_digest: Option<Vec<u8>>,
}

impl DscSubstreamManager {
pub fn new(
hash_method: openssl::hash::MessageDigest,
hash_method_byte: u8,
content_uuid: Option<[u8; 16]>,
) -> Result<Self> {
// Initialize the first substream
let substream = DscSubstream::new(hash_method)?;

Ok(Self {
hash_method_byte,
content_uuid,
substreams: vec![Some(substream)],
last_digest: None,
})
}

pub fn add_to_substream(&mut self, substream_id: usize, data: &[u8]) -> Result<()> {
if substream_id >= self.substreams.len() {
return Err(anyhow!("Invalid substream ID: {}", substream_id));
}

if self.substreams[substream_id].is_none() {
return Err(anyhow!("Substream {} not initialized", substream_id));
}

gst::trace!(*CAT, "DscSubstreamManager::add_to_substream - substream {}, {} bytes total",
substream_id, data.len());

if let Some(ref mut substream) = self.substreams[substream_id] {
substream.add_to_substream(data)?;
}

Ok(())
}

// Creates the data packet that will be signed: [ref_digest][current_digest][hash_method][uuid?]
pub fn create_data_packet(&mut self, substream_id: usize) -> Result<Vec<u8>> {
let current_digest = self.finalize_substream(substream_id)?;

gst::debug!(*CAT, "Creating data packet for substream {}", substream_id);
gst::debug!(*CAT, "Current digest ({} bytes): {:02x?}", current_digest.len(), current_digest);

let mut data_packet = Vec::new();

// Reference digest (all 0xFF for first GOP, or last digest from previous GOP)
let ref_digest = if let Some(ref last) = self.last_digest {
gst::debug!(*CAT, "Using previous digest as reference ({} bytes)", last.len());
last.clone()
} else {
gst::debug!(*CAT, "First GOP - using all 0xFF as reference digest");
vec![0xFF; current_digest.len()]
};

gst::debug!(*CAT, "Reference digest ({} bytes): {:02x?}", ref_digest.len(), &ref_digest[..std::cmp::min(32, ref_digest.len())]);
data_packet.extend_from_slice(&ref_digest);

// Current digest
data_packet.extend_from_slice(&current_digest);

// Hash method type byte
data_packet.push(self.hash_method_byte);
gst::debug!(*CAT, "Hash method byte: {}", self.hash_method_byte);

// Content UUID (if present)
if let Some(ref uuid) = self.content_uuid {
data_packet.extend_from_slice(uuid);
gst::debug!(*CAT, "Added content UUID: {:02x?}", uuid);
}

gst::debug!(*CAT, "Final data packet ({} bytes): first 32: {:02x?}, last 32: {:02x?}",
data_packet.len(),
&data_packet[..std::cmp::min(32, data_packet.len())],
&data_packet[data_packet.len().saturating_sub(32)..]);

// Store current digest for next GOP
self.last_digest = Some(current_digest);

Ok(data_packet)
}

fn finalize_substream(&mut self, substream_id: usize) -> Result<Vec<u8>> {
if let Some(ref mut substream) = self.substreams.get_mut(substream_id).and_then(|s| s.as_mut()) {
let digest = substream.finalize()?;
self.substreams[substream_id] = None;
Ok(digest)
} else {
Err(anyhow!("Substream {} not found or already finalized", substream_id))
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_dsc_substream_manager_basic() {
let hash_method = openssl::hash::MessageDigest::sha256();
let mut manager = DscSubstreamManager::new(hash_method, 2, None).unwrap();

// Add some test NAL unit data
let nal_data1 = vec![0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x80]; // SPS-like
let nal_data2 = vec![0x00, 0x00, 0x00, 0x01, 0x68, 0x48, 0x90]; // PPS-like

manager.add_to_substream(0, &nal_data1).unwrap();
manager.add_to_substream(0, &nal_data2).unwrap();

// Create data packet (this finalizes the substream)
let data_packet = manager.create_data_packet(0).unwrap();

// Should contain: zero_digest + current_digest + hash_method_byte
// For SHA256: 32 + 32 + 1 = 65 bytes
assert_eq!(data_packet.len(), 65);
assert_eq!(data_packet[64], 2); // hash_method_byte
}

#[test]
fn test_dsc_substream_manager_with_content_uuid() {
let hash_method = openssl::hash::MessageDigest::sha256();
let content_uuid = Some([0u8; 16]);
let mut manager = DscSubstreamManager::new(hash_method, 2, content_uuid).unwrap();

let nal_data = vec![0x00, 0x00, 0x00, 0x01, 0x67];
manager.add_to_substream(0, &nal_data).unwrap();

let data_packet = manager.create_data_packet(0).unwrap();

// Should contain: zero_digest + current_digest + hash_method_byte + uuid
// For SHA256: 32 + 32 + 1 + 16 = 81 bytes
assert_eq!(data_packet.len(), 81);
assert_eq!(&data_packet[65..81], &[0u8; 16]); // UUID at the end
}
}
Loading