Summary
Invocation.CapabilityAction defaults to string.Empty and is serialized into
the canonical payload, so an invocation document that omits capabilityAction
is canonicalized with "capabilityAction":"" injected — a field that was never
present on the wire. zcap-py and the JCS reference omit it. The injected field
changes the signing payload, so a signature over an invocation that legitimately
omits capabilityAction will not verify across implementations.
Evidence
ZCAP-LD interop harness, real zcap-py 0.6.0 vs zcap-dotnet @ 8a059d1,
2026-06-16. Fixture invocation-missing-capability-action-edge.
Source document (no capabilityAction):
{"id":"urn:uuid:inv-missing-action","capability":"urn:uuid:cap-missing-action","invocationTarget":"https://resource.example/missing-action"}
Canonical output:
zcap-py → {"capability":"urn:uuid:cap-missing-action","id":...,"invocationTarget":...,"proof":{...}} (omitted)
sha256 9f913062f54c191ccfd7b7a2fb112a8869d505ef0758282596101512341c0eec
zcap-dotnet → {"capability":"urn:uuid:cap-missing-action","capabilityAction":"","id":...,...} (injected)
sha256 a72906592145f2ca7698119830796598feca77b46030651220482de8e4803061
Suggested fix
Make CapabilityAction nullable and [JsonIgnore(WhenWritingNull)] (parallel
to the #37 fix for capability optional fields) so an absent field stays absent
through canonicalization.
Acceptance criteria
invocation-missing-capability-action-edge produces the same canonical bytes
as zcap-py (no capabilityAction key when the source omits it).
Related
Summary
Invocation.CapabilityActiondefaults tostring.Emptyand is serialized intothe canonical payload, so an invocation document that omits
capabilityActionis canonicalized with
"capabilityAction":""injected — a field that was neverpresent on the wire.
zcap-pyand the JCS reference omit it. The injected fieldchanges the signing payload, so a signature over an invocation that legitimately
omits
capabilityActionwill not verify across implementations.Evidence
ZCAP-LD interop harness, real
zcap-py 0.6.0vszcap-dotnet @ 8a059d1,2026-06-16. Fixture
invocation-missing-capability-action-edge.Source document (no
capabilityAction):{"id":"urn:uuid:inv-missing-action","capability":"urn:uuid:cap-missing-action","invocationTarget":"https://resource.example/missing-action"}Canonical output:
zcap-py→{"capability":"urn:uuid:cap-missing-action","id":...,"invocationTarget":...,"proof":{...}}(omitted)sha256
9f913062f54c191ccfd7b7a2fb112a8869d505ef0758282596101512341c0eeczcap-dotnet→{"capability":"urn:uuid:cap-missing-action","capabilityAction":"","id":...,...}(injected)sha256
a72906592145f2ca7698119830796598feca77b46030651220482de8e4803061Suggested fix
Make
CapabilityActionnullable and[JsonIgnore(WhenWritingNull)](parallelto the #37 fix for capability optional fields) so an absent field stays absent
through canonicalization.
Acceptance criteria
invocation-missing-capability-action-edgeproduces the same canonical bytesas
zcap-py(nocapabilityActionkey when the source omits it).Related