Skip to content

feat(rpc): expose sign_canonical for C2PA creator-binding #186

@realmeylisdev

Description

@realmeylisdev

Summary

Divine mobile's C2PA creator-binding flow needs a sign_canonical JSON-RPC
method on the Keycast server so OAuth-signed-in users get a real inline
creator-binding assertion on every Divine post. Today the mobile-side
fallback at KeycastRpc.signCanonicalPayload returns null (no method on
the backend), so NostrCreatorBindingService.createAssertion skips the
binding and the C2PA proof manifest ships without it.

The mobile side is already wired and forward-compatible (mobile PR
divinevideo/divine-mobile#3683):
once this method ships, every Divine OAuth post gets the binding for free
with no further mobile change.

Method contract

Field Value
Method name sign_canonical
Params [base64(payload)] — single base64-encoded byte string
Result hex-encoded schnorr signature (String)
Error standard JSON-RPC error response on unsupported / failure

Determinism contract (REQUIRED)

The signature must be deterministic for a given (privateKey, payload)
pair so repeated signing of the same payload produces the same signature —
this is what keeps creator-binding assertions stable across re-publishes,
re-uploads, and proof-manifest re-evaluations.

The mobile-side local signer at
mobile/lib/services/local_key_signer.dart:55-74
implements this as:

  1. Hash: digest = SHA-256(payload) → 32-byte digest, hex-encoded as a
    lowercase string.
  2. Sign: BIP-340 schnorr signature over the digest using the account's
    private key.
  3. Auxiliary data: 32 zero bytes (constant
    _canonicalPayloadAux = '0' * 64 in hex). BIP-340 randomized aux would
    produce different signatures across calls; that is unacceptable for
    creator-binding stability.

Pseudocode (Rust-ish — adjust to Keycast's stack):

fn sign_canonical(account_private_key: SecretKey, payload_b64: String) -> Result<String> {
    let payload = base64::decode(payload_b64)?;
    let digest: [u8; 32] = sha256(&payload);
    let aux: [u8; 32] = [0; 32]; // MUST be all zeros for determinism
    let sig = schnorr::sign(account_private_key, digest, aux);
    Ok(hex::encode(sig))
}

Test vector

Computed against the Dart bip340 package (which the mobile side uses).
A backend implementation should self-verify by signing the same payload
with the same private key and asserting bit-equal output.

Field Value
payload (UTF-8) divine-creator-binding-test
payload (hex) 646976696e652d63726561746f722d62696e64696e672d74657374
payload (base64) ZGl2aW5lLWNyZWF0b3ItYmluZGluZy10ZXN0
privateKey 5ee1c8000ab28edd64d74a7d951ac2dd559814887b1b9e1ac7c5f89e96125c12
digest (hex) 39341a12a2f77007d6e72841f667523d39463a825d82c6c98981881283fb7ed0
aux (hex) 0000000000000000000000000000000000000000000000000000000000000000
expected signature 9baed2647e5f9d059b68eb03c6e3e6dcdf53cbe94fb143af70fb6e7332ee9997cc7ba5ac9cdb9049a0e47c8c20e2031843e88c59dcba3c3ff8fc34eeae4a565f

If your BIP-340 implementation produces a different signature for these
inputs, the most likely cause is non-zero aux bytes (the default for many
schnorr libraries) — make sure the aux is forced to 32 zero bytes.

Mobile-side wiring (already shipped on the PR branch)

Permalinks pinned to commit
7c6c0f78a
(head of fix/publish-hang-disconnected-relay):

Acceptance

  • sign_canonical accepts the param shape above and returns a hex
    schnorr signature.
  • Signing the same (privateKey, payload) twice produces bit-equal
    output.
  • Cross-implementation signature for the test vector above matches
    9baed2647e5f9d059b68eb03c6e3e6dcdf53cbe94fb143af70fb6e7332ee9997cc7ba5ac9cdb9049a0e47c8c20e2031843e88c59dcba3c3ff8fc34eeae4a565f.
  • Failure modes (unknown method, bad params, internal error) surface
    as standard JSON-RPC error responses; mobile already treats those
    as "skip binding gracefully."

Source

Metadata

Metadata

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions