Repository navigation
feat(security): define provider-neutral credential registry contract #344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
seonghobae
wants to merge
22
commits into
main
Choose a base branch
from
feat/credential-registry-contract-main-20260811
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
4b70001
test(security): define credential registry contract
seonghobae fecd141
feat(security): add credential purpose separation
seonghobae 6af1d08
feat(security): add purpose-scoped credential reference
seonghobae 26dfadb
feat(security): add redacted credential snapshot
seonghobae f7335f9
feat(security): add provider-neutral credential registry port
seonghobae 76416c6
chore: reconcile credential registry contract onto current protected …
seonghobae 56c001e
chore: reconcile credential registry contract with protected main
seonghobae d89707d
Merge branch 'main' into feat/credential-registry-contract-main-20260811
opencode-agent[bot] 2b88939
test(security): reject credential purpose mismatch
seonghobae fa8cc34
fix(security): enforce credential purpose separation
seonghobae 1fab7df
test(security): cover missing credential material
seonghobae 7c1cf45
test(security): hide raw credential resolve boundary
seonghobae 0f7e7b2
fix(security): separate credential adapter hook
seonghobae 4747a69
test(security): exercise explicit credential adapter hook
seonghobae 760bc7e
test(security): reject credential identity substitution
seonghobae 2f7e9db
fix(security): bind credential identity at registry boundary
seonghobae fbadf96
docs(security): ground credential registry in NIST key management
seonghobae 0aa56e2
docs(architecture): record credential registry boundary
seonghobae fc4af3c
docs(changelog): record fail-closed credential registry contract
seonghobae 62a53c9
Merge branch 'main' into feat/credential-registry-contract-main-20260811
opencode-agent[bot] 3fa8fe1
test(security): exercise credential purpose mismatch guard
seonghobae 1e35496
docs(research): pin NIST key-management artifact provenance
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
docs/research/2026-08-29-credential-registry-key-management-grounding.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # Credential registry key-management grounding | ||
|
|
||
| ## Decision | ||
|
|
||
| The provider-neutral credential registry boundary binds each server-owned logical credential name to the returned credential identifier and `CredentialPurpose`. Resolution fails closed when material is missing, the logical identity differs, or the cryptographic purpose differs. The provider-returned version remains metadata for later rotation and audit integration. | ||
|
|
||
| This keeps provider translation inside the adapter boundary: if a backing KMS, HSM, database, or managed-secret service uses a provider-specific alias, the adapter must map that alias back to the server-owned logical credential identifier before returning a `CredentialSnapshot`. Runtime consumers do not accept ambiguous or substituted identities. | ||
|
|
||
| ## Standards traceability | ||
|
|
||
| NIST SP 800-57 Part 1 Rev. 5 treats key-control metadata such as key identifiers and intended usage as information that must be protected so cryptographic keying material is used correctly. It also recommends that a key generally be used for only one purpose. These controls support Clearfolio's strict logical-identity binding and distinct `CredentialPurpose` values for tenant-claims signing and artifact-token signing. | ||
|
|
||
| Revision 5 remains the current final Part 1 publication. NIST published Revision 6 only as an Initial Public Draft on December 5, 2025; its public-comment period closed on February 5, 2026. The final-revision contract therefore continues to cite Revision 5 rather than silently adopting draft guidance. | ||
|
|
||
| This PR defines only the provider-neutral contract. Persistence, KMS/HSM integration, rotation/compatibility windows, audit-record writes, Spring bootstrap, and runtime migration of `TenantAccessService` and `ArtifactLinkService` remain owned by issue #319. | ||
|
|
||
| ## Source artifact provenance | ||
|
|
||
| - Official publication page: <https://www.nist.gov/publications/recommendation-key-management-part-1-general-1> | ||
| - Official PDF: <https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf> | ||
| - DOI: <https://doi.org/10.6028/NIST.SP.800-57pt1r5> | ||
| - Redistribution status: the publication itself states that it is not subject to copyright in the United States and that attribution is appreciated by NIST. NIST's Research Library likewise states that NIST Federal-government publications are generally public-domain works in the United States and may be reprinted or copied with source credit. | ||
|
|
||
| `AGENTS.md` asks substantive PRs to commit paper PDFs when redistribution is permissible. The repository does not currently contain this NIST PDF under `docs/papers/`, so this note deliberately does not invent a repository-relative attachment. Promotion remains blocked on the open review requirement until the actual binary artifact is committed and linked here, or the repository rule is changed through its canonical governance path. | ||
|
|
||
| ## APA 7 reference | ||
|
|
||
| Barker, E. (2020). *Recommendation for key management: Part 1—General* (NIST Special Publication 800-57 Part 1 Rev. 5). National Institute of Standards and Technology. <https://doi.org/10.6028/NIST.SP.800-57pt1r5> |
21 changes: 21 additions & 0 deletions
21
src/main/java/com/clearfolio/viewer/credential/CredentialPurpose.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.clearfolio.viewer.credential; | ||
|
|
||
| /** | ||
| * Cryptographic purpose that constrains how resolved credential material may be used. | ||
| * | ||
| * <p>Purpose separation prevents one signing key from silently becoming authority | ||
| * for a different security boundary. Registry adapters must return material whose | ||
| * stored purpose matches the requested reference.</p> | ||
| */ | ||
| public enum CredentialPurpose { | ||
|
|
||
| /** | ||
| * HMAC material used only for tenant-claim signing and verification. | ||
| */ | ||
| TENANT_CLAIMS_SIGNING, | ||
|
|
||
| /** | ||
| * HMAC material used only for artifact-link signing and verification. | ||
| */ | ||
| ARTIFACT_TOKEN_SIGNING | ||
| } |
26 changes: 26 additions & 0 deletions
26
src/main/java/com/clearfolio/viewer/credential/CredentialReference.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.clearfolio.viewer.credential; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| /** | ||
| * Immutable server-owned reference to one purpose-scoped credential. | ||
| * | ||
| * @param credentialName stable logical credential name, never secret material | ||
| * @param purpose cryptographic purpose that the resolved material may serve | ||
| */ | ||
| public record CredentialReference(String credentialName, CredentialPurpose purpose) { | ||
|
|
||
| /** | ||
| * Validates and normalizes the credential authority reference. | ||
| * | ||
| * @throws IllegalArgumentException when the credential name is absent or blank | ||
| * @throws NullPointerException when the credential purpose is absent | ||
| */ | ||
| public CredentialReference { | ||
| if (credentialName == null || credentialName.isBlank()) { | ||
| throw new IllegalArgumentException("credentialName must not be blank"); | ||
| } | ||
| credentialName = credentialName.strip(); | ||
| Objects.requireNonNull(purpose, "purpose"); | ||
| } | ||
| } |
62 changes: 62 additions & 0 deletions
62
src/main/java/com/clearfolio/viewer/credential/CredentialRegistry.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package com.clearfolio.viewer.credential; | ||
|
seonghobae marked this conversation as resolved.
Resolved
seonghobae marked this conversation as resolved.
Resolved
|
||
|
|
||
| import java.util.Objects; | ||
|
|
||
| /** | ||
| * Provider-neutral boundary for resolving versioned credential material. | ||
| * | ||
| * <p>Implementations may use an encrypted database registry, managed secret | ||
| * service, HSM/KMS-backed adapter, or deterministic test fixture. Runtime | ||
| * consumers depend only on this contract so environment variables can remain | ||
| * bootstrap transport rather than the credential source of truth.</p> | ||
| * | ||
| * <p>Runtime consumers call {@link #resolveScoped(CredentialReference)}. The | ||
| * single abstract {@link #resolveAdapterMaterial(CredentialReference)} method is | ||
| * deliberately named as an adapter hook so the lambda-friendly implementation | ||
| * surface cannot be mistaken for the authority-enforcing consumer boundary.</p> | ||
| */ | ||
| @FunctionalInterface | ||
| public interface CredentialRegistry { | ||
|
|
||
| /** | ||
| * Version of the provider-neutral credential resolution contract. | ||
| */ | ||
| String CONTRACT_VERSION = "credential-registry-v1"; | ||
|
|
||
| /** | ||
| * Obtains one credential snapshot from a provider adapter. | ||
| * | ||
| * <p>This method is an adapter implementation hook only. Runtime consumers | ||
| * must use {@link #resolveScoped(CredentialReference)}, which independently | ||
| * verifies that returned material is present and bound to the requested | ||
| * logical credential identity and cryptographic purpose.</p> | ||
| * | ||
| * @param reference server-owned logical credential reference | ||
| * @return versioned credential snapshot supplied by the adapter | ||
| */ | ||
| CredentialSnapshot resolveAdapterMaterial(CredentialReference reference); | ||
|
|
||
| /** | ||
| * Resolves credential material and fails closed when adapter authority drifts. | ||
| * | ||
| * @param reference server-owned logical credential reference | ||
| * @return versioned credential snapshot whose identity and purpose match the reference | ||
| * @throws NullPointerException when the reference is absent | ||
| * @throws IllegalStateException when the adapter returns no material or returns | ||
| * material for a different logical identity or purpose | ||
| */ | ||
| default CredentialSnapshot resolveScoped(CredentialReference reference) { | ||
| CredentialReference requiredReference = Objects.requireNonNull(reference, "reference"); | ||
| CredentialSnapshot snapshot = resolveAdapterMaterial(requiredReference); | ||
| if (snapshot == null) { | ||
| throw new IllegalStateException("credential registry returned no material"); | ||
| } | ||
| if (!snapshot.credentialId().equals(requiredReference.credentialName())) { | ||
| throw new IllegalStateException("credential registry returned material for a different identity"); | ||
| } | ||
| if (snapshot.purpose() != requiredReference.purpose()) { | ||
| throw new IllegalStateException("credential registry returned material for a different purpose"); | ||
| } | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
|
||
| return snapshot; | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
|
||
| } | ||
| } | ||
113 changes: 113 additions & 0 deletions
113
src/main/java/com/clearfolio/viewer/credential/CredentialSnapshot.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package com.clearfolio.viewer.credential; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| /** | ||
| * Immutable versioned snapshot of secret material returned by a credential registry. | ||
| * | ||
| * <p>Secret bytes are defensively copied on construction and access. The string | ||
| * representation is deliberately redacted so routine logging cannot reveal key | ||
| * material.</p> | ||
| */ | ||
| public final class CredentialSnapshot { | ||
|
|
||
| /** Stable logical identifier for the resolved credential. */ | ||
| private final String credentialId; | ||
|
|
||
| /** Version identifier of the resolved credential material. */ | ||
| private final String version; | ||
|
|
||
| /** Cryptographic purpose authorized for the resolved material. */ | ||
| private final CredentialPurpose purpose; | ||
|
|
||
| /** Private defensive copy of the resolved secret bytes. */ | ||
| private final byte[] secretBytes; | ||
|
|
||
| /** | ||
| * Creates one immutable credential snapshot. | ||
| * | ||
| * @param credentialId stable logical credential identifier | ||
| * @param version exact resolved credential version | ||
| * @param purpose cryptographic purpose authorized for this material | ||
| * @param secretBytes non-empty secret material copied by this instance | ||
| * @throws IllegalArgumentException when an identifier, version, or secret is absent | ||
| * @throws NullPointerException when the credential purpose is absent | ||
| */ | ||
| public CredentialSnapshot( | ||
| String credentialId, | ||
| String version, | ||
| CredentialPurpose purpose, | ||
| byte[] secretBytes | ||
| ) { | ||
| this.credentialId = requireText(credentialId, "credentialId"); | ||
| this.version = requireText(version, "version"); | ||
| this.purpose = Objects.requireNonNull(purpose, "purpose"); | ||
| if (secretBytes == null || secretBytes.length == 0) { | ||
| throw new IllegalArgumentException("secretBytes must not be empty"); | ||
| } | ||
| this.secretBytes = secretBytes.clone(); | ||
| } | ||
|
|
||
| /** | ||
| * Returns the stable logical credential identifier. | ||
| * | ||
| * @return credential identifier | ||
| */ | ||
| public String credentialId() { | ||
| return credentialId; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the exact resolved credential version. | ||
| * | ||
| * @return credential version | ||
| */ | ||
| public String version() { | ||
| return version; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the cryptographic purpose authorized for this material. | ||
| * | ||
| * @return credential purpose | ||
| */ | ||
| public CredentialPurpose purpose() { | ||
| return purpose; | ||
| } | ||
|
|
||
| /** | ||
| * Returns a defensive copy of the secret material. | ||
| * | ||
| * @return newly copied secret bytes | ||
| */ | ||
| public byte[] secretBytes() { | ||
| return secretBytes.clone(); | ||
| } | ||
|
|
||
| /** | ||
| * Returns metadata while redacting the secret payload. | ||
| * | ||
| * @return redacted snapshot description | ||
| */ | ||
| @Override | ||
| public String toString() { | ||
| return "CredentialSnapshot[credentialId=" + credentialId | ||
| + ", version=" + version | ||
| + ", purpose=" + purpose | ||
| + ", secret=<redacted>]"; | ||
| } | ||
|
|
||
| /** | ||
| * Validates and strips a public metadata field. | ||
| * | ||
| * @param value metadata value | ||
| * @param fieldName field name used only in the controlled error message | ||
| * @return stripped non-blank value | ||
| */ | ||
| private static String requireText(String value, String fieldName) { | ||
| if (value == null || value.isBlank()) { | ||
| throw new IllegalArgumentException(fieldName + " must not be blank"); | ||
| } | ||
| return value.strip(); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.