Skip to content
18 changes: 17 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ To be released.

### @fedify/fedify

- Fixed `verifyProof()` so Ed25519 JCS proofs authenticate every received
proof option except `proofValue`, including `expires`, `domain`,
`challenge`, `nonce`, and extension options. It now rejects expired or
malformed proof options, and callers can provide expected `domain` and
`challenge` values through `VerifyProofOptions` to prevent cross-domain or
replay use. [[#968]]

- Added `verifyPortableObjectProof()` to enforce the [FEP-ef61] proof policy
for portable actors, activities, objects, and signed collections. Its
detailed result distinguishes documents outside the policy, unsecured
collections, missing or invalid [FEP-8b32] proofs, unsupported verification
methods, DID authority mismatches, and successful verification. [[#832],
[#968]]

- Updated `verifyObject()` so [FEP-8b32] proofs signed by `did:key`
verification methods can authenticate portable objects whose owner is an
`ap:` or `ap+ef61:` URI with the same [FEP-fe34] cryptographic origin.
Expand Down Expand Up @@ -92,9 +106,9 @@ To be released.
bundles `temporal-polyfill`, while type declarations rely on the standard
`esnext.temporal` lib reference. [[#823], [#925]]

[FEP-ef61]: https://w3id.org/fep/ef61
[FEP-8b32]: https://w3id.org/fep/8b32
[FEP-fe34]: https://w3id.org/fep/fe34
[FEP-ef61]: https://w3id.org/fep/ef61
[ActivityPub Media Upload extension]: https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload
[Standard Schema]: https://standardschema.dev/
[#206]: https://github.com/fedify-dev/fedify/issues/206
Expand All @@ -108,13 +122,15 @@ To be released.
[#823]: https://github.com/fedify-dev/fedify/issues/823
[#827]: https://github.com/fedify-dev/fedify/issues/827
[#829]: https://github.com/fedify-dev/fedify/issues/829
[#832]: https://github.com/fedify-dev/fedify/issues/832
[#915]: https://github.com/fedify-dev/fedify/pull/915
[#923]: https://github.com/fedify-dev/fedify/pull/923
[#925]: https://github.com/fedify-dev/fedify/pull/925
[#926]: https://github.com/fedify-dev/fedify/pull/926
[#927]: https://github.com/fedify-dev/fedify/pull/927
[#930]: https://github.com/fedify-dev/fedify/issues/930
[#934]: https://github.com/fedify-dev/fedify/pull/934
[#968]: https://github.com/fedify-dev/fedify/pull/968

### @fedify/astro

Expand Down
8 changes: 5 additions & 3 deletions docs/manual/opentelemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,9 +524,11 @@ Fedify records the following OpenTelemetry metrics:
`verifyRequest()` / `verifyRequestDetailed()`, `verifyJsonLd()`, and
`verifyProof()` each emit exactly one measurement, even when the
implementation retries internally after a cache mismatch. Wrappers
such as `verifyObject()` emit one measurement per inner `verifyProof()`
call (and none when the object has no proofs); higher-level inbox
handling can perform several verification attempts in series.
such as `verifyObject()` and `verifyPortableObjectProof()` emit one
measurement per inner `verifyProof()` call. They emit none when there
are no proofs or portable proof policy rejects the document before
cryptographic verification; higher-level inbox handling can perform
several verification attempts in series.

Kind-specific optional attributes are recorded only when the value
matches a small, spec-bounded set, to keep cardinality safe even when
Expand Down
45 changes: 45 additions & 0 deletions docs/manual/send.md
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,51 @@ When `verifyObject()` checks whether a proof authenticates an object's actor or
attribution, it treats matching portable `ap:`/`ap+ef61:` IDs and `did:key`
controllers as the same [FEP-fe34] cryptographic origin.

`verifyProof()` authenticates every received proof option except `proofValue`
as part of the Ed25519 JCS signature. It rejects expired proofs and malformed
standard options. When a proof is bound to a security domain or challenge,
pass the expected `domain` or `challenge` through `VerifyProofOptions`; a
missing or different value makes verification fail.

For received portable objects, use `verifyPortableObjectProof()`. It keeps
`verifyProof()` focused on cryptographic verification while also enforcing the
[FEP-ef61] policy: a portable actor, activity, or object must have proofs, every
proof must use a DID URL, and that DID must match the authority of the portable
object ID. The detailed result distinguishes documents outside the policy,
missing or invalid proofs, unsupported verification methods, DID mismatches,
and successful verification:

~~~~ typescript
import { verifyPortableObjectProof } from "@fedify/fedify";

async function verifyPortableResponse(response: Response): Promise<void> {
const jsonLd: unknown = await response.json();
let result;
try {
result = await verifyPortableObjectProof(jsonLd);
} catch (error) {
if (error instanceof TypeError) {
throw new Error("Malformed portable object.", { cause: error });
}
throw error;
}
if (result.verified) {
console.log("Verified with", result.keys);
} else if (result.reason.type === "unsecuredCollection") {
// Apply the gateway trust policy for an unsigned portable collection.
} else if (result.reason.type !== "notPortableObject") {
throw new Error(`Portable proof rejected: ${result.reason.type}`);
}
}
~~~~

A portable collection that has proofs is verified in the same way. A portable
collection without a proof produces the `unsecuredCollection` result so the
caller can apply the separate gateway trust policy allowed by [FEP-ef61].
`verifyPortableObjectProof()` examines only the top-level JSON-LD node. An
embedded portable object needs its own verification; success for an outer
activity does not authenticate portable objects nested inside it.

> [!TIP]
> HTTPS Signatures, Linked Data Signatures, and Object Integrity Proofs can
> coexist in an application and be used together for maximum compatibility.
Expand Down
Loading