Skip to content
Draft
Show file tree
Hide file tree
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 Aug 10, 2026
fecd141
feat(security): add credential purpose separation
seonghobae Aug 10, 2026
6af1d08
feat(security): add purpose-scoped credential reference
seonghobae Aug 10, 2026
26dfadb
feat(security): add redacted credential snapshot
seonghobae Aug 10, 2026
f7335f9
feat(security): add provider-neutral credential registry port
seonghobae Aug 10, 2026
76416c6
chore: reconcile credential registry contract onto current protected …
seonghobae Aug 12, 2026
56c001e
chore: reconcile credential registry contract with protected main
seonghobae Aug 13, 2026
d89707d
Merge branch 'main' into feat/credential-registry-contract-main-20260811
opencode-agent[bot] Aug 15, 2026
2b88939
test(security): reject credential purpose mismatch
seonghobae Aug 18, 2026
fa8cc34
fix(security): enforce credential purpose separation
seonghobae Aug 18, 2026
1fab7df
test(security): cover missing credential material
seonghobae Aug 18, 2026
7c1cf45
test(security): hide raw credential resolve boundary
seonghobae Aug 29, 2026
0f7e7b2
fix(security): separate credential adapter hook
seonghobae Aug 29, 2026
4747a69
test(security): exercise explicit credential adapter hook
seonghobae Aug 29, 2026
760bc7e
test(security): reject credential identity substitution
seonghobae Aug 29, 2026
2f7e9db
fix(security): bind credential identity at registry boundary
seonghobae Aug 29, 2026
fbadf96
docs(security): ground credential registry in NIST key management
seonghobae Aug 29, 2026
0aa56e2
docs(architecture): record credential registry boundary
seonghobae Aug 29, 2026
fc4af3c
docs(changelog): record fail-closed credential registry contract
seonghobae Aug 29, 2026
62a53c9
Merge branch 'main' into feat/credential-registry-contract-main-20260811
opencode-agent[bot] Aug 29, 2026
3fa8fe1
test(security): exercise credential purpose mismatch guard
seonghobae Aug 30, 2026
1e35496
docs(research): pin NIST key-management artifact provenance
seonghobae Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Architecture Map

Last updated: 2026-02-23
Last updated: 2026-08-29

## System Purpose

Expand Down Expand Up @@ -45,6 +45,13 @@ Current state: viewer/state API is implemented in this repository; downstream S2
- Domain lifecycle and retry metadata (`attemptCount`, `maxAttempts`, `retryAt`, `deadLettered`) plus manual dead-letter retry transition.
- `ViewerBootstrapResponse` (`src/main/java/com/clearfolio/viewer/api/ViewerBootstrapResponse.java`)
- Includes deterministic `sourceExtension` and `rendererAdapter` metadata for viewer adapter bootstrap.
- `CredentialRegistry` (`src/main/java/com/clearfolio/viewer/credential/CredentialRegistry.java`)
- Provider-neutral credential-resolution boundary for the issue #319 secret-source migration.
- Runtime consumers use `resolveScoped(...)`; provider adapters implement only `resolveAdapterMaterial(...)`.
- The scoped boundary fails closed when material is absent or when the returned logical credential identity or `CredentialPurpose` differs from the server-owned `CredentialReference`.
- `CredentialSnapshot` carries provider-returned version metadata, defensively copies secret bytes, and redacts secret material from string rendering.
- Provider-specific aliases remain inside adapters; runtime code accepts only the normalized server-owned logical credential identity. Persistence, KMS/HSM binding, rotation windows, audit writes, Spring bootstrap, and migration of existing HMAC consumers remain issue #319 follow-up scope.
- Standards traceability is recorded in `docs/research/2026-08-29-credential-registry-key-management-grounding.md`.

## State Model

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

### Security

- issue #319의 런타임 HMAC 비밀 이동을 위한 provider-neutral `CredentialRegistry` 계약을 추가했습니다. 런타임은 `resolveScoped(...)`만 사용하고 provider adapter는 `resolveAdapterMaterial(...)`을 구현하며, 반환 material이 없거나 server-owned logical credential identity 또는 `CredentialPurpose`가 다르면 fail closed 처리합니다. `CredentialSnapshot`은 secret bytes를 방어적으로 복사하고 문자열 출력에서 비밀을 숨깁니다. NIST SP 800-57 Part 1 Rev. 5 기반 key identity/purpose 분리 근거와 후속 KMS/HSM·rotation·audit·consumer migration 경계를 문서화했습니다.
- `GET /api/v1/convert/jobs/{jobId}/download`가 리소스 조회 전에 전용 `artifact:read` 권한을 검증하고, PDF 저장소 접근 전에 작업의 tenant 소유권을 확인하도록 강화했습니다. `job:read`만으로는 문서 바이트를 읽을 수 없으며, 인증 누락·권한 누락·교차 tenant UUID 접근은 각각 fail closed 처리되고 교차 tenant 요청은 리소스 존재를 숨기는 `404`를 반환합니다.
- Maven XML 테스트 보고서 검증기는 각 `testsuite`의 `tests`, `skipped`, `failures`, `errors` 속성을 모두 필수 증거로 요구합니다. 누락된 결과 수를 암묵적으로 0으로 간주하지 않고 fail closed 처리하며, 각 속성 누락 회귀 테스트를 추가했습니다.
- Maven XML 테스트 보고서 검증기는 UTF-8만 허용하고 UTF-8 BOM은 수용하며, NUL 바이트·DTD·엔터티 선언을 파싱 전에 거부합니다. UTF-16 같은 대체 인코딩으로 위험 선언을 바이트 검사에서 숨기는 우회와 외부 엔터티 읽기·엔터티 확장형 서비스 거부를 회귀 테스트로 차단했습니다.
Expand Down
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>
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
}
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");
}
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.clearfolio.viewer.credential;
Comment thread
seonghobae marked this conversation as resolved.
Resolved
Comment thread
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");
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
return snapshot;
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
}
}
113 changes: 113 additions & 0 deletions src/main/java/com/clearfolio/viewer/credential/CredentialSnapshot.java
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();
}
}
Loading
Loading