FEP-8b32: Object Integrity Proofs

Well, unless it has to do Linked Data Signatures validation, Mastodon is currently optimistic about the shorthands matching its own context, but that is not correct behavior and this is already an issue with Bookwyrm which use the same quote shorthand for a different property than Mastodon, causing compatibility issues. This is something systematic JSON-LD processing would fix, but it would render embedded signatures useless, as the underlying JSON would be changed by JSON-LD processing (either compaction or expansion). In themselves, JCS embedded signed objects would not break anything, but they would be basically unverifiable and just be a waste in the payload.

My understanding is that Fedify currently only supports verifying top-level FEP-8b32 signatures, by verifying them before JSON-LD processing, and does not support embedded signed objects yet.

I think they can be verified deepest-first, which does require walking the entire JSON document and verifying proofs in reverse depth order.

The question is if outer proofs include the inner proofs, or if the proofs are simply “included” (@included) alongside the data. Generally it makes more sense to do the former, although nested proofs has the same complexities as nested contexts – it’s way easier to do this with a document boundary rather than compound documents, because then you don’t have to deal with any cascades.

I think RDFC has the same issues because it’s not immediately apparent if the signature input includes the entire graph or if it includes a subgraph. You have to understand if a property is semantically defined as @container: @graph or not. The RDFC proof is scoped to the immediate parent graph. (In Activity Streams 2.0’s normative context, none of the properties are currently defined as graph containers, so proofs typically apply to the entire AS2 document.)

With JCS proofs, the scoping issue is in the reverse direction; it’s about whether the proof applies only to the current JSON node or to subnodes as well. In other words, using compound documents is unexpected and discouraged.

For more on proof graphs, see https://www.w3.org/TR/vc-data-integrity/#proof-graphs

I think the intended solution might be the previousProof property: https://www.w3.org/TR/vc-data-integrity/#previous-proofs

also tangentially i noticed this part of the FEP:

The proof type SHOULD be DataIntegrityProof, as specified in section 3.1 DataIntegrityProof

@silverpill actually the type MUST be DataIntegrityProof per current spec (TR from May 2025). relatedly, the data-integrity/v2 @context definition scopes all properties to this type, so none of the proof properties mean anything unless this type is present

per https://w3id.org/security/data-integrity/v2

{
  "@context": {
    "DataIntegrityProof": {
      "@id": "https://w3id.org/security#DataIntegrityProof",
      "@context": {...}
    }
  }
}

this allows using more generic terms like “created”, “expires”, “challenge”, “domain”, and “nonce” in other parts of the document without conflicts.

1 Like

I don’t think so? This is about having multiple proofs for the same document, but having one of the proofs only be valid if another one is valid too. What we are currently talking about is having a proof on a sub-graph and possibly no second proof. I see nothing in the Verifiable Credential Data Integrity spec addressing sub-graphs, only full documents (excluding proof graphs, but what we want to omit from the inner proof is not a proof graph). But that seems to imply RDFC would have the same issue indeed.

I agree, it should be changed to MUST. I used other types for custom cryptosuites while the Data Integrity spec was still a draft, but today any custom cryptosuite can be specified using the cryptosuite property.

Scoping to subgraphs or subdocuments

It sounds like per https://github.com/w3c/vc-di-eddsa/issues/117#issuecomment-4904520279 that:

  • Proofs are scoped to the default graph or equivalently the current document minus the proof(s);
  • There may be a vague intent to scope them to the current JSON node/object;
  • Generalized RDF proofs or verification are out of scope of the VCDI work

And as in https://github.com/w3c/vc-di-eddsa/issues/117#issuecomment-4907261944 this implies to me a possibility of a sort of “container” approach if it is to be resolved cleanly.

Compound documents

But per https://github.com/w3c/vc-data-integrity/issues/350 this is indeed not clarified. There’s no consideration for compound documents (merging by id). I think the cleanest that can currently be done is to store the entire object and its proof as a JSON document, and dynamically resolve references to it as needed. So instead of this:

{
  "id": "foo",
  "type": "Activity",
  "object": {
    "id": "bar",
    "type": "Object",
    "proof": {...}
  },
  "proof": {...}
}

It would have to be decomposed as at least this, somehow:

[
{
  "id": "foo",
  "type": "Activity",
  "object": "bar",
  "proof": {...}
},
{
  "id": "bar",
  "type": "Object",
  "proof": {...}
}
]

Or more practically what we are describing is something like this:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/data-integrity/v2",
    {
      "data": {
        "@id": "https://data.example/",
        "@container": "@graph"
      }
    }
  ],
  "@graph": [
    {
      "type": "_:VerifiableData",
      "data": {
        "id": "https://foo.example/",
        "type": "Activity",
        "object": "https://bar.example/"
      },
      "proof": {
        "type": "DataIntegrityProof"
      }
    },
    {
      "type": "_:VerifiableData",
      "data": {
        "id": "https://bar.example/",
        "type": "Object"
      },
      "proof": {
        "type": "DataIntegrityProof"
      }
    }
  ]
}

Here, we have a graph of container objects, and each container object contains a data graph and a proof graph. The proof graph would be used to verify the data graph (or possibly the _:VerifiableData container object instead).

Stripping all proofs

Within VC 2.0 section 5.13 “Securing Mechanism Specifications”, there’s this bit:

The securing mechanism MUST secure all graphs in the verifiable credential or the verifiable presentation, except for any proof graphs securing the verifiable credential or the verifiable presentation itself.

So it seems like the interpretation is actually that the outer proof should secure this:

{
  "id": "foo",
  "type": "Activity",
  "object": {
    "id": "bar",
    "type": "Object"
  }
}

Instead of this:

{
  "id": "foo",
  "type": "Activity",
  "object": {
    "id": "bar",
    "type": "Object",
    "proof": {...}
  }
}

Conclusions

So in keeping with VCDI maybe the FEP should specify that all instances of proof must be stripped, but scoping of proof is a document extracted from the current JSON node (including the total sum of all applied contexts, plus descendant statements)? That second part is really shaky, but it would be helpful at a JSON subdocument level (though not at an RDF subgraph level). Or JSON-LD @included (similar to JSON:API included) could allow for a more natural separation of which parts of the JSON document are verifiable by which proofs (but not the RDF subgraphs)

{
  "actor": "https://actor.example",
  "type": "Activity",
  "object": "https://object.example/",
  "@included": [
    {"id": "https://actor.example", "type": "Person"},
    {"id": "https://object.example", "type": "Object", "proof": {...}}
  ]
}

This plus having the FEP specify that proof always applies to a document extracted from the current JSON object/node could solve the issue, but it would be specifying something not currently part of VCDI.

Alternatively, the FEP could recommend against compound documents, since that would be in keeping with VCDI’s current state, but that would be disheartening indeed. It’s still the safest answer for now, sadly.

1 Like

If VCDI doesn’t allow compound documents, why did they close this issue: eddsa-jcs-2022 and nested documents · Issue #81 · w3c/vc-di-eddsa · GitHub? :thinking:

Compound secured documents are absolutely necessary - we use them in FEP-ef61, for example. When Create activity is generated, the embedded object is signed first, then activity itself. This works very well in JSON world. If there is a solution for JSON-LD world, but it’s not part of VCDI, let’s describe it in FEP-8b32 in a non-normative section.

My understanding is that the cryptosuite does not specify or include in its scope what to use as the unsecured document. The “remove the proof property” bit actually comes from VCDI, which is itself an implementation of a VC 2.0 “Securing Mechanism”.

So the issue probably got closed as “nothing to do” in scope of the cryptosuite, and I think a more appropriate resolution from msporny would have been to move it from w3c/vc-di-eddsa to w3c/vc-data-integrity. I filed https://github.com/w3c/vc-data-integrity/issues/350 to see if there’s any upstream guidance that can be included in the VCDI spec about how to get the unsecured document in a consistent way. Essentially, if I’m reading the spec correctly, then the “outer proof” needs to sign the inner object but not the inner proof, and the “inner proof” applying only to the inner object is just a convention that extends VCDI. With compound documents, we’d need to specify how to extract a complete “inner” document from the current (compound) document.

Yeah, I think it’s something like this:

Compound documents

  • FEP-8b32 proof MAY be present at any level in an AS2 document.
  • When FEP-8b32 proof is present on a JSON node/map/object, the sub-document is a JSON document containing only that JSON object at the top level. (This is what isn’t defined in VCDI.)
    • @context is the total context array active at that part of the document, so in other words, it inherits any @context declarations from higher levels.
  • When FEP-8b32 proof is present at the top level, the unsecured document is the same as the AS2 document, but with all instances of the proof key removed from the entire document.

Example 1: only a top-level proof

The unsecured document is the current document with proof removed, as currently expected.

Example 2: only an inner proof

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://foo.example/",
  "type": "Activity",
  "object": {
    "@context": "https://another-context.example/",
    "id": "https://bar.example/",
    "type": "Object",
    "proof": {"id": "https://inner-proof.example/", "type": "DataIntegrityProof"}
  }
}

Expected sub-document:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://another-context.example/"
  ],
  "id": "https://bar.example/",
  "type": "Object",
  "proof": {"id": "https://inner-proof.example/", "type": "DataIntegrityProof"}
}

Expected unsecured sub-document:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://another-context.example/"
  ],
  "id": "https://bar.example/",
  "type": "Object"
}

Example 3: outer proof with an inner proof

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://foo.example/",
  "type": "Activity",
  "object": {
    "@context": "https://another-context.example/",
    "id": "https://bar.example/",
    "type": "Object",
    "proof": {"id": "https://inner-proof.example/", "type": "DataIntegrityProof"}
  },
  "proof": {"id": "https://outer-proof.example/", "type": "DataIntegrityProof"}
}

Expected sub-document:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://another-context.example/"
  ],
  "id": "https://bar.example/",
  "type": "Object",
  "proof": {"id": "https://inner-proof.example/", "type": "DataIntegrityProof"}
}

Expected sub-document and unsecured sub-document: same as in Example 2

Expected unsecured document:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "https://foo.example/",
  "type": "Activity",
  "object": {
    "@context": "https://another-context.example/",
    "id": "https://bar.example/",
    "type": "Object"
  }
}

Note that inner-proof was removed even though we are verifying outer-proof. Basically we are not expected to sign other people’s signatures.


Additional note: JSON:API included or JSON-LD @included can make extracting sub-documents easier. Consider this example:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {"included": "@included"}
  ],
  "actor": "https://actor.example",
  "type": "Activity",
  "object": "https://object.example",
  "included": [
    {"id": "https://actor.example", "type": "Person", "proof": {...}},
    {"id": "https://object.example", "type": "Object", "proof": {...}}
  ],
  "proof": {...}
}

(You’d still need to strip all instances of proof.)

Thanks for bringing Fedify into this discussion. I took a closer look at how nested proofs interact with Fedify’s current implementation.

Fedify’s automatic inbox path currently verifies only the top-level activity proof. A nested proof remains part of the outer proof’s JCS input, but it is not independently verified against the nested object’s owner or portable id. This also means that Hollo not having encountered a problem is not evidence that nested verification already works. The nested proof was simply not being verified independently.

On the signing side, Fedify already follows the inner-first, outer-second construction: createProof() removes only the proof on the object currently being secured. If a signed Note is embedded in a Create and the Create is then signed, the outer proof covers the complete signed Note, including its proof. This is the same construction that the Data Integrity editors accepted in w3c/vc-di-eddsa#81, so I think it is the best interpretation for Fedify to follow.

For JSON-LD-aware processing, my current proposal is to require every nested secured object to carry an explicit, complete @context. Fedify could then walk the original JSON document and verify nested proofs from the deepest object outward, before any JSON-LD expansion or compaction. Each verification would remove only the proof belonging to the current object, preserving descendant proofs in the outer proof’s input.

I don’t think Fedify should recursively strip every descendant proof unless FEP-8b32 or VCDI adopts that rule. Keeping the inner proof in the outer proof’s input binds the outer activity to the exact signed assertion it embeds. Replacing the inner proof would invalidate the outer proof, but that seems appropriate for an immutable activity.

I opened fedify-dev/fedify#938 to track the implementation and the open interoperability questions. Since compound-document scoping is still being discussed in w3c/vc-data-integrity#350, we would document this behavior as an interoperability profile and revisit it if the upstream specification settles on a different rule.

I’d be interested in your thoughts, especially on requiring each nested secured object to carry its own complete @context.

I don’t think the linked issue agrees with this. What was described in eddsa issue 81 was the pattern of securing an inner document then an outer document, but NOT what the unsecured document (“input”) should be in either case. The VCDI spec defines an “unsecured document” as one where there are zero proof nodes or proof graphs. It would be a contradiction to have an unsecured document that contained an inner proof. This is what the other linked issue on vc-data-integrity (issue 350) seeks to clarify upstream.

basically, the example document in issue 81 is valid, but the verification of such proofs is ambiguous due to undefined behavior.

jsonld context stacks and is inherited from parent nodes in the same document. you can “reset” the context in some cases by declaring an array with null as one of the elements, which overrides earlier declarations but can break with protected term definitions. the only way you can get clean separation between contexts is to use separate documents, i.e. not compound documents.

this is again what eddsa issue 81 was concluding, that it’s okay to merge documents after verification, but republishing that merged/compound document is not specified. in other words, the following has no issues:

  1. verify document a
  2. verify document b
  3. construct a compound document using a as the base and injecting merged statements about b within an inner node

but the following cannot be done reliably:

  1. verify a and b given the compound document from step 3 above

you must obtain an unambiguous canonical representation of a and b to verify a and b. what you do internally after that doesn’t matter – combine them or don’t combine them, if you wish.

note that even if you are including statements initially asserted by someone else in another document, you are still reasserting them in your own document / default graph. their proof graph remains separate though, and signing signatures is a bad idea – use previousProof if you need to establish a chain of proofs.

Thanks for the correction. I overstated what w3c/vc-di-eddsa#81 establishes. It confirms that securing the Note first, inserting it, and then securing the Create produces a valid result, but it does not define how a verifier should recover the input for each proof from the resulting compound document. I do not read the issue as explicitly limiting the valid case to merging documents after verification either, but I agree that it does not settle the verification scope.

There is a related wrinkle in VCDI: it distinguishes an unsecured data document from an input document, and says that an input document may contain a proof added by a previous process. The eddsa-jcs-2022 verification algorithm also removes securedDocument.proof, rather than explicitly removing every descendant proof. I had read that as removing the proof on the current JSON map only.

That reading still depends on first deciding which map constitutes securedDocument, though. VCDI does not say how to select that boundary inside a compound document. I agree that this is the ambiguity raised by w3c/vc-data-integrity#350, and that #81 alone is not enough to resolve it.

I also take your point about JSON-LD context inheritance. My proposed @context requirement was meant as part of a narrower JCS profile: extract each nested JSON map from the original representation, treat it as an independent secured document, and verify it as such. Giving that map its own complete @context would make the extracted representation self-contained for eddsa-jcs-2022. It would not provide general separation between JSON-LD contexts in the compound document. More importantly, the extraction rule itself would need to be defined by FEP-8b32 rather than inferred from VCDI.

I am less certain that previousProof resolves this particular case, since VCDI defines it for proof chains on a secured document and it does not by itself assign nested proofs to subdocuments. Still, I agree that Fedify should not treat signing over the embedded proof as the established interpretation while that scope remains undefined.

Fedify currently preserves descendant proofs in the outer JCS input, so that remains useful implementation experience to bring to the discussion, but it is only one possible profile. I will adjust the framing of fedify-dev/fedify#938 accordingly.

1 Like