Skip to content

feat(dir): add ownership claim referrer, search, and CLI (v1) - #1478

Open
vivekkrishna wants to merge 2 commits into
agntcy:mainfrom
vivekkrishna:feat/ownership-v1
Open

feat(dir): add ownership claim referrer, search, and CLI (v1)#1478
vivekkrishna wants to merge 2 commits into
agntcy:mainfrom
vivekkrishna:feat/ownership-v1

Conversation

@vivekkrishna

@vivekkrishna vivekkrishna commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements SPIFFE-based record ownership using the existing `api/sign/v1/Signature` referrer type — as requested in the review: "make it part of the standard Signature field instead of adding new claims object."

Ownership claims are now pushed as `SignatureReferrerType` referrers with `content_type = "agntcy.dir.ownership.v1"` and `annotations["owner_id"] = `. No new proto message is needed.

Design

Field Value
Referrer type `agntcy.dir.sign.v1.Signature` (existing)
`content_type` `agntcy.dir.ownership.v1`
`annotations["owner_id"]` SPIFFE ID of the claiming principal
`signed_at` RFC3339 timestamp
`signature` Base64 ECDSA/RSA sig over SHA-256(owner_id + ":" + signed_at)
`certificate` Base64 DER-encoded SPIFFE SVID X.509 certificate

Unsigned claims (no `--key`/`--cert`) are accepted in dev environments; the reconciler logs a warning and indexes them without verification.

Changes

API

  • `api/core/v1/referrer_types.go`: `OwnershipContentType = "agntcy.dir.ownership.v1"` constant
  • `api/ownership/v1/sign.go`: `NewClaim`, `SignClaim`, `VerifyClaim`, `IsSigned`, `IsOwnershipClaim`, `GetOwnerID` — all operating on `*signv1.Signature`
  • Removed: `proto/agntcy/dir/ownership/v1/claim.proto` + generated files (no new proto needed)

Search

  • `proto/agntcy/dir/search/v1/record_query.proto`: `RECORD_QUERY_TYPE_OWNER = 20`
  • `server/types/search.go`: `Owners []string` filter + `WithOwners()`
  • `server/database/gorm/record.go`: owner JOIN filter
  • `server/database/utils/utils.go`: `RECORD_QUERY_TYPE_OWNER` → `WithOwners()`

Database

  • `server/database/gorm/owners.go`: `Owner` model (`record_cid`, `owner_id`, `claimed_at`)
  • `server/database/gorm/migration.go`: `Owner{}` in `AutoMigrate` list
  • `server/types/database.go`: `OwnershipDatabaseAPI` interface (`AddOwner`, `RemoveOwners`)

Ingestor

  • `server/ingest/ingest.go`: eagerly indexes ownership claims on push so search is immediate without waiting for the reconciler

Reconciler

  • `reconciler/tasks/ownership/task.go`: walks `SignatureReferrerType` referrers, filters by `IsOwnershipClaim`, verifies SPIFFE signatures, syncs `owners` table
  • `reconciler/config/config.go`: `Ownership` config field
  • `reconciler/service/service.go`: ownership task registration

CLI

  • `cli/cmd/ownership/ownership.go`: `dirctl ownership claim [--key --cert ]`

Test plan

  • All unit tests pass (`go test ./...` in api, server, reconciler, cli modules)
  • Lint clean (golangci-lint)
  • 7 ownership search e2e tests pass (exact match, wildcards, combined filter, negative cases)
  • 7 ownership signing e2e tests pass (signed claim indexed, identity mismatch rejected, idempotent re-push)

Generated with Claude Code

@vivekkrishna
vivekkrishna requested a review from a team as a code owner May 9, 2026 12:27
@github-actions github-actions Bot added the size/M Denotes a PR that changes 200-999 lines label May 9, 2026
@vivekkrishna

Copy link
Copy Markdown
Contributor Author

@ramizpolic Any comments on this?

@ramizpolic

ramizpolic commented May 20, 2026

Copy link
Copy Markdown
Member

Hey @vivekkrishna, thanks for the PR. It looks good from my end, the only issue is that we are working on simplifying the interfaces and API surface in this PR #1500, and I am not sure which order to take (merge that PR, adjust this, or something else). I think that work will be done soon (end of next week). Can we hold this PR open until then and do a rebase together to merge this PR?

Alternatively, I think you don't even need any code changes for your functionality since you can already push whichever referrer you want and it should work out of box. Code snippet below

// Route based on referrer type
switch referrer.GetType() {
case corev1.SignatureReferrerType:
// TODO: validate signature
return s.pushReferrer(ctx, recordCID, referrer)
case corev1.PublicKeyReferrerType:
// TODO: validate public key
return s.pushReferrer(ctx, recordCID, referrer)
default:
// Store as generic OCI referrer
return s.pushReferrer(ctx, recordCID, referrer)
}

The only issue would be that the CLI is not available, but this can be handled through a separate CLI for your needs built with DIR Go SDK. Lets sync tomorrow/when it works for you on Slack and we can decide next steps together.

@ramizpolic
ramizpolic self-requested a review May 20, 2026 21:00
@github-project-automation github-project-automation Bot moved this to Backlog in Discovery May 20, 2026
@ramizpolic ramizpolic moved this from Backlog to In Progress in Discovery May 20, 2026
@vivekkrishna

Copy link
Copy Markdown
Contributor Author

Sure @ramizpolic , I will sync up over slack.

@vivekkrishna

vivekkrishna commented May 23, 2026

Copy link
Copy Markdown
Contributor Author

Hi @ramizpolic . After analysis, merging as-is makes sense for these reasons.

The generic PushReferrer path (referrers.go:72-74) just stores a blob in OCI with no DB indexing — no owners table, no --owner search, and no foundation for manager subtree search. #1478's owners table + eager indexing + reconciler is exactly what makes ownership searchable and is the prerequisite for manager search (#1468).

On the #1500 question: planning to land #1478 on v1 PushReferrer as-is, then rebuild ownership onto the v2 ObjectStore + ObjectReferrer path once #1500 merges — does that sequencing work? Only immediate blocker is Target() added to StoreAPI in #1500 will break #1478's compile, so I'll add that method to the v1 OCI store when rebasing. Happy to hold if you'd rather do it right in v2 from the start — just didn't want to block manager search on #1500's timeline.

Comment thread server/controller/store.go Outdated
Comment on lines +258 to +264
var ownershipClaim *ownershipv1.Claim

if request.GetType() == corev1.OwnershipClaimReferrerType {
claim := &ownershipv1.Claim{}
if err := claim.UnmarshalReferrer(&corev1.RecordReferrer{Data: request.GetData()}); err != nil {
errMsg := fmt.Sprintf("failed to decode ownership claim: %v", err)

@ramizpolic ramizpolic May 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this ownership claim is currently unverifiable since the following things can happen:

  • user has write access to the OCI store (either directly or via sync)
  • user writes an ownership claim to a non-owned resource through OCI referrers (either directly or through its own OCI that gets synced to a targeted node)
  • target node indexes ownership through the reconciler
  • the resource is reported as owned by someone who actually doesnt own it

this behavior enables targeted attacks across the network since the data has 2 access points:

  • unsafe route through OCI that doesnt get verified in the reconciler
  • secure route through the controller that does get verified

my concern here is that the ownership claims are not verifiable. we already support verifiable claims through a public key and keyless OIDC signing (dirctl sign) via COSIGN which gets verified and indexed into the database. I am not sure if we currently have a mechanism to search over this data, but it is present and may only need query params to be exposed on the client-side. perhaps @paralta can assist on this question

i think a secure/verifiable way to enable this functionality is to add support for SPIFFE-based signing. this would provide all the things you need, and i would happily merge it. it would also be a super important feature for us, not just for local use-case, but across the (insecure) network.

could you please adjust the implementation according to the notes above and we can merge it right away

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the input @ramizpolic , addressed with new commit.

vivekkrishna pushed a commit to vivekkrishna/dir that referenced this pull request May 26, 2026
Addresses ramizpolic's security comment on PR agntcy#1478. Without signing,
any caller with OCI write access could forge ownership claims; the
reconciler would index them blindly.

Changes:
- proto: add signature (bytes) and certificate (bytes) fields to Claim
- api/ownership/v1/sign.go: SignClaim, VerifyClaim, IsSigned; supports
  ECDSA (P256/P384/P521) and RSA; canonical payload SHA-256(owner_id:claimed_at)
- cli: --key / --cert flags on ownership claim; identity mismatch caught
  client-side before any network call
- reconciler: TrustedCACertFile config; verifyClaim() gate before db.AddOwner();
  unsigned claims accepted with warning in dev mode (no SPIFFE configured)
- server/controller: indexOwnershipClaim() helper verifies signed claims
  before eager db.AddOwner() so tampered claims are rejected at push time
- tests: GenerateSpiffeTestCert helper (pure crypto/x509, no SPIRE needed);
  14_ownership_signing_test.go (happy path, identity mismatch, idempotent re-push);
  5 unit tests covering round-trip, tamper detection, and unsigned-claim rejection
@github-actions github-actions Bot added size/L Denotes a PR that changes 1000-1999 lines and removed size/M Denotes a PR that changes 200-999 lines labels May 26, 2026
vivekkrishna pushed a commit to vivekkrishna/dir that referenced this pull request May 26, 2026
Addresses ramizpolic's security comment on PR agntcy#1478. Without signing,
any caller with OCI write access could forge ownership claims; the
reconciler would index them blindly.

Changes:
- proto: add signature (bytes) and certificate (bytes) fields to Claim
- api/ownership/v1/sign.go: SignClaim, VerifyClaim, IsSigned; supports
  ECDSA (P256/P384/P521) and RSA; canonical payload SHA-256(owner_id:claimed_at)
- cli: --key / --cert flags on ownership claim; identity mismatch caught
  client-side before any network call
- reconciler: TrustedCACertFile config; verifyClaim() gate before db.AddOwner();
  unsigned claims accepted with warning in dev mode (no SPIFFE configured)
- server/controller: indexOwnershipClaim() helper verifies signed claims
  before eager db.AddOwner() so tampered claims are rejected at push time
- tests: GenerateSpiffeTestCert helper (pure crypto/x509, no SPIRE needed);
  14_ownership_signing_test.go (happy path, identity mismatch, idempotent re-push);
  5 unit tests covering round-trip, tamper detection, and unsigned-claim rejection

Signed-off-by: Vivek Krishna Choppa <vivekkrishnachoppa@gmail.com>
vivekkrishna pushed a commit to vivekkrishna/dir that referenced this pull request May 27, 2026
Addresses ramizpolic's security comment on PR agntcy#1478. Without signing,
any caller with OCI write access could forge ownership claims; the
reconciler would index them blindly.

Changes:
- proto: add signature (bytes) and certificate (bytes) fields to Claim
- api/ownership/v1/sign.go: SignClaim, VerifyClaim, IsSigned; supports
  ECDSA (P256/P384/P521) and RSA; canonical payload SHA-256(owner_id:claimed_at)
- cli: --key / --cert flags on ownership claim; identity mismatch caught
  client-side before any network call
- reconciler: TrustedCACertFile config; verifyClaim() gate before db.AddOwner();
  unsigned claims accepted with warning in dev mode (no SPIFFE configured)
- server/controller: indexOwnershipClaim() helper verifies signed claims
  before eager db.AddOwner() so tampered claims are rejected at push time
- tests: GenerateSpiffeTestCert helper (pure crypto/x509, no SPIRE needed);
  14_ownership_signing_test.go (happy path, identity mismatch, idempotent re-push);
  5 unit tests covering round-trip, tamper detection, and unsigned-claim rejection

Signed-off-by: Vivek Krishna Choppa <vivekkrishnachoppa@gmail.com>
@vivekkrishna

Copy link
Copy Markdown
Contributor Author

should we add this 1.5 milestone?

@ramizpolic ramizpolic added this to the [WIP] DIR v1.5 milestone May 29, 2026
@ramizpolic ramizpolic linked an issue May 29, 2026 that may be closed by this pull request
vivekkrishna pushed a commit to vivekkrishna/dir that referenced this pull request May 30, 2026
Addresses ramizpolic's security comment on PR agntcy#1478. Without signing,
any caller with OCI write access could forge ownership claims; the
reconciler would index them blindly.

Changes:
- proto: add signature (bytes) and certificate (bytes) fields to Claim
- api/ownership/v1/sign.go: SignClaim, VerifyClaim, IsSigned; supports
  ECDSA (P256/P384/P521) and RSA; canonical payload SHA-256(owner_id:claimed_at)
- cli: --key / --cert flags on ownership claim; identity mismatch caught
  client-side before any network call
- reconciler: TrustedCACertFile config; verifyClaim() gate before db.AddOwner();
  unsigned claims accepted with warning in dev mode (no SPIFFE configured)
- server/controller: indexOwnershipClaim() helper verifies signed claims
  before eager db.AddOwner() so tampered claims are rejected at push time
- tests: GenerateSpiffeTestCert helper (pure crypto/x509, no SPIRE needed);
  14_ownership_signing_test.go (happy path, identity mismatch, idempotent re-push);
  5 unit tests covering round-trip, tamper detection, and unsigned-claim rejection

Signed-off-by: Vivek Krishna Choppa <vivekkrishnachoppa@gmail.com>
@ramizpolic ramizpolic modified the milestones: DIR v1.5, v1.6 Jun 15, 2026
@ramizpolic ramizpolic added the triage/needs-information Indicates an issue or PR needs more information in order to work on it. label Jun 15, 2026
@ramizpolic

ramizpolic commented Jun 15, 2026

Copy link
Copy Markdown
Member

Hi @vivekkrishna, we will include this feature in v1.6, but we will need to make it part of the standard Signature field instead of adding new claims object. Are you okay if we cherry pick commits from this PR and continue working on it?

@vivekkrishna

Copy link
Copy Markdown
Contributor Author

Hi @ramizpolic , that works for me. Can you let me know the planned timeline for v1.6 release? Also see if you can add me or tag me in new PR for review or modify this same PR?. Also can you confirm search filter by --owner is still preserved after the signature approach, that is crucial for overall epic?

@ramizpolic ramizpolic removed this from the DIR v1.6 milestone Jul 15, 2026
@ramizpolic ramizpolic closed this Jul 15, 2026
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Discovery Jul 15, 2026
@ramizpolic ramizpolic reopened this Jul 15, 2026
vivekkrishna pushed a commit to vivekkrishna/dir that referenced this pull request Jul 26, 2026
Addresses ramizpolic's security comment on PR agntcy#1478. Without signing,
any caller with OCI write access could forge ownership claims; the
reconciler would index them blindly.

Changes:
- proto: add signature (bytes) and certificate (bytes) fields to Claim
- api/ownership/v1/sign.go: SignClaim, VerifyClaim, IsSigned; supports
  ECDSA (P256/P384/P521) and RSA; canonical payload SHA-256(owner_id:claimed_at)
- cli: --key / --cert flags on ownership claim; identity mismatch caught
  client-side before any network call
- reconciler: TrustedCACertFile config; verifyClaim() gate before db.AddOwner();
  unsigned claims accepted with warning in dev mode (no SPIFFE configured)
- server/controller: indexOwnershipClaim() helper verifies signed claims
  before eager db.AddOwner() so tampered claims are rejected at push time
- tests: GenerateSpiffeTestCert helper (pure crypto/x509, no SPIRE needed);
  14_ownership_signing_test.go (happy path, identity mismatch, idempotent re-push);
  5 unit tests covering round-trip, tamper detection, and unsigned-claim rejection

Signed-off-by: Vivek Krishna Choppa <vivekkrishnachoppa@gmail.com>
@vivekkrishna

Copy link
Copy Markdown
Contributor Author

@ramizpolic Can you check if these updates look good to approve based on recent comments with signature?

@vivekkrishna
vivekkrishna force-pushed the feat/ownership-v1 branch 2 times, most recently from 75cd280 to caa182a Compare July 27, 2026 04:39
…rrer

Implements ownership claims as part of the standard api/sign/v1/Signature
referrer (content_type = "agntcy.dir.ownership.v1") rather than a bespoke
proto type, following reviewer feedback from ramizpolic.

Changes:
- Add api/ownership/v1/sign.go: NewClaim, SignClaim, VerifyClaim, IsOwnershipClaim,
  GetOwnerID helpers operating on *signv1.Signature; SPIFFE/X.509 SVID signing
  with ECDSA/RSA support; canonical payload SHA-256(owner_id + ":" + signed_at)
- Add ownership reconciler task (reconciler/tasks/ownership/): walks
  SignatureReferrerType referrers, filters by content_type, verifies SPIFFE
  cert chain and signature, indexes owners into DB
- Add server/database/gorm/owners.go: Owner model (record_cid, owner_id,
  claimed_at) with AddOwner/RemoveOwners; migrate via AutoMigrate
- Add OwnershipDatabaseAPI to server/types/database.go and DatabaseAPI composite
- Add owner search filter (RECORD_QUERY_TYPE_OWNER = 20, WithOwners/WithoutOwners)
  wired through proto, pb.go, DB utils, and search types, including
  --exclude-owner to match the repo's include/exclude filter invariant
- Add server/ingest/ingest.go: eager ownership index on Signature referrer push
  so search reflects new owner immediately without waiting for reconciler
- Add CLI subcommand: dirctl ownership claim [--key/--cert] <record-cid> <owner-id>
- Add e2e tests: 13_ownership_search_test.go (unsigned claims, wildcard owner
  search), 14_ownership_signing_test.go (signed SPIFFE claims, reconciler
  re-index, identity mismatch rejection)
- Bump buf.lock for protovalidate to resolve a stale dependency lock that
  failed CI's "task gen" clean-tree check

Signed-off-by: Vivek Krishna Choppa <vivekkrishnachoppa@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli area/dir/server size/L Denotes a PR that changes 1000-1999 lines triage/needs-information Indicates an issue or PR needs more information in order to work on it.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Feature] Support SPIFFE-based record identifiers

2 participants