22import io
33import os
44import hashlib
5+ import hmac
56import base64
67import zipfile
78from otdf_python .manifest import (
@@ -334,7 +335,7 @@ def create_tdf(
334335 segment_size = (
335336 getattr (config , "default_segment_size" , None ) or self .SEGMENT_SIZE
336337 )
337- hasher = hashlib . sha256 ()
338+ segment_hashes_raw = []
338339 total = 0
339340 # Write encrypted payload in segments
340341 with writer .payload () as f :
@@ -346,9 +347,14 @@ def create_tdf(
346347 break
347348 encrypted = aesgcm .encrypt (chunk )
348349 f .write (encrypted .as_bytes ())
349- seg_hash = base64 .b64encode (
350- hashlib .sha256 (encrypted .as_bytes ()).digest ()
351- ).decode ()
350+ # Calculate segment hash using GMAC (last 16 bytes of encrypted segment)
351+ # This matches the platform SDK when segmentHashAlg is "GMAC"
352+ encrypted_bytes = encrypted .as_bytes ()
353+ gmac_length = 16 # kGMACPayloadLength from platform SDK
354+ if len (encrypted_bytes ) < gmac_length :
355+ raise ValueError ("Encrypted segment too short for GMAC" )
356+ seg_hash_raw = encrypted_bytes [- gmac_length :] # Take last 16 bytes
357+ seg_hash = base64 .b64encode (seg_hash_raw ).decode ()
352358 segments .append (
353359 ManifestSegment (
354360 hash = seg_hash ,
@@ -360,14 +366,19 @@ def create_tdf(
360366 ), # Changed from encrypted_segment_size to encryptedSegmentSize
361367 )
362368 )
363- hasher .update (encrypted .as_bytes ())
369+ # Collect raw segment hash bytes for root signature calculation
370+ segment_hashes_raw .append (seg_hash_raw )
364371 total += len (chunk )
365372 # Use config fields for policy
366373 policy_json = self ._build_policy_json (config )
367374 # Encode policy as base64 to match Java SDK
368375 policy_b64 = base64 .b64encode (policy_json .encode ()).decode ()
369376
370- root_sig = base64 .b64encode (hasher .digest ()).decode ()
377+ # Calculate root signature: HMAC-SHA256 over concatenated segment hash raw bytes
378+ # This matches the platform SDK approach
379+ aggregate_hash = b"" .join (segment_hashes_raw )
380+ root_sig_raw = hmac .new (key , aggregate_hash , hashlib .sha256 ).digest ()
381+ root_sig = base64 .b64encode (root_sig_raw ).decode ()
371382 integrity_info = ManifestIntegrityInformation (
372383 rootSignature = ManifestRootSignature (
373384 alg = "HS256" , sig = root_sig
@@ -454,7 +465,6 @@ def read_payload(
454465 from otdf_python .aesgcm import AesGcm
455466 from otdf_python .asym_crypto import AsymDecryption
456467 import base64
457- import hashlib
458468
459469 with zipfile .ZipFile (io .BytesIO (tdf_bytes ), "r" ) as z :
460470 manifest_json = z .read ("0.manifest.json" ).decode ()
@@ -485,8 +495,15 @@ def read_payload(
485495 for seg in segments :
486496 enc_len = seg .encryptedSegmentSize # Changed field name
487497 enc_bytes = encrypted_payload [offset : offset + enc_len ]
488- # Integrity check (SHA256 HMAC)
489- seg_hash = base64 .b64encode (hashlib .sha256 (enc_bytes ).digest ()).decode ()
498+ # Integrity check using GMAC (last 16 bytes of encrypted segment)
499+ # This matches how segments are hashed when segmentHashAlg is "GMAC"
500+ gmac_length = 16 # kGMACPayloadLength from platform SDK
501+ if len (enc_bytes ) < gmac_length :
502+ raise ValueError (
503+ "Encrypted segment too short for GMAC verification"
504+ )
505+ seg_hash_raw = enc_bytes [- gmac_length :] # Take last 16 bytes
506+ seg_hash = base64 .b64encode (seg_hash_raw ).decode ()
490507 if seg .hash != seg_hash :
491508 raise ValueError ("Segment signature mismatch" )
492509 iv = enc_bytes [: AesGcm .GCM_NONCE_LENGTH ]
0 commit comments