Repository navigation
fix: fail closed on malformed JSON, never publish altered text, and enforce DID Core id/URI syntax (#145) - #149
Conversation
…#145) A lone UTF-16 surrogate escape in a JSON member name, at the did:webvh log-entry boundary and at DidDocumentSerializer's consumption boundary, at the root and nested. Against the pre-fix source, 14 webvh tests fail (notFound / raw InvalidOperationException) and 16 Core tests fail (raw InvalidOperationException). Value-position controls and the witness-file pins pass on both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntent error (#145) JsonDocument.Parse with AllowDuplicateProperties = false decodes every member name to compare it, so a lone surrogate in a NAME throws InvalidOperationException from Parse itself. LogEntrySerializer caught only JsonException, so a fetched did:webvh log resolved as notFound instead of invalidDidLog and Update/Deactivate leaked a raw InvalidOperationException instead of FormatException. DidDocumentSerializer.Deserialize leaked the same exception instead of JsonException. Both catch sites now map it with fixed text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CHANGELOG [Unreleased] Fixed entry, PRD parse-boundary text and DidDocumentSerializer consumption contract, <exception> docs on the public Deserialize overloads (JsonException for malformed JSON, InvalidDidException for an invalid DID string), a lessons.md entry, and the issue plan file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ates, ill-formed UTF-8 (#145) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l review (#145) - did:webvh: DeserializeEntry decodes every string value once, so a lone surrogate in a value the typed model never reads (e.g. an unknown publicKeyJwk member) fails at the parse boundary as invalidDidLog instead of surfacing during hashing as notFound. - Core: Deserialize(string) maps a raw lone surrogate char (ArgumentException from UTF-8 transcoding) to JsonException; Deserialize(ReadOnlySpan<byte>) requires strict UTF-8, so an ill-formed nested extension name can no longer slip past the raw-byte duplicate check. - DataIntegrityProofValue.Parse maps the raw-char ArgumentException to its documented JsonException. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orm first (#145) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
did:ethr RPC result member access (lone-surrogate name/value, duplicate name), did:webvh witness authoring with non-JCS proofs, Core malformed publicKeyJwk and undecodable extension values, and did:peer:4 documents that resolve but cannot be serialized. Against 14205c1: ethr 10 failed, Core 8 failed, webvh 9 failed, peer 2 failed; positive controls and the resolution-parser guard pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mInteractionException (#145) GetRpcObject now builds the JsonObject member dictionary once, so a duplicate or lone-surrogate member name fails there instead of escaping from the first indexer as ArgumentException/InvalidOperationException. GetRpcString maps the lone-surrogate decode failure. A malformed finalized-block response therefore disables event-history caching as documented instead of failing resolution as internalError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oring path (#145) A proof outside the JCS data model (lone surrogate, 1e400) made MergeWitnessProofs throw NetCid.JcsFormatException out of Create, Update and Deactivate. WitnessProofs and CurrentWitnessContent are now checked at their trust boundaries and rejected with ArgumentException naming the parameter. Resolution still treats such a proof as one invalid proof. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nsumption (#145) DidDocumentSerializer.Deserialize now maps IdentityModel's ArgumentException (IDX10805) for a malformed publicKeyJwk to JsonException, and decodes every string value once in the duplicate-rejecting pre-parse. A lone surrogate in an extension value, VM or service extension, or serviceEndpoint map no longer deserializes into a document that cannot be serialized (for example a did:peer:4 long form now fails closed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oring, Core JWK/values) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rrogates (#145) Core: DID Core-forbidden shapes (null/missing type and id, null array entries, blank or nested serviceEndpoint, bad alsoKnownAs/relationship entries) and unpaired surrogates in every string Serialize writes. did:webvh: state shapes that crashed the model, authoring with lone surrogates (services, NewDocument, watchers, pre-rotation commitments, witness proofs), and null witness inputs. did:peer: numalgo 2/4 Create with lone surrogates and numalgo 4 shapes. did:ethr: lone-surrogate service endpoints and node text echoed in receipt messages. Against a305a77: Core 30 failed, webvh 12, peer 3, ethr 4. Positive controls and already-mapped shapes pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… write (#145) Read: VM and service id/type must be strings, array entries must be objects/strings as DID Core requires, serviceEndpoint is required and must be a non-blank string, a map, or a non-empty set of strings and maps (no nested sets). Any remaining JSON-access or model-construction failure at the three Deserialize entry points maps to JsonException, so no raw KeyNotFoundException/InvalidOperationException/ArgumentException escapes and no accepted document crashes Serialize. Write: every caller string the converters write is checked for well-formed UTF-16 (new internal Utf16Text) and rejected with ArgumentException, instead of Utf8JsonWriter silently publishing U+FFFD. InternalsVisibleTo now includes did:webvh and did:ethr for Utf16Text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… altered (#145) LogEntrySerializer now checks every string it writes (versionId, parameters including watchers/nextKeyHashes/updateKeys, witness ids, modeled proof members) for well-formed UTF-16 and throws ArgumentException, instead of Utf8JsonWriter silently signing and publishing U+FFFD. The witness-proof authoring guard (now RequirePublishableProofs) also rejects null entries/proof lists/proofs/ versionIds and lone surrogates in a versionId or modeled proof member, and observes the cancellation token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er (#145) Numalgo2ServiceEncoder.Encode checks the service id, type, endpoint strings, map keys and extension keys for well-formed UTF-16 and throws ArgumentException, instead of JsonSerializer silently encoding U+FFFD into the DID. Numalgo 4 Create is covered by the Core serializer check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ode text in receipt errors (#145) A service endpoint with an unpaired UTF-16 surrogate is rejected with ArgumentException before any RPC traffic, instead of Encoding.UTF8 silently writing (or revoking) EF BF BD on-chain. Receipt status and mismatched-hash errors no longer copy node-supplied text (unbounded, CR/LF allowed) into exception messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an file Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moisesja
left a comment
There was a problem hiding this comment.
I would not approve this yet. Two reproducible P2 concerns remain: the new read-side endpoint restriction lets did:peer:4 Create succeed with a DID that immediately resolves as notFound, and the writer hardening still silently replaces malformed text inside caller-supplied JsonElements. Details and requested regression tests are inline.
The 20 commit subjects are clear. The 23-file scope is broad but coherent for this shared boundary defect; the main concern is incomplete coverage of the new guarantees, not arbitrary scope. Independent review found no additional blocker in the ethr or webvh changes.
Validation at e20ec80: Release build 0 warnings/errors; default suite 2,015 passed, 7 opt-in EVM tests skipped (Core 533, Key 52, Peer 56, WebVh 598, Ethr 506, DI 26, W3C 233, integration gate 11). Separate real-EVM run: 18/18 passed. All five offline samples passed; diff whitespace check clean; GitHub CI green. Those results do not cover the two failing targeted scenarios below.
…sarial pass (#145) Nested serviceEndpoint sets at the model/authoring boundary (with the reviewer's did:peer:4 Create/Resolve scenario and flat mixed-set controls), caller JsonElement contents with ill-formed UTF-8 or escaped lone surrogates at every write position (Core, did:peer:2, did:webvh), non-string/non-JsonElement @context entries, reserved service extension keys and did:peer:2 key collisions, strict UTF-8 on did:peer:2/4 resolution, and a null did:ethr service endpoint. Against e20ec80: Core 22 failed, peer 11, webvh 1, ethr 1; controls pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ServiceEndpointValue.FromSet copies its entries once and rejects null and nested-set entries (W3C DID Core §5.4), so no producer can build the shape the reader rejects. did:peer:4 Create could previously return a DID that immediately resolved as notFound. - Every caller JsonElement the serializer writes (document, VM and service extensions, @context elements, endpoint map values) is decoded first via the shared Utf16Text.RequireDecodable; ill-formed UTF-8 or an escaped lone surrogate is ArgumentException instead of being written as U+FFFD (or as JsonException). The read side now uses the same helper. - @context entries must be strings or JsonElements (ToString() replaced lone surrogates before any check and wrote objects as strings). - A reserved name in Service.AdditionalProperties is ArgumentException, matching the document-level #121 rule (it emitted a duplicate member). Issue136_DereferenceAsync_Path_EmptiedUriSetFailsClosed relied on mutating the caller's list after FromSet to build an empty set. That state is now unconstructible, so the test pins the stronger invariant (clearing the caller's list leaves the validated endpoint unchanged). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-entry boundary (#145) Replaces the private RequireDecodableStrings copy; behavior is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and, DID-scoped fragment lookup (#145) IsAbsoluteUri now follows scheme ":" hier-part, so "[" and "]" are accepted only around an IP-literal host. The bare-id shorthand is one segment without ':', '/', '?' or '#', so real relative references ("/files", "//host/x", "?service=x") are no longer read as "#value". Fragment dereferencing compares ids through ResolveRelativeId, so a VM or service id belonging to another DID no longer matches by fragment alone. A new internal ValidateForProduction runs Serialize's producer checks for methods that build a document without serializing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#145) The local duplicate-id check compared raw ids, so a bare "service" beside the auto-generated "#service" slipped through, and id and serviceEndpoint syntax were never checked. Create and Resolve now call DidDocumentSerializer.ValidateForProduction, which resolves ids before comparing and applies the DID Core id/URI rules; Resolve maps a violation to invalidDid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t before signing (#145) The did.json projection only recognised overrides in did:webvh form, so "did:web:example.com#whois" got a second implicit #whois and the document had duplicate service ids. Materialize now also matches the projection DID's form. Create and Update build did.json before signing, so an invalid projection fails before any signature is produced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eads them; refuse unresolvable did/svc writes (#145) Resolution keeps only did/svc/ names in the reference grammar (^did/svc/(\w+)(/\w+)?(/\w+)?$, typed by the first segment) while still counting every did/svc/ event, and rebuilds JSON values with JSON.parse's last-wins duplicate semantics instead of dropping them. AddServices rejects a ServiceType that is not one [A-Za-z0-9_]+ segment, and AddAttributes rejects a did/svc/ name or value the resolver would drop. BrickProbeTests now publish their junk endpoints with a raw setAttribute, as another ERC-1056 tool could, since NetDid no longer writes them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n absolute URIs (#145) The did.json collision tests now count signer calls on Create and Update: with the pre-fix order each signed once and then threw (proven by reverting DidWebVhMethod.cs to 9633c6b). The IsAbsoluteUri theory gains "http://host:abc", which the path branch accepted after the authority branch failed; it fails against ab17f1a. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RFC 3986 hier-part is "//" authority path-abempty, or a path that cannot start with "//". When the authority branch failed (for example a non-numeric port), the path branch still accepted the whole "//host:abc" remainder, so "http://host:abc" passed as an absolute URI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Id comparison cost in duplicate detection and dereferencing must follow the ids, not the DID (a did:peer DID embeds its document); IP-literals are IPv6address or IPvFuture only; did:peer:4 rewrites the template's placeholder DID in ids as it does in references, at a DID boundary; did:ethr filters events whose bytes32 label is not strict UTF-8 out of counting and versionId, as ethr-did-resolver does; prefix checks on untrusted text (did/pub, did/svc, did:key:, EcdsaSecp256k1) are ordinal. 16 defect rows fail against 22f78f9; the controls pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e prefix (#145) Duplicate detection and dereferencing compared ids by building "{did}#x" per element, so a long did:peer DID (which embeds its document) cost services x DID length: 7 GB for one hostile did:peer:2. ComparableId now maps the document's own ids ("#x", "x", "{did}#x") to "#x" without touching the DID, and the dereferencer builds its target once. The relationship branch no longer re-searches every VM per entry. An IP-literal must be an IPv6address or IPvFuture. Context selection compares "EcdsaSecp256k1" ordinally (ICU ignores NUL). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t a DID boundary (#145) A template naming a placeholder DID had its references rewritten to the did:peer:4 DID but its VM and service ids left as-is, so its own authentication reference no longer dereferenced. Ids are now rewritten too, and both rewrites require a DID boundary ("did:example:abcd" is not "did:example:abc" + "d") and compare ordinally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#145) The check was culture-sensitive and ICU ignores NUL, so "d\0id:key:z..." passed and yielded a shifted, bogus key instead of null. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s ordinally, like ethr-did-resolver (#145) bytes32 names and delegate types are decoded as strict UTF-8 like the reference bytes32toString. An event whose label is not UTF-8 becomes a FilteredEvent: it still links the history walk, but is not counted and sets no versionId/updated/nextVersionId. NetDid used to decode labels as ASCII ('?') and count them. did/pub and did/svc prefixes are ordinal (ICU ignored NUL, so "d\0id/svc/x" was counted). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CHANGELOG and PRD describe the RFC 3986 hier-part and IP-literal grammar, the narrower bare shorthand, DID-scoped and linear-cost id comparison, did:peer:2 producer checks, did:peer:4 placeholder-DID rewriting, did.json overrides built before signing, and did:ethr service names, last-wins JSON members, write guards, strict-UTF-8 labels with FilteredEvent, and ordinal prefixes. The plan file gains a Review section; lessons gain four entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Final pass on the id/URI round: all findings fixed (
|
| # | Severity | Defect | Fix |
|---|---|---|---|
| A1 | High | Duplicate detection built <did>#x per service. A did:peer DID embeds its document, so one hostile 446 KB did:peer:2 allocated 7 GB, and a ~1 MB one killed the process. (Introduced earlier in this PR; never released.) |
DidParser.ComparableId compares own ids as #x, so cost follows the ids. Pinned by an allocation bound. |
| A2 | Medium | The dereferencer rebuilt <did>#x on every comparison (14 GB measured). The relationship branch searched every VM once per entry. |
Target built once; VM search hoisted. |
| A3 | Low–Med | did:ethr decoded bytes32 labels as ASCII and counted events whose label isn't UTF-8. ethr-did-resolver filters those. The did/pub and did/svc prefix checks were culture-sensitive (ICU ignores NUL). |
Strict-UTF-8 labels. Undecodable events become FilteredEvent: they link the history walk but are not counted and set no versionId/updated/nextVersionId. Prefix checks are ordinal. End-to-end test with real transactions on the emulator. |
| A4 | Low | did:peer:4 rewrote a template's placeholder DID in references but not in ids, so its own authentication reference stopped dereferencing. The reference rewrite had no DID boundary. |
Ids are rewritten too, only at a DID boundary. |
| A5 | Low | The IP-literal grammar accepted [foo] and [:]. |
RFC 3986 IPv6address / IPvFuture. Timed linear on 100K-char hostile inputs. |
| — | Low | Same class as A3: culture-sensitive did:key: check on a webvh proof verificationMethod (returned a bogus key, never a match), and on the EcdsaSecp256k1 prefix used for context selection. |
Ordinal. |
Public API note (did:ethr):
Erc1056EventParser.Parsecan now return the newFilteredEvent.AbiDecoder.DecodeDelegateChangedDataandDecodeAttributeChangedDatareturn the strict-UTF-8 label, or null when it isn't valid UTF-8.AbiDecoder.TryDecodeBytes32AsUtf8is new.
Verification
- Release build: 0 warnings, 0 errors.
- Tests:
- Core 706, Ethr 529, WebVh 604, Peer 98, Key 54, DI 26.
- W3C conformance 233 (100%).
- Real-EVM 18/18 with
NETDID_ETHR_INTEGRATION=1.
- All five samples exit 0, and
git diff --checkis clean. - DIF did:webvh vector replay: identical to the
2daf183baseline. - Docs: the CHANGELOG
[Unreleased]did:webvh: an unpaired-surrogate escape in a JSON member name resolves notFound instead of invalidDidLog #145 entry, the PRD (id/URI syntax, did:peer:4 contextualization, did:ethr label decoding), and the Review section intasks/todo20261028-issue145.md.
The PR is ready for your review; I have not merged it.
🤖 Generated with Claude Code
The reviewer's record of the first review of PR #149 at e20ec80: the two confirmed findings (nested serviceEndpoint sets on the authoring path, unchecked JsonElement contents in writers), both fixed on this branch, and the verification run at that head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moisesja
left a comment
There was a problem hiding this comment.
Re-reviewed at 0e8d868. Both original findings are fixed: nested endpoint sets are rejected and privately snapshotted, and malformed caller JsonElements now throw ArgumentException instead of publishing replacement text. I reran the original probes.
I still would not approve this revision. The expanded changes introduce one conformance regression and leave the new peer:2 producer validation bypassable. Both are reproduced through public APIs; the two inline comments give the exact triggers and requested tests.
Validation: Release build 0 warnings/errors; 2,261 default tests passed, with 7 opt-in EVM cases skipped (Core 706, Key 54, Peer 98, WebVh 604, Ethr 529, DI 26, W3C 233, integration gate 11). Separate real-EVM run 18/18; all five offline samples exit 0; CI run 40754984649 succeeded. Independent review found no additional actionable issue in the new ethr/webvh changes.
The PR has grown to 62 commits and 44 files. Commit subjects remain specific, and CHANGELOG/PRD explain the expansion, but please update the PR body: it still describes the initial scope and test counts, omitting the new URI/parser, identifier-rewriting and ethr service-decoding changes. Minor hygiene: git diff origin/main...HEAD --check also reports new blank lines at EOF in the Core Issue145 test file and DidKeyMethodTests.cs.
DID Core 3.2.2 relative DID URLs (path, query and fragment references) must be read and written, and compared by RFC 3986 reference resolution against the DID, also in DefaultVerificationRelationshipResolver, at linear cost. did:peer:2 and did:peer:4 Create must return the document their DID encodes, even when a caller list reads differently twice (the reviewer's split enumerator/indexer repro). 26 defect rows fail against 0e8d868; the controls and the retained negatives pass. Also drops two blank lines at EOF reported by the branch-wide diff check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… RFC 3986 resolution (#145) Relative DID URLs "MAY contain relative path components, query parameters, and fragment identifiers" and are resolved by RFC 3986 §5 against the DID (authority = method:method-specific-id). The new identifier validation accepted only absolute DID URLs, "#fragment" and the bare shorthand, so "/keys/1", "keys/1#k" and "?versionId=1#key-1" were rejected on read and write. IsValidDidReference now accepts the RFC 3986 relative-ref forms except network-path (which replaces the DID itself), and ComparableId resolves references against the DID (merge, remove_dot_segments), so "keys/1#k", "/keys/1#k" and "<did>/keys/1#k" are one id. The bare shorthand "x" keeps meaning "#x" (issue #136). DefaultVerificationRelationshipResolver drops its own normalization, which read "/keys/1" as "#/keys/1" and cost entries x DID length, for the shared one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
did:peer:2 Create encoded options.Services by enumeration but built and validated its document through the indexer (and enumerated Keys twice), so a caller list whose reads disagree minted a DID resolving to invalidDid while Create reported a valid document. Create now reads each list once, encodes once, and returns that DID resolved, which also runs the producer checks; failures throw ArgumentException. did:peer:4 Create builds its document from the JSON it hashed instead of re-reading the caller's InputDocument. BuildDocument and GenerateServiceId are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
?service= selects by fragment ("agent/v1" is "#agent/v1", never a
path); a DID URL with a path and a fragment names "<did>/path#fragment",
not "#fragment"; an id that is no URI reference never matches a valid
one; did:peer:2 Create rejects a key string containing the segment
separator, follows the DIF spec abbreviations (recursive over key
names, DIDCommMessaging<->dm for the type value only; the spec's own
example DID is reproduced and resolved), refuses names it cannot round
trip, preserves explicit relative service ids and rejects absolute ones
("service id attributes MUST be relative"). The old auto-id test now
uses services without ids. 20 defect rows fail against 3e70eb0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…145) The ("#service", "svc") row relied on "svc" being dropped and replaced by a generated "#service". The DIF spec keeps an explicit relative id and generates one only when the id is not set, so the collisions are an explicit "#service" beside a service without id, and the shorthand "svc" beside "#svc". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erenced resource, invalid ids never match (#145) The service parameter names a service by fragment ("agent/v1" selects "#agent/v1"); routing it through RFC 3986 resolution made it "/agent/v1" and lost the service. A DID URL with a path and a fragment now selects "<did>/path#fragment" instead of ignoring the path, so "<did>/k#x" can no longer return the method "#x" from another relationship. An id that is no URI reference (a network-path "//host#k", "k 1") gets a form that equals no valid id, so it cannot match "<did>//host#k". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es; key strings cannot add segments (#145) Per the DIF spec, member names are abbreviated recursively (type, serviceEndpoint, routingKeys, accept -> t, s, r, a, also inside "s") and only the type value DIDCommMessaging becomes "dm". NetDid used one table for names and values and no recursion, so it could not produce the spec's example DID, resolved it with "a"/"r" endpoint keys, read a type "t" back as "type" and a member "dm" as "DIDCommMessaging". Names or the type "dm" that cannot round trip are now refused. Explicit relative service ids are kept ("service id attributes MUST be relative"); absolute ids are rejected on Create and Resolve instead of being silently replaced. A key MultibasePublicKey containing '.' (the segment separator) is rejected instead of injecting keys or services. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CHANGELOG and PRD: relative DID URLs (DID Core 3.2.2) accepted and compared by RFC 3986 resolution with the bare-shorthand exception; ?service= by fragment; path+fragment dereferencing; did:peer Create returns what its DID encodes; did:peer:2 DIF abbreviation and service id rules. Corrects the earlier claim that "/files", "?service=x" and "a/b" ids are rejected. Lessons gain three entries; net-did-verify runs the branch-wide diff check; the plan file gains Review round 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 2: addressed (
|
moisesja
left a comment
There was a problem hiding this comment.
Both review-round-2 findings are fixed and verified at 6f05f34. The fixes were already on the branch when this implementation pass began; I validated them rather than duplicate them.
- Relative path/query DID URLs now pass the original public read/write probes, with shared RFC 3986 comparison and negative controls retained.
- peer:2 validates and returns the document decoded from its exact encoded DID. The original split-indexer/enumerator regression now rejects the invalid input; normal Create/Resolve equality controls pass. peer:4 uses its encoded JSON too.
No remaining blocker found in one independent adversarial pass over these fixes. The two original round-1 findings remain fixed as well. I am resolving my four review threads.
Fresh verification: Release build 0 warnings/errors; 2,319 default tests passed (7 opt-in EVM cases skipped), separate real-EVM suite 18/18, W3C 233/233, all five offline samples exit 0, both diff checks clean. CI 40762475189 passed. Ready for merge from my review perspective; not merged.
Fixes #145.
Problem
NetDid rejects duplicate JSON members, which makes
System.Text.Jsondecode every member name while parsing. A lone surrogate escape in a name ("\uD800":1) therefore throwsInvalidOperationExceptionfromJsonDocument.Parse, and two boundaries caught onlyJsonException:notFoundinstead ofinvalidDidLog;DidDocumentSerializer.Deserializeleaked a raw exception.Later rounds found more defects of the same kind: hostile input escaping a trust boundary as a raw exception, or being accepted and failing later. They also found the write-side mirror image (text silently altered on publish) and gaps against the DID Core id and URI rules. All of it is fixed here:
The full history is in
tasks/todo20261028-issue145.md; the user-facing summary is the #145 entry inCHANGELOG.md.Changes
Read side: every boundary reports its documented error
did:webvh logs: an undecodable string anywhere in an entry is
invalidDidLog.UpdateAsyncandDeactivateAsyncthrow their documentedFormatException.Core
DeserializethrowsJsonExceptionfor:publicKeyJwk, or one withoutkty;serviceEndpoint.A closing net maps any other JSON-access failure, so every document that is read can be written again.
did:peer: service blocks are consumed under the same rules as a document (duplicate members, reserved or colliding keys, depth). The embedded JSON is decoded as strict UTF-8.
did:peer:2 follows the DIF abbreviation rules: member names are abbreviated at every depth (also inside
serviceEndpoint), and only the type valueDIDCommMessagingbecomesdm. The spec's example DID is produced byte for byte and resolved withaccept/routingKeys. Explicit relative service ids are kept ("service id attributes MUST be relative"), and absolute ones are rejected.did:ethr:
EthereumInteractionException, and receipt errors never echo node text.JSON.parsekeeping the last of a duplicate member, raw-string fallback, and its name grammar.Write side: never publish altered or unreadable output
JsonElements, and throwsArgumentExceptioninstead of writing U+FFFD. This covers CoreSerialize, did:webvh state,did.json, parameters and witness proofs, did:peer:2/4 Create, and did:ethr endpoints.ServiceEndpointValue.FromSetenforces DID Core §5.4 (no null entries, no nested sets) and snapshots its entries.JsonSerializerOptionsnow produce a real DID document; NetDid's converters always come first.did.jsonbefore signing, so an invalid projection never costs a signature.DID Core id and URI syntax (§3.2, §5.1.3, §5.2, §5.4)
\zanchoring, no raw whitespace, controls, non-ASCII or format characters, and no malformed%escapes.#key-1,/keys/1,keys/1#k,?versionId=1#key-1; network-path//hostis rejected);serviceEndpointstrings andalsoKnownAsentries (absolute URIs: RFC 3986 hier-part, IPv6/IPvFuture-only IP-literals).keys/1#k,/keys/1#kand<did>/keys/1#kare one id. The one documented exception is NetDid's bare shorthand ([High] did:webvh implicit#files/#whoisservices are neither materialized in the resolved document nor resolvable as DID URLs #136): one plain segmentfilesmeans#files.DefaultVerificationRelationshipResolver. Its cost is linear even for a megabyte-long did:peer DID, and an id of another DID never matches by fragment alone.<did>/path#fragmentfor a DID URL with a path and a fragment (it used to ignore the path), and?service=selects by fragment.UpdateAsyncrefuses service values and names ethr-did-resolver would drop. Prefix checks on untrusted text are ordinal (ICU ignores NUL).Behavior changes
All of these apply to malformed, hostile, or non-conformant input only:
invalidDidLog,invalidDid,JsonException,FormatException,EthereumInteractionException,ArgumentException) replacenotFoundand raw exceptions.serviceEndpointsets cannot be built.t,s,r,a), the typedm, an absolute service id, and a key string containing.. It keeps explicit relative service ids instead of replacing them with#service.kty.JsonSerializerOptionsare locked on first use.Erc1056EventParser.Parsecan return the newFilteredEvent.AbiDecoder.DecodeDelegateChangedDataandDecodeAttributeChangedDatareturn the strict-UTF-8 label, or null when it is not valid UTF-8.AbiDecoder.TryDecodeBytes32AsUtf8is new.NetDidVersionis not bumped; the changes are listed under[Unreleased].Verification
Fail-first: every defect test failed against the pre-fix code of its round, by red-commit or by mutation of the committed fix. Positive controls pass.
Build: Release, 0 warnings / 0 errors.
Tests (vs
main):mainReal-EVM suite: 18/18 with
NETDID_ETHR_INTEGRATION=1.Samples: all five exit 0.
Interop: the DIF did:webvh vector replay is identical before and after the id/URI round.
Hygiene:
git diff origin/main...HEAD --checkandgit diff --checkare clean.Note: unrequested "update by push" events moved this branch three times (10:07, 10:37, 10:38). Each pushed SHA was one of my local commits, and
mainand tags were untouched. The cause is not established; see the plan file.🤖 Generated with Claude Code