Skip to content

[Phase 1] Canonical Request Signing — Deterministic HMAC Across SDKs#33

Open
RachanaB5 wants to merge 3 commits into
c2siorg:mainfrom
RachanaB5:feat/canonical-request-signing
Open

[Phase 1] Canonical Request Signing — Deterministic HMAC Across SDKs#33
RachanaB5 wants to merge 3 commits into
c2siorg:mainfrom
RachanaB5:feat/canonical-request-signing

Conversation

@RachanaB5
Copy link
Copy Markdown
Contributor

@RachanaB5 RachanaB5 commented Apr 4, 2026

Summary

Implements JSON canonicalization before HMAC computation to ensure deterministic signing across the Python SDK and Go sidecar. Previously, semantically identical JSON with different key ordering or whitespace would produce different HMAC signatures, breaking cross-platform verification.

Closes #32 .


Problem

Payloads were signed as raw bytes without canonicalization, causing HMAC verification to fail when the same JSON was serialized differently across SDKs.

Python sends:    {"b":2,"a":1}
Go receives:     {"a":1,"b":2}
Result:          ❌ HMAC mismatch — semantically identical, cryptographically different

Solution

Canonicalize JSON (sorted keys, no whitespace) before signing on both sides, guaranteeing identical HMAC input regardless of serialization order.

Python sends:    {"b":2,"a":1}  →  canonical: {"a":1,"b":2}
Go receives:     {"a":1,"b":2}  →  canonical: {"a":1,"b":2}
Result:          ✅ Identical HMAC

Changes

File Change
sidecar/internal/transport/frame.go SignedMessage() now parses, canonicalizes JSON, returns ([]byte, error)
sidecar/internal/transport/listener.go Handles canonicalization error in handleConn() with structured logging
sidecar/internal/transport/frame_test.go +5 new canonicalization tests
sdk/python/acf/frame.py signed_message() canonicalizes via json.dumps(sort_keys=True, separators=(',', ':'))
sdk/python/tests/test_frame.py +7 new canonicalization tests

Net: +209 lines, -18 lines across 5 files

Go — frame.go

// Before
func SignedMessage(version byte, length uint32, nonce [16]byte, payload []byte) []byte {
    buf := make([]byte, 1+4+16+len(payload))
    buf[0] = version
    binary.BigEndian.PutUint32(buf[1:5], length)
    copy(buf[5:21], nonce[:])
    copy(buf[21:], payload)
    return buf
}

// After
func SignedMessage(version byte, length uint32, nonce [16]byte, payload []byte) ([]byte, error) {
    var obj interface{}
    if err := json.Unmarshal(payload, &obj); err != nil {
        return nil, fmt.Errorf("payload is not valid JSON: %w", err)
    }
    canonical, _ := json.Marshal(obj)

    buf := make([]byte, 1+4+16+len(canonical))
    buf[0] = version
    binary.BigEndian.PutUint32(buf[1:5], uint32(len(canonical)))
    copy(buf[5:21], nonce[:])
    copy(buf[21:], canonical)
    return buf, nil
}

Python — frame.py

# Before
def signed_message(version: int, length: int, nonce: bytes, payload: bytes) -> bytes:
    return struct.pack(">B I 16s", version, length, nonce) + payload

# After
def signed_message(version: int, length: int, nonce: bytes, payload: bytes) -> bytes:
    try:
        obj = json.loads(payload)
    except json.JSONDecodeError as e:
        raise ValueError(f"Payload is not valid JSON: {e}")

    canonical = json.dumps(obj, sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode('utf-8')
    return struct.pack(">B I 16s", version, len(canonical), nonce) + canonical

Test Results

Go 27/27 ✅
Python 23/23 ✅
Race detector 0 races ✅
Module Coverage
Go transport 92.3%
Go crypto 85.7%
Python frame 95%+

Updates

  • Added canonical JSON signing (sorted keys, whitespace normalization)
  • Added cross-SDK interop tests using fixed nonce
  • Added tests for key order, nested JSON, and canonical consistency

Notes

  • Canonical payload is used for signed message construction
  • Verified deterministic behavior using fixed nonce tests

Would appreciate feedback on whether deriving canonical length inside SignedMessage() aligns with the intended design.

@RachanaB5
Copy link
Copy Markdown
Contributor Author

Added additional canonicalization and interop tests based on discussion.
Would appreciate feedback, especially on the SignedMessage() design decision around canonical length handling.

@RachanaB5
Copy link
Copy Markdown
Contributor Author

Updated tests and addressed feedback around assertions, wrapper usage, and Unicode normalization.

Would appreciate maintainer feedback on the overall approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Phase 1] Canonical Request Signing — Deterministic HMAC Across SDKs

1 participant