fix(dpp): refuse encryptedFor paths inside a transient object (PV14) - #4948
Conversation
validate_encrypted_for_declarations compared the recipient, recipientKey and senderKey paths with transient_fields as declared, so a path inside a transient object (transient: ["meta"], recipient: "meta.recipientId") passed while Drive strips the whole object before storage. The check now covers the path and every dotted prefix of it. The prefix check is one pub(crate) is_transient(DocumentTypeRef, path) in document_type/property/mod.rs, the same signature as the private copies in the open lookup-source and listElement PRs, which can switch to it. Generation 3 of try_from_schema and meta-schema v3 are selected only by protocol version 14 (unreleased), so this is an in-place edit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedNext included review available in 20 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository: dashpay/platform/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
📖 Book Preview built successfully. Download the preview from the workflow artifacts. Updated at 2026-09-23T14:10:54.392Z |
…ment Merges anyOf/allOf (#4942) and the transient-object fixes (#4943, #4948), and reworks listElement to the shape Sam proposed: "refersTo": { "type": "listElement", "documentType": "electedCharter", "propertyAgreement": { "electedCharterId": "$id" }, "inList": "members" } - The list's document is found by the agreement pair with `$id` on the referenced side (exactly one, read from a stored identifier property, never $ownerId); `documentProperty`/`list` are gone. `$id` joins $ownerId and $creatorId as a referenced-side agreement name for every document reference. - A list element is a document reference: contractId allowed, `as_any_document_reference` carries it with `in_list`, so registration checks its contract, type and pairs through the shared code; the $id property needs no refersTo of its own. - Write time: the document is fetched by id through a per-write memo shared with every by-id reference (one fetch for the charter and its list elements); lists are collected once into a set. Replace triggers are the agreement's (binds_a_changed_property). - listElement is a combinable leaf of anyOf/allOf (target variant 9). - Tests, fixtures, meta-schema, changelog item 34, book and wasm-dpp2 updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Issue being fixed or feature implemented
encryptedFor(#4919, protocol version 14, unreleased) refuses a transient recipient, recipient key or sender key, because a transient value is stripped before storage and the stored ciphertext would lose its recipe. The check compared the named path againsttransient_fieldsas declared, so a path inside a transient object got through:transient: ["meta"]withrecipient: "meta.recipientId"passed, and Drive then drops the wholemetaobject from the stored document (data.retain(|key, _| !transient_fields.contains(key))in the create transition action). A reader of the stored document could not tell whom the bytes are for or which key decrypts them, which is what the rule exists to guarantee.It has to be fixed before 4.2 ships: the check runs on every parse (try_from_schema generation 3, called unconditionally), so tightening it once protocol version 14 is live would make stored contracts unparseable and would need a version gate.
What was done?
validate_encrypted_for_declarationschecks the path and every dotted prefix of it against the transient set, for the recipient and for both key paths. The message now says "is transient or inside a transient object".pub(crate) fn is_transient(document_type: DocumentTypeRef, path: &str) -> boolindocument_type/property/mod.rs. The same fix forrefersTolookup key sources (#4943) andlistElementreferences (#4940) each carry an identical private copy; neither has merged, so whichever lands after this one swaps its copy foruse super::is_transient;(same signature, no call site changes).encryptedFordescription in meta-schema v3 now states the rule (it did not mentiontransientat all), and the book's Encrypted Properties section adds "or sits inside a transient object".Only DPNS declares
transientamong system contracts and test fixtures, and it has noencryptedFor; no contract in the repository declaresencryptedFor.Before / after
A document type whose recipient and key ids live in a transient object:
{ "type": "object", "properties": { "meta": { "type": "object", "position": 0, "properties": { "recipientId": { "type": "array", "byteArray": true, "minItems": 32, "maxItems": 32, "contentMediaType": "application/x.dash.dpp.identifier", "position": 0 }, "recipientKeyId": { "type": "integer", "minimum": 0, "maximum": 4294967295, "position": 1 }, "senderKeyId": { "type": "integer", "minimum": 0, "maximum": 4294967295, "position": 2 } }, "required": ["recipientId", "recipientKeyId", "senderKeyId"], "additionalProperties": false }, "encryptedMessage": { "type": "array", "byteArray": true, "minItems": 32, "maxItems": 1040, "position": 1, "encryptedFor": { "recipient": "meta.recipientId", "recipientKey": "meta.recipientKeyId", "senderKey": "meta.senderKeyId", "scheme": "ecdh-secp256k1-aes256-cbc" } } }, "required": ["meta", "encryptedMessage"], "transient": ["meta"], "additionalProperties": false }Before: the contract registers. Every stored document keeps
encryptedMessageand losesmeta, so no reader can tell whom the bytes are for or which keys to use.After: the contract is refused with
InvalidContractStructure(10231):(and the same for
recipientKey/senderKeywhen the recipient is outside the object). The same schema without"transient": ["meta"]still registers, as does a top-leveltransiententry that only shares a text prefix with the path (metadoes not covermetadata.recipientId).In-place changes to shipped generations
None. The edited check belongs to try_from_schema generation 3, selected only by
CONTRACT_VERSIONS_V6, which only protocol version 14 (unreleased) uses. Meta-schema v3 is likewise protocol version 14 only, and the change there is adescription.How Has This Been Tested?
New rs-dpp tests in
try_from_schema/mod.rs, next toshould_reject_encrypted_for_naming_a_transient_recipient_or_key:should_reject_encrypted_for_naming_a_recipient_or_key_inside_a_transient_object: recipient, recipientKey and senderKey each pointed into a required object whose leaves are required; the schema parses under full validation withouttransient, and is refused with the new message on both the validating and the non-validating parse once the object is transient.should_find_a_path_transient_through_itself_or_an_enclosing_object_only: the helper on a top-level transient property, the transient object itself, one and two levels inside it, and a look-alike name (metadata.authorIdwithmetatransient) that must not match.Run locally:
All 389 tests matching
try_from_schema,encrypted_for,transientormeta_schemapass; clippy is clean. With the oldcontainscheck restored, the new rejection test fails and the example schema above registers under full validation.Breaking Changes
None for any released protocol version. At protocol version 14 (unreleased), a contract declaring
encryptedForwith a recipient or key path inside a transient object is now refused.Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull requestFor repository code-owners and collaborators only
🤖 Generated with Claude Code