Skip to content

core: sign_lifecycle inherits created_by_device from the chain head while signing with the current device, so any continuation by another device fails verify_asset #475

Description

@justin13888

What the code does

capsule-core/src/lifecycle/provenance.rs, Workspace::sign_lifecycle, builds every continuation manifest (delete, trash-restore, metadata-update) as:

let core = ManifestCore {
    action,
    prior_provenance_hash: prior,
    retention_until,
    metadata_blob_hash,
    timestamp: now_rfc3339(),
    client_version: self.client_version.clone(),
    ..base.clone()          // <- created_by_user AND created_by_device come from the chain head
};
core.sign(self.device_signer.as_ref(), album.write_tier_signer()?)

So the record is signed by the current device but attributed to the base manifest's device.

Why that does not verify

capsule-core/src/crypto/verify_asset.rs resolves the signature this way:

  1. step 6 — directory.core.user_id != core.created_by_user → Reject(UnknownDevice);
  2. directory.device(&core.created_by_device) → Reject(UnknownDevice) when absent;
  3. step 8 — the device signature must verify under that entry's dsk_public.

design/cryptography/provenance.md states the same rule in words: "device_sig — hybrid Ed25519 + ML-DSA-65 by the uploading device's DSK", and design/web-upload.md:91 has the adopting client set created_by_user/created_by_device to "the adopter (the cryptographic author)", not to the original uploader.

So the pair names the signer of this record. Inheriting it is correct only while the writing device is the creating device:

Case created_by_device signer verify_asset
same device continues creating device same passes
second device of the same account continues creating device other device BadDeviceSig
writer member (S-C51) continues the owner's chain owner's device member's device UnknownDevice (member's device is not in the owner's directory) / BadDeviceSig

The multi-device case is not exotic — it is any user with a phone and a laptop.

Server-side consequence

POST /v1/albums/{album_id}/ops enforces invariant 7 (capsule-server/src/routes/ops.rs): created_by_device must be in the caller's published directory. A manifest built by today's sign_lifecycle from another device therefore also gets 400 error.upload.device_not_authorized — for its own, independent reason. The server refusal and the verify_asset refusal are the same underlying defect seen from two sides.

This is why the writer-member lifecycle path opened by S-C51 (#405, PR #458) has no client that can produce a manifest anything accepts. The server side is landed and documented; the client side is this.

What a fix has to decide

Which of the two readings is normative, and then make one place enforce it:

  1. The signer is the author (what verify_asset, provenance.md and web-upload.md all say): sign_lifecycle should set created_by_user/created_by_device from self.account / self.device_signer on every continuation, exactly as the three create paths already do (lifecycle/import.rs:465, lifecycle/drops.rs:305, drop/mod.rs:500). Then the chain records who made each change, which is what design/cryptography/provenance.md's "Provenance of Library Modifications" describes ("Every modification ... produces a provenance record — timestamp, device, client version, action").
  2. The creator is the author and a separate pair names the writer: then verify_asset must resolve the signature against the writer's directory, the manifest needs the extra fields, and it is a schema change under a new protocol_version.

(1) needs no wire change and is what every doc in the tree already says. It is almost certainly the answer, but it changes what a stored chain means, so it should be decided explicitly rather than by patch.

Tests that should exist either way

  • capsule-core: a second device of the same account continues a chain; assert verify_asset accepts.
  • capsule-core: a writer member continues another account's chain; assert whatever the decision makes correct.
  • capsule-server: tests/ops.rs currently constructs its member-write envelope by hand and cannot catch this; once core can produce a real continuation, that case should be driven from a real signed manifest.

Refs #405. Found while addressing the review of PR #458.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions