Skip to content

Commit b3f02f4

Browse files
authored
Bugfixes when signing with FH+TG (#510)
Some special cases did break the signing and vaildation when using FH+TG. In particular three cases: 1. If OBU Metadata is added to I-frames that "SEI" will belong to the previous GOP, since GOPs are triggered on primary slices. That itself is not a problem. The problematic part is the very first GOP. When triggering on the second I-frame this first GOP looks different from the others due to an extra OBU Metadata "SEI". 2. Single Frame Headers (FH) without Tile Groups (TG) shall also be hashed. So far it has been expected to be a TG after every FH, but a stream can have FHs without TGs. 3. On the signing side, TGs can come in parts, which complicated FH+TG a little bit. This commit includes * 3 new tests - Sign with data split in parts - Stream has FHs without TGs - Scrap first GOP * 2 new dummy OBUs; 1) a no show FH (FH without TG), and 2) a TD. * SEIs cannot become linked hashes on auth side * Mark the first SEI as 'U' if not a Signed Video SEI and the current GOP is incomplete. * Include the show_existing_frame bit when determining frame type. * Add a no_hash hash function * Do not update frame counter on FH * Do not trigger a new GOP on FH * Generate SEI if at least one BU was hashed before the first I-frame. * Fixes start timestamp if stream starts with SEI Also bumps the version to v2.2.7
1 parent b8a953a commit b3f02f4

File tree

10 files changed

+201
-36
lines changed

10 files changed

+201
-36
lines changed
Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
name: Signed Video Framework approval workflow
22
on: pull_request_review
33
jobs:
4-
labelWhenApproved:
5-
name: Label when approved
4+
label-when-approved:
5+
name: "Label when approved"
66
runs-on: ubuntu-latest
7+
outputs:
8+
isApprovedByCommiters: ${{ steps.label-when-approved-by-commiters.outputs.isApproved }}
9+
isApprovedByAnyone: ${{ steps.label-when-approved-by-anyone.outputs.isApproved }}
710
steps:
8-
- name: Label when approved
9-
uses: pullreminders/label-when-approved-action@master
10-
env:
11-
APPROVALS: "2"
12-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
13-
ADD_LABEL: "signed-video-framework approved"
11+
- name: Label when approved by commiters
12+
uses: TobKed/[email protected]
13+
id: label-when-approved-by-commiters
14+
with:
15+
token: ${{ secrets.GITHUB_TOKEN }}
16+
label: 'ready to merge (committers)'
17+
require_committers_approval: 'true'
18+
remove_label_when_approval_missing: 'false'
19+
comment: 'PR approved by at least one committer and no changes requested.'
20+
- name: Label when approved by anyone
21+
uses: TobKed/[email protected]
22+
id: label-when-approved-by-anyone
23+
with:
24+
token: ${{ secrets.GITHUB_TOKEN }}

lib/src/sv_auth.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ update_link_hash(signed_video_t *self, const bu_list_item_t *sei)
216216
if (item == sei) {
217217
break;
218218
}
219+
// A SEI cannot be a linked hash.
220+
// TODO: Investigate if a SEI can become a linked hash when signing multiple GOPs.
221+
if (item->bu && item->bu->bu_type == BU_TYPE_SEI) {
222+
break;
223+
}
219224

220225
sv_update_linked_hash(self, item->hash, hash_size);
221226
break;
@@ -1256,6 +1261,17 @@ maybe_validate_gop(signed_video_t *self, bu_info_t *bu)
12561261
}
12571262

12581263
if (start_sei_in_sync != validation_flags->sei_in_sync) {
1264+
// For AV1 OBU Metadata (SEIs) are hashed. Since these prepend picture OBUs. When
1265+
// prepending an I-frame they, by design, belong to the previous GOP, but will
1266+
// still be present when exporting to file. If so, the first one is not feasible
1267+
// to validate and should be marked as 'U'.
1268+
// TODO: Find a more generic solution to this.
1269+
bu_list_item_t *item = self->bu_list->first_item;
1270+
bool is_other_sei = (item->bu && item->bu->bu_type == BU_TYPE_SEI && !item->bu->is_sv_sei);
1271+
if (item->tmp_validation_status == 'N' && is_other_sei) {
1272+
item->tmp_validation_status = 'U';
1273+
}
1274+
12591275
// Some partial GOPs may have been discarded due to being out of sync at that time
12601276
// because they were in fact correctly invalid. Get stats another time and update
12611277
// |latest->authenticity|.

lib/src/sv_codec_av1.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ parse_av1_obu_header(bu_info_t *obu)
7777
obu_header_is_valid &= (obu_size == 0);
7878
break;
7979
case 3: // 3 OBU_FRAME_HEADER
80-
obu->bu_type = ((*obu_ptr & 0x60) >> 5) == 0 ? BU_TYPE_I : BU_TYPE_P;
80+
// Read frame_type (2 bits), include also show_existing_frame since it must be 0
81+
obu->bu_type = ((*obu_ptr & 0xf0) >> 5) == 0 ? BU_TYPE_I : BU_TYPE_P;
8182
obu->is_primary_slice = true;
8283
obu->ongoing_hash = obu->bu_type == BU_TYPE_I ? 2 : 1;
8384
obu->is_fh = true;

lib/src/sv_common.c

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ typedef svrc_t (*hash_wrapper_t)(signed_video_t *, const bu_info_t *, uint8_t *,
6464
static hash_wrapper_t
6565
get_hash_wrapper(signed_video_t *self, const bu_info_t *bu);
6666
static svrc_t
67+
no_hash(signed_video_t *self, const bu_info_t *bu, uint8_t *hash, size_t hash_size);
68+
static svrc_t
6769
update_hash(signed_video_t *self, const bu_info_t *bu, uint8_t *hash, size_t hash_size);
6870
#ifndef SIGNED_VIDEO_DEBUG
6971
static svrc_t
@@ -755,7 +757,9 @@ get_hash_wrapper(signed_video_t *self, const bu_info_t *bu)
755757
{
756758
assert(self && bu);
757759

758-
if (!bu->is_last_bu_part || bu->ongoing_hash) {
760+
if (!bu->is_hashable) {
761+
return no_hash;
762+
} else if (!bu->is_last_bu_part || bu->ongoing_hash) {
759763
// If this is not the last part of a BU, update the hash. Or if there is an
760764
// |ongoing_hash| for AV1 FH+TG.
761765
return update_hash;
@@ -775,6 +779,18 @@ get_hash_wrapper(signed_video_t *self, const bu_info_t *bu)
775779

776780
/* Hash wrapper functions */
777781

782+
/* no_hash()
783+
*
784+
* performs no hashing. */
785+
static svrc_t
786+
no_hash(signed_video_t ATTR_UNUSED *self,
787+
const bu_info_t ATTR_UNUSED *bu,
788+
uint8_t ATTR_UNUSED *hash,
789+
size_t ATTR_UNUSED hash_size)
790+
{
791+
return SV_OK;
792+
}
793+
778794
/* update_hash()
779795
*
780796
* takes the |hashable_data| from the Bitstream Unit, and updates the hash in |crypto_handle|. */
@@ -957,7 +973,7 @@ sv_hash_and_add(signed_video_t *self, const bu_info_t *bu)
957973
if (!self || !bu) return SV_INVALID_PARAMETER;
958974

959975
if (!bu->is_hashable) {
960-
DEBUG_LOG("This Bitstream Unit (type %d) was not hashed", bu->bu_type);
976+
DEBUG_LOG("This Bitstream Unit (type %s) was not hashed", bu_type_to_str(bu));
961977
return SV_OK;
962978
}
963979

@@ -968,10 +984,11 @@ sv_hash_and_add(signed_video_t *self, const bu_info_t *bu)
968984

969985
svrc_t status = SV_UNKNOWN_FAILURE;
970986
SV_TRY()
971-
if (bu->is_first_bu_part && (!bu->is_last_bu_part || bu->is_fh)) {
972-
// If this is the first part of a non-complete BU, or an OBU_FRAME_HEADER (FH),
973-
// initialize the |crypto_handle| to enable sequentially updating the hash with more
974-
// parts.
987+
if (bu->is_first_bu_part && (!bu->is_last_bu_part || bu->is_fh) &&
988+
(bu->bu_type != BU_TYPE_TG)) {
989+
// If this is the first part of a non-complete BU (Tile Group excluded), or an
990+
// OBU_FRAME_HEADER (FH), initialize the |crypto_handle| to enable sequentially
991+
// updating the hash with more parts.
975992
SV_THROW(sv_openssl_init_hash(self->crypto_handle, false));
976993
}
977994
// Select hash function, hash the BU and store as 'latest hash'
@@ -1003,8 +1020,9 @@ hash_and_add_for_auth(signed_video_t *self, bu_list_item_t *item)
10031020
bu_info_t *bu = item->bu;
10041021
if (!bu) return SV_INVALID_PARAMETER;
10051022

1006-
if (!bu->is_hashable) {
1007-
DEBUG_LOG("This Bitstream Unit (type %d) was not hashed.", bu->bu_type);
1023+
// Abort if this BU is not hashable unless there is an |ongoing_hash|.
1024+
if (!bu->is_hashable && !(item->prev && item->prev->bu && item->prev->bu->ongoing_hash)) {
1025+
DEBUG_LOG("This Bitstream Unit (type %s) was not hashed.", bu_type_to_str(bu));
10081026
return SV_OK;
10091027
}
10101028
if (!self->validation_flags.hash_algo_known) {
@@ -1022,8 +1040,8 @@ hash_and_add_for_auth(signed_video_t *self, bu_list_item_t *item)
10221040
bu_info_t *prev_bu = item->prev ? item->prev->bu : NULL;
10231041

10241042
// Update |ongoing_hash| from the previous item.
1025-
if (bu->bu_type == BU_TYPE_TG && item->prev) {
1026-
bu->ongoing_hash = item->prev->bu->ongoing_hash;
1043+
if (bu->bu_type == BU_TYPE_TG && prev_bu) {
1044+
bu->ongoing_hash = prev_bu->ongoing_hash;
10271045
} else {
10281046
// Find the hash of the previous FH if there is an |ongoing_hash|.
10291047
while (prev_item && prev_item->bu && prev_item->bu->ongoing_hash && !prev_item->bu->is_fh) {

lib/src/sv_internal.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
#define DEFAULT_HASH_SIZE (256 / 8)
5151

5252
#define SV_VERSION_BYTES 3
53-
#define SIGNED_VIDEO_VERSION "v2.2.6"
53+
#define SIGNED_VIDEO_VERSION "v2.2.7"
5454
#define SV_VERSION_MAX_STRLEN 19 // Longest possible string including 'ONVIF' prefix
5555

5656
#define DEFAULT_AUTHENTICITY_LEVEL SV_AUTHENTICITY_LEVEL_FRAME

lib/src/sv_sign.c

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,8 @@ signed_video_add_nalu_part_for_signing_with_timestamp(signed_video_t *self,
610610
// Update |ongoing_hash| from the previous BU.
611611
if (bu_info.bu_type == BU_TYPE_TG) {
612612
bu_info.ongoing_hash = self->last_bu->ongoing_hash;
613+
bu_info.is_primary_slice = self->last_bu->is_primary_slice;
614+
bu_info.is_first_bu_in_gop = self->last_bu->is_first_bu_in_gop;
613615
}
614616
// Finalize ongoing hash as soon as the incoming one is not a TG. Only TGs are
615617
// expected after an FH.
@@ -638,8 +640,9 @@ signed_video_add_nalu_part_for_signing_with_timestamp(signed_video_t *self,
638640
SV_THROW_IF(bu_info.is_valid < 0, SV_INVALID_PARAMETER);
639641

640642
// Note that |recurrence| is counted in frames and not in BUs, hence we only increment the
641-
// counter for primary slices.
642-
if (bu_info.is_primary_slice && bu_info.is_last_bu_part) {
643+
// counter for primary slices. Frame counting is updated at the end of a frame, hence
644+
// shall be ignored for FH.
645+
if (bu_info.is_primary_slice && bu_info.is_last_bu_part && !bu_info.is_fh) {
643646
if ((self->frame_count % self->recurrence) == 0) {
644647
self->has_recurrent_data = true;
645648
}
@@ -648,6 +651,8 @@ signed_video_add_nalu_part_for_signing_with_timestamp(signed_video_t *self,
648651

649652
// Determine if a SEI should be generated.
650653
bool new_gop = (bu_info.is_first_bu_in_gop && bu_info.is_last_bu_part);
654+
// Do not trigger on FH.
655+
new_gop &= !bu_info.is_fh;
651656
// Trigger signing if number of frames exceeds the limit for a partial GOP.
652657
bool trigger_signing = ((self->max_signing_frames > 0) &&
653658
(gop_info->num_frames_in_partial_gop >= self->max_signing_frames));
@@ -669,11 +674,23 @@ signed_video_add_nalu_part_for_signing_with_timestamp(signed_video_t *self,
669674
gop_info->has_timestamp = true;
670675
}
671676

672-
if (self->sei_generation_enabled) {
677+
// SEI creation is triggered by I-frames. Skip the first trigger unless there are
678+
// already hashed BUs.
679+
if (self->sei_generation_enabled || gop_info->list_idx > 0) {
673680
// If there are hashes added to the hash list, the |computed_gop_hash| can be finalized.
674681
SV_THROW(sv_openssl_finalize_hash(self->crypto_handle, gop_info->computed_gop_hash, true));
675682
// The previous GOP is now completed. The gop_hash was reset right after signing and
676683
// adding it to the SEI.
684+
685+
// Update GOP counter and |start_timestamp| if this first GOP started without an
686+
// I-frame.
687+
if (!self->sei_generation_enabled && gop_info->list_idx > 0) {
688+
gop_info->start_timestamp = gop_info->end_timestamp;
689+
if (gop_info->current_partial_gop < 0) {
690+
gop_info->current_partial_gop = 0;
691+
}
692+
trigger_signing = true;
693+
}
677694
SV_THROW(generate_sei_and_add_to_buffer(self, trigger_signing));
678695
if (new_gop && (self->num_gops_until_signing == 0)) {
679696
// Reset signing counter only upon new GOPs

meson.build

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
project('signed-video-framework', 'c',
2-
version : '2.2.6',
2+
version : '2.2.7',
33
meson_version : '>= 0.53.0',
44
default_options : [ 'warning_level=2',
55
'werror=true',

tests/check/check_codec_av1.c

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ START_TEST(signed_stream_with_fh)
5757
// |settings_av1|; See signed_video_helpers.h.
5858

5959
struct sv_setting setting = settings_av1[_i];
60-
setting.with_fh = true;
6160
test_stream_t *list = create_signed_stream("ItPtPtItPtPtItPtPtItPtPtItPtPt", setting);
6261
test_stream_check_types(list, "ItPtPtItSPtPtItSPtPtItSPtPtItSPtPt");
6362

@@ -79,6 +78,87 @@ START_TEST(signed_stream_with_fh)
7978
}
8079
END_TEST
8180

81+
START_TEST(signed_stream_in_parts)
82+
{
83+
test_stream_t *list =
84+
create_signed_stream_splitted_bu("ItPtPtItPtPtItPtPtItPtPt", settings_av1[_i]);
85+
test_stream_check_types(list, "ItPtPtItSPtPtItSPtPtItSPtPt");
86+
// ItPtPtItSPtPtItSPtPtItSPtPt
87+
//
88+
// ItPtPtItS ......PP. (valid, 2 pending)
89+
// ItSPtPtItS .......PP. (valid, 2 pending)
90+
// ItSPtPtItS .......PP. (valid, 2 pending)
91+
// 6 pending
92+
// ItSPtPt PP.PPPP (valid, 7 pending)
93+
signed_video_accumulated_validation_t final_validation = {
94+
SV_AUTH_RESULT_OK, false, 27, 20, 7, SV_PUBKEY_VALIDATION_NOT_FEASIBLE, true, 0, 0};
95+
const struct validation_stats expected = {
96+
.valid_gops = 3, .pending_bu = 6, .final_validation = &final_validation};
97+
validate_stream(NULL, list, expected, true);
98+
99+
test_stream_free(list);
100+
}
101+
END_TEST
102+
103+
START_TEST(has_fh_without_tg)
104+
{
105+
// This test runs in a loop with loop index _i, corresponding to struct sv_setting _i in
106+
// |settings_av1|; See signed_video_helpers.h.
107+
108+
struct sv_setting setting = settings_av1[_i];
109+
test_stream_t *list = create_signed_stream("|ZIt|Ptf|Pt|It|Pt|Ptf|It|Pt|Pt|It|Pt", setting);
110+
// Note that 'f' (single FH) turns into a 'P' when reading the BU.
111+
test_stream_check_types(list, "|ZIt|SPtP|Pt|It|SPt|PtP|It|SPt|Pt|It|SPt");
112+
113+
// |ZIt|SPtP|Pt|It|SPt|PtP|It|SPt|Pt|It|SPt
114+
//
115+
// |ZIt|S _.PP_. (valid, 2 pending)
116+
// It|SPtP|Pt|It|S .._...._.._PP_. (valid, 2 pending)
117+
// It|SPt|PtP|It|S .._..._..._PP_. (valid, 2 pending)
118+
// It|SPt|Pt|It|S .._..._.._PP_. (valid, 2 pending)
119+
// 8 pending
120+
// It|SPt PP_.PP (valid, 6 pending)
121+
signed_video_accumulated_validation_t final_validation = {
122+
SV_AUTH_RESULT_OK, false, 40, 34, 6, SV_PUBKEY_VALIDATION_NOT_FEASIBLE, true, 0, 0};
123+
const struct validation_stats expected = {
124+
.valid_gops = 4, .pending_bu = 8, .final_validation = &final_validation};
125+
validate_stream(NULL, list, expected, true);
126+
127+
test_stream_free(list);
128+
}
129+
END_TEST
130+
131+
START_TEST(scrap_first_gop_mixed_with_fh_td_and_obu_metdata)
132+
{
133+
// This test runs in a loop with loop index _i, corresponding to struct sv_setting _i in
134+
// |settings_av1|; See signed_video_helpers.h.
135+
136+
struct sv_setting setting = settings_av1[_i];
137+
test_stream_t *list = create_signed_stream("|ZIt|Ptf|Pt|ZIt|Pt|Ptf|ZIt|Pt|Pt|ZIt|Pt", setting);
138+
// Note that 'f' (single FH) turns into a 'P' when reading the BU.
139+
test_stream_check_types(list, "|ZIt|SPtP|Pt|ZIt|SPt|PtP|ZIt|SPt|Pt|ZIt|SPt");
140+
const int obus_to_remove = 12;
141+
test_stream_t *scrapped_gop = test_stream_pop(list, obus_to_remove);
142+
test_stream_free(scrapped_gop);
143+
test_stream_check_types(list, "|ZIt|SPt|PtP|ZIt|SPt|Pt|ZIt|SPt");
144+
145+
// |ZIt|SPt|PtP|ZIt|SPt|Pt|ZIt|SPt
146+
//
147+
// |ZIt|S _PPP_. (signed, 3 pending)
148+
// ZIt|SPt|PtP|ZIt|S U.._..._..._.PP_. ( valid, 2 pending)
149+
// It|SPt|Pt|ZIt|S .._..._.._.PP_. ( valid, 2 pending)
150+
// 7 pending
151+
// It|SPt PP_.PP ( valid, 6 pending)
152+
signed_video_accumulated_validation_t final_validation = {
153+
SV_AUTH_RESULT_OK, false, 31, 25, 6, SV_PUBKEY_VALIDATION_NOT_FEASIBLE, true, 0, 0};
154+
const struct validation_stats expected = {
155+
.valid_gops = 2, .pending_bu = 7, .has_signature = 1, .final_validation = &final_validation};
156+
validate_stream(NULL, list, expected, true);
157+
158+
test_stream_free(list);
159+
}
160+
END_TEST
161+
82162
static Suite *
83163
signed_video_suite(void)
84164
{
@@ -95,6 +175,9 @@ signed_video_suite(void)
95175

96176
// Add tests
97177
tcase_add_loop_test(tc, signed_stream_with_fh, s, e);
178+
tcase_add_loop_test(tc, signed_stream_in_parts, s, e);
179+
tcase_add_loop_test(tc, has_fh_without_tg, s, e);
180+
tcase_add_loop_test(tc, scrap_first_gop_mixed_with_fh_td_and_obu_metdata, s, e);
98181

99182
// Add test case to suit
100183
suite_add_tcase(suite, tc);

tests/check/test_helpers.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -678,16 +678,20 @@ validate_stream(signed_video_t *sv,
678678
break;
679679
}
680680
public_key_has_changed |= latest->public_key_has_changed;
681-
has_timestamp |= latest->has_timestamp;
682681

683682
if (latest->has_timestamp) {
684683
if (sv->onvif || sv->legacy_sv) {
685684
// Media Signing and Legacy code only have one timestamp
686685
ck_assert_int_eq(latest->start_timestamp, latest->end_timestamp);
687686
} else {
688-
ck_assert_int_lt(latest->start_timestamp, latest->end_timestamp);
687+
if (has_timestamp) {
688+
ck_assert_int_lt(latest->start_timestamp, latest->end_timestamp);
689+
} else {
690+
ck_assert_int_le(latest->start_timestamp, latest->end_timestamp);
691+
}
689692
}
690693
}
694+
has_timestamp |= latest->has_timestamp;
691695

692696
// Check if product_info has been received and set correctly.
693697
if ((latest->authenticity != SV_AUTH_RESULT_NOT_SIGNED) &&

0 commit comments

Comments
 (0)