haechi 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +3 -1
- package/README.md +3 -1
- package/docs/current/api-stability.ko.md +3 -3
- package/docs/current/api-stability.md +3 -3
- package/docs/current/config-version.ko.md +2 -2
- package/docs/current/config-version.md +2 -2
- package/docs/current/configuration.ko.md +1 -1
- package/docs/current/configuration.md +1 -1
- package/docs/current/operations-runbook.ko.md +1 -1
- package/docs/current/operations-runbook.md +1 -1
- package/docs/current/plugin-signing-and-trust.ko.md +1 -1
- package/docs/current/plugin-signing-and-trust.md +1 -1
- package/docs/current/release-process.ko.md +1 -1
- package/docs/current/release-process.md +1 -1
- package/docs/current/risk-register-release-gate.ko.md +9 -8
- package/docs/current/risk-register-release-gate.md +9 -8
- package/docs/current/shared-responsibility.ko.md +1 -1
- package/docs/current/shared-responsibility.md +1 -1
- package/docs/current/threat-model.ko.md +3 -2
- package/docs/current/threat-model.md +3 -2
- package/package.json +1 -1
- package/packages/crypto/index.mjs +69 -6
- package/packages/token-vault/index.mjs +3 -2
package/README.ko.md
CHANGED
|
@@ -229,7 +229,7 @@ JWT/JWKS 인증과 KMS 기반 key custody(및 기타 선택 기능)는 **`haechi
|
|
|
229
229
|
|
|
230
230
|
## 위성 패키지
|
|
231
231
|
|
|
232
|
-
선택 기능은 **npm에 독립 발행되는 `haechi-*` 패키지**로 제공됩니다 — 각각 core와 별도로 버저닝되고, 기본적으로 `node:` 전용이며(KMS나 Redis 클라이언트 같은 무거운 SDK는 optional peer), `haechi` peer 범위를 `>=0.8.0 <2.0.0
|
|
232
|
+
선택 기능은 **npm에 독립 발행되는 `haechi-*` 패키지**로 제공됩니다 — 각각 core와 별도로 버저닝되고, 기본적으로 `node:` 전용이며(KMS나 Redis 클라이언트 같은 무거운 SDK는 optional peer), core major를 추적하는 `haechi` peer 범위를 선언합니다. 대부분의 위성은 `>=0.8.0 <2.0.0`를 유지하고, `haechi-crypto-kms@0.3.0`은 core v2 crypto-AAD helper를 공유하므로 `>=1.7.0 <2.0.0`을 요구합니다.
|
|
233
233
|
|
|
234
234
|
**위성과 함께 core를 반드시 설치하세요** — `haechi`는 **번들되지 않은 peer dependency**이므로, 위성만으로는 동작하지 않습니다:
|
|
235
235
|
|
|
@@ -383,3 +383,5 @@ Haechi는 의도적으로 범위를 좁혔습니다. 아래는 숨기지 않고
|
|
|
383
383
|
1.5.0은 **fleet-readiness** 트랙을 시작합니다. audit sink와 token vault가 주입 가능한 **store 시임**(`createAuditSink({ store })` / `createTokenVault({ store })` + 기본 `createFileAuditStore`/`createFileTokenStore`)을 갖게 되어, 공유 저장소가 sha256 audit 해시 체인과 token vault를 **여러 replica** 전반에서 뒷받침할 수 있습니다 — [Known limitations](#known-limitations)에서 짚은 프로세스별 / 단일 writer 한계를 해소합니다. 파일 기반 기본값은 바이트 동일하고 `createJsonlAuditSink`/`createLocalTokenVault`는 하위호환 래퍼로 유지됩니다. `haechi-store-redis` 위성(두 시임의 Redis 어댑터)이 운영 소비자입니다. additive(**마이너**); `configVersion`은 `1` 유지; core는 zero runtime dependency 유지.
|
|
384
384
|
|
|
385
385
|
1.6.0은 **crypto 봉투**를 강화합니다. 로컬 AES-256-GCM provider가 이제 키별 nonce 예산을 강제합니다 — 랜덤 96-bit IV는 키당 한정된 횟수의 암호화까지만 안전하므로(NIST SP 800-38D §8.3), provider가 키별 암호화 횟수를 세어 키 파일에 영속화하고, 50%에서 경고하며, **한도에서 fail-closed**(`haechi init --force`로 회전)합니다. 읽기 전용 키 파일은 프로세스 단위 한도로 degrade합니다. `haechi status`가 예산을 노출하고(`keys.nonceBudget`, 사용 %), `haechi/crypto`는 `readNonceBudget`를 export합니다. 명명된 `gate:security` CI 잡이 횡단 보안 invariant(audit 평문 없음, hard-block 비억제, AAD/AEAD 바인딩, nonce 예산, privacy-profile strengthen-only)를 독립 required check로 실행합니다. additive(**마이너**); `configVersion`은 `1` 유지; core는 zero runtime dependency 유지.
|
|
386
|
+
|
|
387
|
+
1.7.0은 다음 crypto-envelope hardening 단계를 닫습니다. 새 envelope는 `v:2`, `aadEncoding:"nfkc-json-v2"`를 사용하므로 crypto AAD가 NFKC-normalized string value/object key를 포함한 정렬 canonical JSON(`canonicalizeCryptoAad`)으로 고정됩니다. Legacy v1 envelope는 기존 AAD canonicalization으로 계속 복호화됩니다. NFKC 때문에 같은 object level에서 key collision이 생기면 fail-closed하고, token-vault ciphertext는 token `expiresAt`를 envelope에 담아 stale ciphertext를 crypto 계층에서도 거부합니다. `haechi-crypto-kms@0.3.0`도 같은 core helper를 사용하며 `haechi >=1.7.0`를 요구합니다. additive(**마이너**); `configVersion`은 `1` 유지; core는 zero runtime dependency 유지.
|
package/README.md
CHANGED
|
@@ -229,7 +229,7 @@ JWT/JWKS auth and KMS-backed key custody (and other optional capabilities) ship
|
|
|
229
229
|
|
|
230
230
|
## Satellite packages
|
|
231
231
|
|
|
232
|
-
Optional capabilities ship as independently-published **`haechi-*` packages on npm** — each versioned separately from core, `node:`-only by default (heavy SDKs like a KMS or Redis client are optional peers), and each declaring a `haechi` peer range
|
|
232
|
+
Optional capabilities ship as independently-published **`haechi-*` packages on npm** — each versioned separately from core, `node:`-only by default (heavy SDKs like a KMS or Redis client are optional peers), and each declaring a `haechi` peer range that tracks the core major. Most satellites keep `>=0.8.0 <2.0.0`; `haechi-crypto-kms@0.3.0` requires `>=1.7.0 <2.0.0` because it shares the core v2 crypto-AAD helper.
|
|
233
233
|
|
|
234
234
|
**Install the core alongside any satellite** — `haechi` is a **peer dependency, not bundled**, so a satellite does nothing on its own:
|
|
235
235
|
|
|
@@ -383,3 +383,5 @@ Haechi is deliberately scoped. These are real, current limitations — listed op
|
|
|
383
383
|
1.5.0 begins the **fleet-readiness** track: the audit sink and token vault gain an injectable **store seam** (`createAuditSink({ store })` / `createTokenVault({ store })` + the default `createFileAuditStore`/`createFileTokenStore`), so a shared store can back the sha256 audit hash chain and the token vault across **multiple replicas** — closing the per-process / single-writer limitations called out under [Known limitations](#known-limitations). The file-backed defaults are byte-identical, and `createJsonlAuditSink`/`createLocalTokenVault` stay as back-compat wrappers. A `haechi-store-redis` satellite (the Redis adapters for both seams) is the production consumer. Additive (a **minor**); `configVersion` stays `1`; core stays zero runtime dependency.
|
|
384
384
|
|
|
385
385
|
1.6.0 hardens the **crypto envelope**: the local AES-256-GCM provider now enforces a per-key nonce budget — random 96-bit IVs are only safe for a bounded number of encryptions per key (NIST SP 800-38D §8.3), so the provider counts encryptions per key, persists the count to the key file, warns at 50%, and **fails closed at the limit** (rotate with `haechi init --force`); a read-only key file degrades to a per-process limit. `haechi status` surfaces the budget (`keys.nonceBudget`, used %) and `haechi/crypto` exports `readNonceBudget`. A named `gate:security` CI job runs the cross-cutting security invariants (no plaintext in audit, hard-block non-suppressible, AAD/AEAD binding, the nonce budget, privacy-profile strengthen-only) as an independently required check. Additive (a **minor**); `configVersion` stays `1`; core stays zero runtime dependency.
|
|
386
|
+
|
|
387
|
+
1.7.0 completes the next crypto-envelope hardening step: new envelopes are `v:2` with `aadEncoding:"nfkc-json-v2"`, so crypto AAD uses sorted canonical JSON with NFKC-normalized string values and object keys (`canonicalizeCryptoAad`). Legacy v1 envelopes still decrypt with the old AAD canonicalization. AAD key collisions introduced by NFKC fail closed, and token-vault ciphertext now carries the token `expiresAt` into the envelope so stale ciphertext is rejected by the crypto layer as defense-in-depth. `haechi-crypto-kms@0.3.0` uses the same core helper and requires `haechi >=1.7.0`. Additive (a **minor**); `configVersion` stays `1`; core stays zero runtime dependency.
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
| `haechi` / `haechi/core` — `createHaechi().protectJson`, `createHaechi().createStreamProtector`, `collectStringEntries`, `pathToString`, `safePathToString`, `shapeOnly`, `summarize` | **FROZEN** (breaking change = major) |
|
|
29
29
|
| `haechi/runtime` — `createRuntime`, `normalizeConfig` (config shape), `defaultConfig`, `loadConfig`, `writeDefaultConfig`, `isValidPort`, `DEFAULT_CONFIG_PATH` | **FROZEN** |
|
|
30
30
|
| `haechi/auth` — `authProvider` 계약, `buildIdentity`, `buildExternalIdentity`, `validateLabels`, `createBearerAuthProvider`, token store (`readAuthStore`, `addToken`, `listTokens`, `revokeToken`), `DEFAULT_ALLOWED_LABEL_KEYS` | **FROZEN** |
|
|
31
|
-
| `haechi/crypto` — `cryptoProvider` 계약, `assertCryptoProviderConformance`, `canonicalize`, `createLocalCryptoProvider`, `initLocalKeyFile`, `readNonceBudget` | **FROZEN** |
|
|
31
|
+
| `haechi/crypto` — `cryptoProvider` 계약, `assertCryptoProviderConformance`, `canonicalize`, `canonicalizeCryptoAad`, `CRYPTO_AAD_ENCODING_V2`, `createLocalCryptoProvider`, `initLocalKeyFile`, `readNonceBudget` | **FROZEN** |
|
|
32
32
|
| `haechi/audit` — audit **event schema** (§2.3), `verifyAuditChain`, `sanitizeAudit`, `createJsonlAuditSink`, `readAuditSummary`, `FORBIDDEN_KEYS`, 그리고 1.5.0 주입 가능한 store 시임 `createAuditSink`, `createFileAuditStore`, `buildIntegrityRecord` | **FROZEN** |
|
|
33
33
|
| `haechi/policy` — `buildPolicy`, `createPolicyEngine`, `createPolicyProfiles`, `validatePolicy`, `ACTION_STRENGTH` (action ordering) | **FROZEN** |
|
|
34
34
|
| `haechi/filter` — `createDefaultFilterEngine`, `detectEntry`, 그리고 **rule/detection shape** | **FROZEN** |
|
|
@@ -110,10 +110,10 @@ audit event(`packages/core/index.mjs`의 `buildAuditEvent`가 생성하고 `pack
|
|
|
110
110
|
위성(예: `haechi-crypto-kms`, `haechi-auth-jwt`, `haechi-dashboard`, `haechi-auth-oidc`)은 core와 **독립적으로** 버저닝합니다 — 위성 릴리스가 `haechi`를 bump하지 않고, 그 반대도 마찬가지입니다.
|
|
111
111
|
|
|
112
112
|
- **pre-1.0:** 위성은 npm semver를 따르며 `0.x` **minor** bump가 breaking change를 담을 수 있습니다. `major.minor`로 핀합니다(예: `haechi-crypto-kms@~0.2`). 각자 자체 `1.0.0`까지 pre-stable입니다.
|
|
113
|
-
- **core 호환성**은 `peerDependencies`
|
|
113
|
+
- **core 호환성**은 `peerDependencies` 범위로 표현합니다 — 위성은 소비자가 설치한 단일 `haechi`를 재사용하므로 crypto/identity 표면이 하나입니다. 대부분의 1.x 위성은 `"haechi": ">=0.8.0 <2.0.0"`를 유지하지만, `haechi-crypto-kms@0.3.0`은 core의 `canonicalizeCryptoAad` export를 사용하므로 `"haechi": ">=1.7.0 <2.0.0"`를 요구합니다. `haechi-auth-oidc`는 추가로 `haechi-auth-jwt`(`">=0.3.0 <2.0.0"`)에 peer-depend하여 둘이 audit되는 단일 JWS/JWKS 검증 경로를 공유합니다. 위성의 `haechi` peer-dependency **상한은 반드시 core MAJOR를 추적해야 하며**(`<2.0.0`), 다음 minor 미만으로 고정해서는 안 됩니다 — core의 minor/major 호환 범프가 위성 설치를 깨뜨리지 않도록 하기 위함입니다. `release:preflight` 게이트(`scripts/check-satellite-peer-ranges.mjs`)가 이를 자동으로 강제합니다.
|
|
114
114
|
- **무거운 백엔드는 optional peer입니다.** `haechi-crypto-kms`는 SDK 백엔드(`@aws-sdk/client-kms`, 그리고 0.2.0의 `@google-cloud/kms`, `@azure/keyvault-keys`, `@azure/identity`)를 `peerDependencies` + `peerDependenciesMeta.optional`로 선언하고 lazy import하므로, 해당 경로를 쓰지 않는 소비자는 설치하지 않고 core는 zero-dependency를 유지합니다. `./vault` 백엔드는 `node:` `fetch`만 사용합니다(optional peer 없음). 위성의 배포 tarball은 항상 **runtime `dependencies` 0**을 선언합니다(CI `check-satellite-packaging`로 강제).
|
|
115
115
|
- **pre-1.0 위성 export**는 preview이며 각 위성의 자체 `1.0.0` 전에 변경될 수 있습니다.
|
|
116
|
-
- `haechi-crypto-kms` (0.8 → 0.
|
|
116
|
+
- `haechi-crypto-kms` (0.8 → 0.3.0): `createKmsCryptoProvider`, `createInMemoryKms`, `./aws`의 `createAwsKmsClient`, 0.2.0 subpath `./gcp`(`createGcpKmsClient`), `./azure`(`createAzureKmsClient`), `./vault`(`createVaultKmsClient`), 그리고 0.3.0의 core v2 crypto-AAD parity(`haechi >=1.7.0` peer floor).
|
|
117
117
|
- `haechi-auth-jwt` (0.2.0): `createJwtAuthProvider`(0.8, behavior-preserving)와 추가된 `createJwtVerifier`(재사용 가능한 JWS 검증 primitive), `isBlockedAddress`(SSRF 범위 술어, `haechi-auth-oidc`가 재사용).
|
|
118
118
|
- `haechi-dashboard` (0.1.0, 신규): `createDashboardServer`, `normalizeDashboardConfig`.
|
|
119
119
|
- `haechi-auth-oidc` (0.1.0, 신규): `createOidcSessionBroker`, `normalizeOidcConfig`.
|
|
@@ -28,7 +28,7 @@ Every `package.json` `exports` subpath and the CLI is classed. There is no silen
|
|
|
28
28
|
| `haechi` / `haechi/core` — `createHaechi().protectJson`, `createHaechi().createStreamProtector`, `collectStringEntries`, `pathToString`, `safePathToString`, `shapeOnly`, `summarize` | **FROZEN** (breaking change = major) |
|
|
29
29
|
| `haechi/runtime` — `createRuntime`, `normalizeConfig` (config shape), `defaultConfig`, `loadConfig`, `writeDefaultConfig`, `isValidPort`, `DEFAULT_CONFIG_PATH` | **FROZEN** |
|
|
30
30
|
| `haechi/auth` — the `authProvider` contract, `buildIdentity`, `buildExternalIdentity`, `validateLabels`, `createBearerAuthProvider`, the token store (`readAuthStore`, `addToken`, `listTokens`, `revokeToken`), `DEFAULT_ALLOWED_LABEL_KEYS` | **FROZEN** |
|
|
31
|
-
| `haechi/crypto` — the `cryptoProvider` contract, `assertCryptoProviderConformance`, `canonicalize`, `createLocalCryptoProvider`, `initLocalKeyFile`, `readNonceBudget` | **FROZEN** |
|
|
31
|
+
| `haechi/crypto` — the `cryptoProvider` contract, `assertCryptoProviderConformance`, `canonicalize`, `canonicalizeCryptoAad`, `CRYPTO_AAD_ENCODING_V2`, `createLocalCryptoProvider`, `initLocalKeyFile`, `readNonceBudget` | **FROZEN** |
|
|
32
32
|
| `haechi/audit` — the audit **event schema** (§2.3), `verifyAuditChain`, `sanitizeAudit`, `createJsonlAuditSink`, `readAuditSummary`, `FORBIDDEN_KEYS`, plus the 1.5.0 injectable store seam `createAuditSink`, `createFileAuditStore`, `buildIntegrityRecord` | **FROZEN** |
|
|
33
33
|
| `haechi/policy` — `buildPolicy`, `createPolicyEngine`, `createPolicyProfiles`, `validatePolicy`, `ACTION_STRENGTH` (action ordering) | **FROZEN** |
|
|
34
34
|
| `haechi/filter` — `createDefaultFilterEngine`, `detectEntry`, and the **rule/detection shape** | **FROZEN** |
|
|
@@ -110,10 +110,10 @@ A migration note is added to `docs/current/release-*.md` or the README whenever
|
|
|
110
110
|
Satellites (e.g. `haechi-crypto-kms`, `haechi-auth-jwt`, `haechi-dashboard`, `haechi-auth-oidc`) version **independently** of core — a satellite release never bumps `haechi`, and vice versa.
|
|
111
111
|
|
|
112
112
|
- **Pre-1.0:** satellites follow npm semver where a `0.x` **minor** bump may carry breaking changes; pin `major.minor` (e.g. `haechi-crypto-kms@~0.2`). Each is pre-stable until its own `1.0.0`.
|
|
113
|
-
- **Core compatibility** is expressed as a `peerDependencies` range
|
|
113
|
+
- **Core compatibility** is expressed as a `peerDependencies` range — a satellite reuses the consumer's single installed `haechi`, so there is one crypto/identity surface. Most 1.x satellites keep `"haechi": ">=0.8.0 <2.0.0"`, but `haechi-crypto-kms@0.3.0` requires `"haechi": ">=1.7.0 <2.0.0"` because it imports core's `canonicalizeCryptoAad` export. `haechi-auth-oidc` additionally peer-depends on `haechi-auth-jwt` (`">=0.3.0 <2.0.0"`) so the two share one audited JWS/JWKS verification path. The satellite `haechi` peer-dependency **upper bound must track the core MAJOR** (`<2.0.0`), never be pinned below the next minor, so a core minor- or major-compatible bump never breaks satellite installs; the `release:preflight` gate (`scripts/check-satellite-peer-ranges.mjs`) enforces this automatically.
|
|
114
114
|
- **Heavy backends are optional peers.** `haechi-crypto-kms` declares its SDK backends (`@aws-sdk/client-kms`, and in 0.2.0 `@google-cloud/kms`, `@azure/keyvault-keys`, `@azure/identity`) under `peerDependencies` + `peerDependenciesMeta.optional` and imports them lazily, so consumers who do not use a given path never install it and core stays zero-dependency. The `./vault` backend uses `node:` `fetch` only (no optional peer). A satellite's published tarball always declares **zero runtime `dependencies`** (CI-gated by `check-satellite-packaging`).
|
|
115
115
|
- **Pre-1.0 satellite exports** are preview and may change before each satellite's own `1.0.0`:
|
|
116
|
-
- `haechi-crypto-kms` (0.8 → 0.
|
|
116
|
+
- `haechi-crypto-kms` (0.8 → 0.3.0): `createKmsCryptoProvider`, `createInMemoryKms`, the `./aws` `createAwsKmsClient`, the 0.2.0 subpaths `./gcp` (`createGcpKmsClient`), `./azure` (`createAzureKmsClient`), `./vault` (`createVaultKmsClient`), and 0.3.0 v2 crypto-AAD parity with core (`haechi >=1.7.0` peer floor).
|
|
117
117
|
- `haechi-auth-jwt` (0.2.0): `createJwtAuthProvider` (0.8, behavior-preserving) plus the additive `createJwtVerifier` (reusable JWS verifier primitive) and `isBlockedAddress` (SSRF range predicate, reused by `haechi-auth-oidc`).
|
|
118
118
|
- `haechi-dashboard` (0.1.0, new): `createDashboardServer`, `normalizeDashboardConfig`.
|
|
119
119
|
- `haechi-auth-oidc` (0.1.0, new): `createOidcSessionBroker`, `normalizeOidcConfig`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi `configVersion` & 업그레이드 노트
|
|
2
2
|
|
|
3
|
-
- 상태: Living document (코어 1.
|
|
3
|
+
- 상태: Living document (코어 1.7.x 추적)
|
|
4
4
|
|
|
5
5
|
`configVersion`는 `haechi.config.json`(및 `haechi.config.example.json`) 최상위에 찍히는 단일 정수입니다. 향후 호환성을 깨는 설정 스키마 변경이 구체적으로 게이트할 수 있는 **버전 앵커**로서, 다른 Haechi 빌드가 쓴 설정을 조용히 잘못 읽는 일을 막습니다.
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
| `configVersion` | 코어 라인 | 노트 |
|
|
23
23
|
|---|---|---|
|
|
24
|
-
| `1` | 1.0 – 1.
|
|
24
|
+
| `1` | 1.0 – 1.7.x | 최초 스탬프. 모든 키는 1.0 frozen 설정 표면(`api-stability.md` §2.4)에 대해 additive입니다. 1.1.x의 additive 키(`logging`, `metrics`, WS4-B의 `limits.maxInFlight` / `limits.shutdownGraceMs` / `limits.requestTimeoutMs` / `limits.headersTimeoutMs`, 그리고 `configVersion` 자체)와 1.2.0 신뢰성 강화 키(`filters.minConfidence` / `filters.allowlist`, `proxy.tls` / `proxy.trustForwardedProto`)는 모두 이전 동작을 기본값으로 합니다. 1.3.0의 추가는 새 키가 아니라 새 *값*입니다 — `target.type`의 `anthropic`/`gemini`, 추가 탐지 타입, `asia-pdpa`/`jp-appi` `privacy.profile` 값입니다. 1.4.0 plugin-signing CLI, 1.5.0 store 시임, 1.6.0 nonce-budget 가시성, 1.7.0 v2 crypto-AAD/freshness 변경은 모두 CLI/API/envelope 동작이며 config key가 아닙니다. 따라서 설정 스키마(및 `configVersion`)는 변경되지 않습니다. 마이그레이션 불필요. |
|
|
25
25
|
|
|
26
26
|
## 업그레이드
|
|
27
27
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi `configVersion` & Upgrade Notes
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
3
|
+
- Status: Living document (tracks core 1.7.x)
|
|
4
4
|
|
|
5
5
|
`configVersion` is a single integer stamped at the top of `haechi.config.json`
|
|
6
6
|
(and `haechi.config.example.json`). It is a **versioned anchor** so a future
|
|
@@ -34,7 +34,7 @@ the "policies only get stronger / fail closed" invariant intact.
|
|
|
34
34
|
|
|
35
35
|
| `configVersion` | Core line | Notes |
|
|
36
36
|
|---|---|---|
|
|
37
|
-
| `1` | 1.0 – 1.
|
|
37
|
+
| `1` | 1.0 – 1.7.x | Initial stamp. All keys are additive over the 1.0 frozen config surface (`api-stability.md` §2.4). The 1.1.x additive keys (`logging`, `metrics`, the WS4-B `limits.maxInFlight` / `limits.shutdownGraceMs` / `limits.requestTimeoutMs` / `limits.headersTimeoutMs`, `configVersion` itself) and the 1.2.0 Reliability-Hardening keys (`filters.minConfidence` / `filters.allowlist`, `proxy.tls` / `proxy.trustForwardedProto`) all default to prior behavior. The 1.3.0 additions are new *values*, not new keys — `target.type` `anthropic`/`gemini`, additional detection types, and the `asia-pdpa`/`jp-appi` `privacy.profile` values. The 1.4.0 plugin-signing CLI (`plugin-keygen`/`plugin-sign`/`plugin-verify`) is **CLI surface, not config**. The 1.5.0 store seams (`createAuditSink`/`createTokenVault` + the file-store defaults) are an **injected-provider** surface. The 1.6.0 nonce-budget visibility and 1.7.0 v2 crypto-AAD/freshness changes are crypto envelope/API behavior, not config keys. So the config schema (and `configVersion`) is unchanged. No migration needed. |
|
|
38
38
|
|
|
39
39
|
## Upgrading
|
|
40
40
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi 설정 레퍼런스
|
|
2
2
|
|
|
3
|
-
- 문서 상태: Living document(core 1.
|
|
3
|
+
- 문서 상태: Living document(core 1.7.x 추적)
|
|
4
4
|
|
|
5
5
|
`haechi init`은 `haechi.config.json`을 생성하며, 비밀 정보를 포함하지 않는 템플릿은 `haechi.config.example.json`에 있습니다. 모든 커맨드는 `--config <path>`로 설정 파일을 읽습니다(기본값: `haechi.config.json`). 설정은 **fail-closed 방식으로 검증**됩니다. 알 수 없는 provider, 범위를 벗어난 숫자, 잘못된 형식의 값은 자동으로 무시되지 않고 로드 시점에 오류를 발생시킵니다. `haechi config`는 이 레퍼런스를 출력하며, `haechi status`는 특정 설정 파일의 *실제 적용* 상태를 출력합니다.
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi Configuration Reference
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
3
|
+
- Status: Living document (tracks core 1.7.x)
|
|
4
4
|
|
|
5
5
|
`haechi init` writes `haechi.config.json`; a non-secret template is at `haechi.config.example.json`. Every command reads it with `--config <path>` (default `haechi.config.json`). Configuration is **validated fail-closed**: unknown providers, out-of-range numbers, and malformed values throw at load time rather than degrading silently. `haechi config` prints this reference; `haechi status` prints the *effective* state of a given config.
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi Operations Runbook (Day-2)
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
3
|
+
- Status: Living document (tracks core 1.7.x)
|
|
4
4
|
|
|
5
5
|
A practical guide to running Haechi in production: deploy, configure via the
|
|
6
6
|
env-var overlay, monitor with health/readiness/metrics, shut down gracefully,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Haechi 리스크 레지스터 및 릴리스 게이트
|
|
2
2
|
|
|
3
|
-
- 문서 상태: Living document(core 1.
|
|
4
|
-
- 작성일: 2026-06-
|
|
5
|
-
- 기준 버전: 1.
|
|
6
|
-
- 기준 브랜치: `
|
|
3
|
+
- 문서 상태: Living document(core 1.7.x 추적)
|
|
4
|
+
- 작성일: 2026-06-23
|
|
5
|
+
- 기준 버전: 1.7.x
|
|
6
|
+
- 기준 브랜치: `irae/prepare-1.7.0`
|
|
7
7
|
|
|
8
8
|
## 1. 현재 판단
|
|
9
9
|
|
|
@@ -14,9 +14,9 @@ Haechi는 `1.x` stable 라인을 출시했습니다. developer preview 게이트
|
|
|
14
14
|
| 구분 | 판단 | 이유 |
|
|
15
15
|
|---|---|---|
|
|
16
16
|
| GitHub public | 허용 | 보안 한계, threat model, shared responsibility가 문서화됨 |
|
|
17
|
-
| GitHub release/tag | `v1.
|
|
18
|
-
| npm stable | `haechi@1.6.0`
|
|
19
|
-
| production use | 운영자 게이트; `1.
|
|
17
|
+
| GitHub release/tag | `v1.7.0` 컷 준비됨 (태그 → attested publish) | `v1.7.0`은 배포 완료된 `v1.6.0` 위의 additive minor입니다. crypto envelope가 versioned NFKC AAD(`nfkc-json-v2`)를 사용하고, legacy `v1` envelope는 계속 복호화되며, token-vault ciphertext에는 envelope freshness가 적용됩니다. G9–G14는 최종 release preflight 전 기준 Pass |
|
|
18
|
+
| npm stable | `haechi@1.6.0` 배포 완료; 다음 목표 `haechi@1.7.0` | `1.7.0`은 frozen `haechi/crypto` export(`canonicalizeCryptoAad`, `CRYPTO_AAD_ENCODING_V2`)를 추가하고 `haechi-crypto-kms@0.3.0`도 같은 v2 AAD helper로 맞춥니다. additive export만, config/API 파괴 없음(`configVersion`은 `1` 유지) |
|
|
19
|
+
| production use | 운영자 게이트; `1.7.0` 업그레이드 준비 | 운영자 네트워크 통제, 인가/인증, key custody가 있을 때만 지원; 여러 replica를 운영하는 운영자는 공유 저장소(`haechi-store-redis` 위성)를 주입해 audit 해시 체인과 token vault가 플릿 전체에서 유지되도록 해야 함 |
|
|
20
20
|
|
|
21
21
|
## 2. 릴리스 게이트
|
|
22
22
|
|
|
@@ -35,7 +35,8 @@ Haechi는 `1.x` stable 라인을 출시했습니다. developer preview 게이트
|
|
|
35
35
|
| G10 | 2026-06-16 코드리뷰 round 2 (CR2) 보완 게이트 | CR2 등록부(`code-review-risk-register-2026-06-16-round2.md`, §5.8)는 **P0/P1을 발견하지 못했습니다**; 세 개의 P2(`CR2-001` 프록시 upstream-cancel, `CR2-002` token-vault audit hygiene, `CR2-003` plugin IPC reply 경계)와 P3 묶음(`CR2-004..008`)이 모두 **Resolved이며 `haechi@1.3.2`로 발행되었고**(`CR2-009` won't-fix, `CR2-010` accepted) 연결된 등록부가 갱신되었습니다. | Pass (`haechi@1.3.2`, 2026-06-16) |
|
|
36
36
|
| G11 | 1.4.0 signed-plugin 저작 CLI | 1.0 Ed25519 trust gate를 위한 1차 저작 CLI — `plugin-keygen`(개인키 `0600`, 공개키 = trust anchor), `plugin-sign`(정확한 entry 바이트 바인딩), `plugin-verify`(런타임 동등 검증, fail-closed, `--allow-capability`); 개인키가 stdout/audit로 유출되지 않음; 적대적 검증 완료; `plugin-signing-and-trust.md` 큐레이션 런북이 P1-SEC-025 "운영자가 앵커를 큐레이션해야 함" 잔여를 해소. additive CLI 표면(config/API 파괴 없음, `configVersion`은 `1` 유지); `tests/api-contract.test.mjs` green; 코어는 zero runtime dependency 유지; 코어 1.3.3 → 1.4.0(additive minor)로 bump. | Pass (`haechi@1.4.0`, 2026-06-17) |
|
|
37
37
|
| G12 | 1.5.0 수평 확장 저장소 시임 | audit sink와 token vault가 주입 가능한 **store**를 갖게 되어, 공유 저장소가 sha256 해시 체인 + token vault를 replica 전반에서 뒷받침할 수 있음(프로세스별 / 단일 writer 플릿 한계를 해소). `createAuditSink({store})` / `createTokenVault({store})` + 기본 `createFileAuditStore`/`createFileTokenStore`; 보안에 결정적인 chaining / `sanitizeAudit` / reveal governance / retention은 코어에 남고, store는 배타적 read-previous+persist(audit) / mutate+read(vault) 프리미티브만 추상화. 적대적 검증 완료: 파일 기본값 바이트 동일, chain 연산은 이전과 diff 동일, 비파일 store에서도 시임 동작, 동시 append/tokenize가 비분기·무손실 유지, CR2-002 audit-no-plaintext 유지. 새 export는 `api-stability.md` + `tests/api-contract.test.mjs`에 frozen; `createJsonlAuditSink`/`createLocalTokenVault`는 하위호환 래퍼; 코어는 zero runtime dependency 유지; 코어 1.4.0 → 1.5.0(additive minor)로 bump. `haechi-store-redis` 위성이 운영 소비자. | Pass (`haechi@1.5.0`, 2026-06-17) |
|
|
38
|
-
| G13 | 1.6.0 crypto 봉투 무결성 — nonce budget + 보안 CI 게이트 | 스코핑 결과 gap review의 "AAD canonicalization"(GAP-P0-001)은
|
|
38
|
+
| G13 | 1.6.0 crypto 봉투 무결성 — nonce budget + 보안 CI 게이트 | 스코핑 결과 gap review의 "AAD canonicalization"(GAP-P0-001)은 sorted canonical JSON으로 부분 해소되어 있었고, nonce 절반(GAP-P0-002)을 닫음: 로컬 AES-256-GCM provider가 `kid`별 암호화 횟수를 세어 미리 예약한 윈도우 단위로 키 파일에 영속화(소비 전 기록 → 재시작은 과대집계, 재사용으로의 과소집계 불가)하고, 50%에서 1회 경고, **2^32에서 fail-closed**(NIST SP 800-38D §8.3)하며 `init --force` 회전을 안내. 읽기 전용 키 파일은 프로세스 단위 한도로 degrade(경고 `HAECHI_NONCE_BUDGET_NOPERSIST`); 다중 프로세스 공유는 범위 밖 잔여(단일 writer 레퍼런스 provider; 운영 custody = KMS). 운영자 가시성은 frozen `haechi/crypto` `readNonceBudget` export + `haechi status`(`keys.nonceBudget` 사용%). 또한 GAP-P0-012로 명명된 `gate:security` CI 잡이 횡단 보안 invariant를 검사. core 1.5.0 → 1.6.0(additive minor); zero runtime dependency; `configVersion`은 `1` 유지. | Pass (`haechi@1.6.0`, 2026-06-18) |
|
|
39
|
+
| G14 | 1.7.0 crypto envelope v2 — NFKC AAD + freshness + KMS parity | 남은 crypto-envelope canonicalization/freshness 절반을 닫음: 새 local-provider envelope는 `v:2`, `aadEncoding:"nfkc-json-v2"`, `createdAt`, optional `expiresAt`를 사용합니다. decrypt는 envelope 버전에 따라 canonicalization을 선택하므로 legacy `v1`/무표기 envelope는 계속 읽을 수 있고, v2는 `canonicalizeCryptoAad()`(정렬 canonical JSON + NFKC-normalized string value/object key)를 해시합니다. 같은 object level에서 NFKC key collision이 나면 두 AAD binding을 조용히 합치지 않고 fail-closed합니다. token-vault ciphertext는 token `expiresAt`를 envelope에 전달해 vault retention check에 더해 crypto 계층에서도 stale ciphertext를 거부합니다. `haechi-crypto-kms@0.3.0`은 동일 helper를 import하고 core peer floor를 `>=1.7.0 <2.0.0`으로 올립니다. NFKC AAD cross-implementation parity 테스트 포함. Additive frozen exports: `canonicalizeCryptoAad`, `CRYPTO_AAD_ENCODING_V2`; `configVersion`은 `1` 유지; core는 zero runtime dependency 유지. 잔여: stream sequence AAD / replay cache는 streaming encryption이 아직 독립 복호화 가능한 stream envelope를 내보내지 않으므로 후속으로 둡니다. | Pass (컷 준비됨; final preflight pending) |
|
|
39
40
|
|
|
40
41
|
## 3. P0 배포 차단 리스크 상태
|
|
41
42
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Haechi Risk Register and Release Gates
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
4
|
-
- Date: 2026-06-
|
|
5
|
-
- Target version: 1.
|
|
6
|
-
- Branch: `
|
|
3
|
+
- Status: Living document (tracks core 1.7.x)
|
|
4
|
+
- Date: 2026-06-23
|
|
5
|
+
- Target version: 1.7.x
|
|
6
|
+
- Branch: `irae/prepare-1.7.0`
|
|
7
7
|
|
|
8
8
|
## 1. Current Assessment
|
|
9
9
|
|
|
@@ -14,9 +14,9 @@ Haechi has shipped its `1.x` stable line. The developer-preview gate (G2, `haech
|
|
|
14
14
|
| Category | Judgment | Rationale |
|
|
15
15
|
|---|---|---|
|
|
16
16
|
| GitHub public | Allowed | Security limitations, threat model, and shared responsibility are documented |
|
|
17
|
-
| GitHub release/tag | `v1.
|
|
18
|
-
| npm stable | `haechi@1.6.0`
|
|
19
|
-
| Production use | Operator-gated; upgrade to `1.
|
|
17
|
+
| GitHub release/tag | `v1.7.0` cut prepared (tag → attested publish) | `v1.7.0` is an additive minor over the shipped `v1.6.0`: crypto envelopes now use versioned NFKC AAD (`nfkc-json-v2`), legacy `v1` envelopes remain decryptable, and envelope freshness is enforced for token-vault ciphertext. G9–G14 are Pass pending final release preflight |
|
|
18
|
+
| npm stable | `haechi@1.6.0` published; next target `haechi@1.7.0` | `1.7.0` adds frozen `haechi/crypto` exports (`canonicalizeCryptoAad`, `CRYPTO_AAD_ENCODING_V2`) and updates `haechi-crypto-kms@0.3.0` to the same v2 AAD helper; additive export only, no config/API break (`configVersion` stays `1`) |
|
|
19
|
+
| Production use | Operator-gated; prepare upgrade to `1.7.0` | Supported only with operator network controls, authz/authn, and key custody; operators running multiple replicas should inject a shared store (the `haechi-store-redis` satellite) so the audit hash chain and token vault hold across the fleet |
|
|
20
20
|
|
|
21
21
|
## 2. Release Gates
|
|
22
22
|
|
|
@@ -35,7 +35,8 @@ Haechi has shipped its `1.x` stable line. The developer-preview gate (G2, `haech
|
|
|
35
35
|
| G10 | 2026-06-16 code-review round 2 (CR2) remediation gate | The CR2 register (`code-review-risk-register-2026-06-16-round2.md`, §5.8) found **no P0/P1**; its three P2s (`CR2-001` proxy upstream-cancel, `CR2-002` token-vault audit hygiene, `CR2-003` plugin IPC reply bound) plus the P3 cluster (`CR2-004..008`) are all **Resolved and shipped in `haechi@1.3.2`** (`CR2-009` won't-fix, `CR2-010` accepted) and the linked register is updated. | Pass (`haechi@1.3.2`, 2026-06-16) |
|
|
36
36
|
| G11 | 1.4.0 signed-plugin authoring CLI | First-party CLI for the 1.0 Ed25519 trust gate — `plugin-keygen` (private key `0600`, public key = trust anchor), `plugin-sign` (binds the exact entry bytes), `plugin-verify` (runtime-equivalent verification, fail-closed, `--allow-capability`); no private-key leak to stdout/audit; adversarially verified; the `plugin-signing-and-trust.md` curation runbook closes the P1-SEC-025 "operator must curate anchors" residual. Additive CLI surface (no config/API break, `configVersion` stays `1`); `tests/api-contract.test.mjs` green; core stays zero runtime dependency; core bumped 1.3.3 → 1.4.0 (additive minor). | Pass (`haechi@1.4.0`, 2026-06-17) |
|
|
37
37
|
| G12 | 1.5.0 horizontal-scale store seams | The audit sink and token vault gain an injectable **store** so a shared store can back the sha256 hash chain + token vault across replicas (closes the per-process / single-writer fleet limitations). `createAuditSink({store})` / `createTokenVault({store})` + the default `createFileAuditStore`/`createFileTokenStore`; the security-critical chaining / `sanitizeAudit` / reveal governance / retention stay in core, the store only abstracts the exclusive read-previous+persist (audit) / mutate+read (vault) primitives. Adversarially verified: file defaults byte-identical, chain math diff-identical to prior, the seam works for a non-file store, concurrent appends/tokenize stay non-forked/lossless, CR2-002 audit-no-plaintext intact. New exports frozen in `api-stability.md` + `tests/api-contract.test.mjs`; `createJsonlAuditSink`/`createLocalTokenVault` are back-compat wrappers; core stays zero runtime dependency; core bumped 1.4.0 → 1.5.0 (additive minor). The `haechi-store-redis` satellite is the production consumer. | Pass (`haechi@1.5.0`, 2026-06-17) |
|
|
38
|
-
| G13 | 1.6.0 crypto envelope integrity — nonce budget + security CI gate | Scoping confirmed the gap review's "AAD canonicalization" (GAP-P0-001) was
|
|
38
|
+
| G13 | 1.6.0 crypto envelope integrity — nonce budget + security CI gate | Scoping confirmed the gap review's "AAD canonicalization" (GAP-P0-001) was partially closed by sorted canonical JSON, and closed the nonce half (GAP-P0-002): the local AES-256-GCM provider counts encryptions per `kid`, persists the count to the key file in pre-reserved windows (written before consumption → restart over-counts, never under-counts into reuse), warns once at 50%, and **fails closed at 2^32** (NIST SP 800-38D §8.3) instructing `init --force`. A read-only key file degrades to a per-process limit (warned, `HAECHI_NONCE_BUDGET_NOPERSIST`); multi-process sharing is an out-of-scope residual (single-writer reference provider; prod custody = KMS). Operator visibility via the frozen `haechi/crypto` `readNonceBudget` export + `haechi status` (`keys.nonceBudget` used%). Also adds GAP-P0-012: a named, branch-protectable `gate:security` CI job (`scripts/security-gate.mjs`) over cross-cutting security invariants. Core bumped 1.5.0 → 1.6.0 (additive minor); zero runtime dependency; `configVersion` stays `1`. | Pass (`haechi@1.6.0`, 2026-06-18) |
|
|
39
|
+
| G14 | 1.7.0 crypto envelope v2 — NFKC AAD + freshness + KMS parity | Closes the remaining crypto-envelope canonicalization/freshness half: new local-provider envelopes use `v:2`, `aadEncoding:"nfkc-json-v2"`, `createdAt`, and an optional `expiresAt`; decrypt chooses canonicalization by envelope version so legacy `v1`/unencoded envelopes remain readable, while v2 hashes `canonicalizeCryptoAad()` (sorted canonical JSON with NFKC-normalized string values and object keys). NFKC key collisions at one object level fail closed rather than collapse two AAD bindings. Token-vault ciphertext passes the token `expiresAt` into the envelope so stale vault ciphertext is rejected by crypto as defense-in-depth in addition to vault retention checks. `haechi-crypto-kms@0.3.0` imports the same helper and raises its core peer floor to `>=1.7.0 <2.0.0`; cross-implementation parity is tested against NFKC AAD. Additive frozen exports: `canonicalizeCryptoAad`, `CRYPTO_AAD_ENCODING_V2`; `configVersion` stays `1`; core stays zero runtime dependency. Remaining residual: stream sequence AAD / replay cache for encrypted stream fragments remains deferred because streaming encryption still emits transformed text, not independently decryptable stream envelopes. | Pass (cut prepared; final preflight pending) |
|
|
39
40
|
|
|
40
41
|
## 3. P0 Distribution-Blocking Risk Status
|
|
41
42
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi Threat Model
|
|
2
2
|
|
|
3
|
-
- 문서 상태: Living document(core 1.
|
|
3
|
+
- 문서 상태: Living document(core 1.7.x 추적)
|
|
4
4
|
- 작성일: 2026-06-10
|
|
5
5
|
|
|
6
6
|
## 1. 보호 대상
|
|
@@ -13,7 +13,7 @@ Haechi가 보호하려는 주요 자산은 다음과 같습니다.
|
|
|
13
13
|
| Tool/resource result | MCP result, local inference response | 응답 내 PII/secret 재유출 차단 |
|
|
14
14
|
| TokenVault record | tokenized PII mapping | 저장 시 암호화, reveal 기본 차단 |
|
|
15
15
|
| Audit event | detection metadata, decision summary | 평문 비포함, hash chain 무결성 |
|
|
16
|
-
| Crypto envelope | encrypted segments |
|
|
16
|
+
| Crypto envelope | encrypted segments | versioned NFKC AAD binding, 제공된 경우 freshness, key provider 교체성 |
|
|
17
17
|
| Plugin manifest | custom provider/filter declaration | capability disclosure, dynamic runtime 차단 |
|
|
18
18
|
|
|
19
19
|
## 2. 신뢰 경계
|
|
@@ -55,6 +55,7 @@ Haechi가 보호하려는 주요 자산은 다음과 같습니다.
|
|
|
55
55
|
| Audit tail truncation | 꼬리 audit 레코드의 무음 삭제 | 추가 전용/별도 미디어의 `audit.anchor` head-hash anchoring으로 마지막 anchor까지의 절단 탐지 (0.7) |
|
|
56
56
|
| Local dev key in production | 소프트웨어 키의 운영 custody 오용 | `assertCryptoProviderConformance`를 통한 외부 `cryptoProvider` 주입; reference KMS adapter (envelope 암호화) |
|
|
57
57
|
| 단일 키의 GCM nonce 고갈 | 로컬 AES-256-GCM provider는 랜덤 96-bit IV를 쓰며, 한 키로 ~2^32회 암호화를 넘기면 birthday bound로 IV 충돌(GCM에 치명적 — 평문 XOR 누출 + 위조 가능) 확률이 무시할 수 없게 됨 | 로컬 provider는 **키당 2^32회 암호화에서 fail-closed**(NIST SP 800-38D §8.3) — 암호화를 거부하고 `haechi init --force` 회전을 안내. 호출 수는 kid별로 카운트되어 미리 예약한 윈도우 단위로 키 파일에 영속화되므로 재시작을 넘겨도 유지됨(과대집계는 가능, 재사용으로의 과소집계는 불가). 50%에서 1회 경고. **수용된 잔여 위험:** 읽기 전용 키 파일은 **프로세스 단위** 한도로 degrade(경고 `HAECHI_NONCE_BUDGET_NOPERSIST`)되고, 하나의 키 파일을 여러 프로세스가 공유하는 경우는 범위 밖(로컬 provider는 단일 writer 레퍼런스이며, 운영 custody는 자체 nonce 규율을 갖는 KMS 위성 사용) |
|
|
58
|
+
| Unicode AAD spoofing 또는 stale ciphertext replay | full-width key/value, compatibility 문자 등 시각적으로 동등한 Unicode AAD로 복호화 context를 흔들거나, retention 이후에도 stale token-vault ciphertext가 복호화될 수 있음 | 새 crypto envelope는 `v:2`, `aadEncoding:"nfkc-json-v2"`를 사용합니다. `canonicalizeCryptoAad()`가 string value와 object key를 NFKC 정규화한 뒤 정렬 canonical JSON으로 해시하며, legacy v1 envelope는 하위호환을 위해 기존 canonicalization으로 계속 복호화됩니다. 같은 object level의 NFKC key collision은 조용히 합치지 않고 fail-closed합니다. Envelope는 `expiresAt`를 가질 수 있고 local/KMS provider가 만료 envelope를 거부합니다. Token-vault ciphertext는 token `expiresAt`를 envelope에 묶어 vault retention check에 더한 방어층을 둡니다. **잔여:** streaming transform은 아직 독립 복호화 가능한 stream envelope를 만들지 않으므로 stream sequence AAD / replay cache는 후속입니다 |
|
|
58
59
|
| Tampered release artifact | 변조된 tarball 설치 | npm provenance + GitHub release tarball의 sigstore attestation + `SHA256SUMS` (0.7) |
|
|
59
60
|
| audit에 원시 credentials/identity 노출 | audit 로그를 통한 token 또는 subject 유출 | Token은 keyed-HMAC 해시로만 저장; identity subject/issuer는 keyed HMAC 처리; `auth_denied` 레코드에 token 미포함 |
|
|
60
61
|
| token round-trip의 타 토큰 복원 | 클라이언트/요청 간 평문 복구 | detokenization은 opt-in(`detokenizeResponses`)이며 요청 스코프: 같은 요청을 보호하며 발급된 토큰만 복원 |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi Threat Model
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
3
|
+
- Status: Living document (tracks core 1.7.x)
|
|
4
4
|
- Date: 2026-06-10
|
|
5
5
|
|
|
6
6
|
## 1. Assets Under Protection
|
|
@@ -13,7 +13,7 @@ The primary assets Haechi protects are:
|
|
|
13
13
|
| Tool/resource result | MCP result, local inference response | Prevent re-leakage of PII/secrets in responses |
|
|
14
14
|
| TokenVault record | tokenized PII mapping | Encrypted at rest, reveal blocked by default |
|
|
15
15
|
| Audit event | detection metadata, decision summary | No plaintext content, hash chain integrity |
|
|
16
|
-
| Crypto envelope | encrypted segments |
|
|
16
|
+
| Crypto envelope | encrypted segments | Versioned NFKC AAD binding, freshness where supplied, swappable key provider |
|
|
17
17
|
| Plugin manifest | custom provider/filter declaration | Capability disclosure, dynamic runtime blocked |
|
|
18
18
|
|
|
19
19
|
## 2. Trust Boundaries
|
|
@@ -55,6 +55,7 @@ The primary assets Haechi protects are:
|
|
|
55
55
|
| Audit tail truncation | Silent deletion of trailing audit records | `audit.anchor` head-hash anchoring on append-only/separate media detects truncation back to the last anchor (0.7) |
|
|
56
56
|
| Local dev key in production | Software key misused as production custody | External `cryptoProvider` injection with `assertCryptoProviderConformance`; reference KMS adapter (envelope encryption) |
|
|
57
57
|
| GCM nonce exhaustion under one key | The local AES-256-GCM provider uses random 96-bit IVs; past ~2^32 encryptions under one key the birthday bound makes an IV collision (catastrophic for GCM — leaks plaintext XOR + enables forgery) non-negligible | The local provider **fails closed at 2^32 encryptions per key** (NIST SP 800-38D §8.3) — it refuses to encrypt and instructs `haechi init --force` to rotate. Invocations are counted per-kid, persisted to the key file in pre-reserved windows so the count survives restarts (over-counts, never under-counts into reuse); a one-time warning fires at 50%. **Accepted residual:** a read-only key file degrades to a per-PROCESS limit (warned, `HAECHI_NONCE_BUDGET_NOPERSIST`) and multiple processes sharing one key file are out of scope (the local provider is the single-writer reference; production custody uses a KMS satellite that owns its own nonce discipline) |
|
|
58
|
+
| Unicode AAD spoofing or stale ciphertext replay | A caller uses visually equivalent Unicode in AAD (full-width keys/values, compatibility characters) to destabilize decryption context, or a stale token-vault ciphertext remains decryptable after retention | New crypto envelopes are `v:2` with `aadEncoding:"nfkc-json-v2"`: `canonicalizeCryptoAad()` NFKC-normalizes string values and object keys before sorted canonical JSON hashing, while legacy v1 envelopes keep the old canonicalization for backward decrypt. NFKC key collisions at one object level fail closed. Envelopes may carry `expiresAt`; local and KMS providers reject expired envelopes. Token-vault ciphertext now binds its token `expiresAt` into the envelope as defense-in-depth. **Residual:** stream sequence AAD / replay cache is still deferred because streaming transforms do not create independently decryptable stream envelopes |
|
|
58
59
|
| Tampered release artifact | Modified tarball installed | npm provenance + sigstore attestation of the GitHub release tarball + `SHA256SUMS` (0.7) |
|
|
59
60
|
| Raw credentials/identity in audit | Token or subject leak through the audit log | Tokens stored only as keyed-HMAC hashes; identity subject/issuer are keyed HMAC; `auth_denied` records no token |
|
|
60
61
|
| Token round-trip restoring foreign tokens | Cross-client/request plaintext recovery | Detokenization is opt-in (`detokenizeResponses`) and request-scoped: only tokens issued while protecting the same request are restored |
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname } from "node:path";
|
|
|
3
3
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
|
|
5
5
|
const ALG = "AES-256-GCM";
|
|
6
|
+
export const CRYPTO_AAD_ENCODING_V2 = "nfkc-json-v2";
|
|
6
7
|
|
|
7
8
|
// Random 96-bit GCM IVs are only safe up to a bounded number of invocations per
|
|
8
9
|
// key: by the birthday bound the IV-collision probability stays negligible only
|
|
@@ -151,26 +152,32 @@ export function createLocalCryptoProvider({ keyFile }) {
|
|
|
151
152
|
readsPlaintext: true,
|
|
152
153
|
networkEgress: false
|
|
153
154
|
},
|
|
154
|
-
async encrypt({ plaintext, aad }) {
|
|
155
|
+
async encrypt({ plaintext, aad, expiresAt = null }) {
|
|
155
156
|
const { active: { kid, key } } = await loadKeys();
|
|
156
157
|
// Fail closed at the per-key random-IV invocation limit BEFORE choosing an
|
|
157
158
|
// IV, so we never generate a nonce past the safe budget (NIST SP 800-38D).
|
|
158
159
|
await consumeNonceBudget(kid);
|
|
159
160
|
const iv = randomBytes(12);
|
|
160
161
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
161
|
-
const aadBytes = Buffer.from(
|
|
162
|
+
const aadBytes = Buffer.from(canonicalizeCryptoAad(aad), "utf8");
|
|
162
163
|
cipher.setAAD(aadBytes);
|
|
163
164
|
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
164
165
|
const tag = cipher.getAuthTag();
|
|
165
|
-
|
|
166
|
-
v:
|
|
166
|
+
const envelope = {
|
|
167
|
+
v: 2,
|
|
167
168
|
alg: ALG,
|
|
168
169
|
kid,
|
|
169
170
|
iv: iv.toString("base64url"),
|
|
170
171
|
ct: ciphertext.toString("base64url"),
|
|
171
172
|
tag: tag.toString("base64url"),
|
|
172
|
-
aadHash: sha256(aadBytes)
|
|
173
|
+
aadHash: sha256(aadBytes),
|
|
174
|
+
aadEncoding: CRYPTO_AAD_ENCODING_V2,
|
|
175
|
+
createdAt: new Date().toISOString()
|
|
173
176
|
};
|
|
177
|
+
if (expiresAt !== null && expiresAt !== undefined) {
|
|
178
|
+
envelope.expiresAt = normalizeEnvelopeExpiry(expiresAt);
|
|
179
|
+
}
|
|
180
|
+
return envelope;
|
|
174
181
|
},
|
|
175
182
|
// Keyed hash over a domain-separated derived key. The raw stored key is an
|
|
176
183
|
// AES-256-GCM key and must never be used for HMAC directly; every use case
|
|
@@ -189,12 +196,13 @@ export function createLocalCryptoProvider({ keyFile }) {
|
|
|
189
196
|
if (envelope.alg && envelope.alg !== ALG) {
|
|
190
197
|
throw new Error(`Unsupported local crypto algorithm: ${envelope.alg}`);
|
|
191
198
|
}
|
|
199
|
+
assertEnvelopeFresh(envelope);
|
|
192
200
|
const selected = envelope.kid ? byKid.get(envelope.kid) : active;
|
|
193
201
|
if (!selected) {
|
|
194
202
|
throw new Error(`Unknown key id in envelope: ${envelope.kid}`);
|
|
195
203
|
}
|
|
196
204
|
const { key } = selected;
|
|
197
|
-
const aadBytes = Buffer.from(
|
|
205
|
+
const aadBytes = Buffer.from(canonicalizeAadForEnvelope(envelope, aad), "utf8");
|
|
198
206
|
if (envelope.aadHash && envelope.aadHash !== sha256(aadBytes)) {
|
|
199
207
|
throw new Error("AAD hash mismatch");
|
|
200
208
|
}
|
|
@@ -397,6 +405,61 @@ export function canonicalize(value) {
|
|
|
397
405
|
return JSON.stringify(value);
|
|
398
406
|
}
|
|
399
407
|
|
|
408
|
+
export function canonicalizeCryptoAad(value) {
|
|
409
|
+
if (Array.isArray(value)) {
|
|
410
|
+
return `[${value.map((item) => canonicalizeCryptoAad(item)).join(",")}]`;
|
|
411
|
+
}
|
|
412
|
+
if (value && typeof value === "object") {
|
|
413
|
+
const seen = new Set();
|
|
414
|
+
const entries = [];
|
|
415
|
+
for (const key of Object.keys(value)) {
|
|
416
|
+
const normalizedKey = key.normalize("NFKC");
|
|
417
|
+
if (seen.has(normalizedKey)) {
|
|
418
|
+
throw new Error(`crypto AAD NFKC key collision: ${JSON.stringify(normalizedKey)}`);
|
|
419
|
+
}
|
|
420
|
+
seen.add(normalizedKey);
|
|
421
|
+
entries.push(`${JSON.stringify(normalizedKey)}:${canonicalizeCryptoAad(value[key])}`);
|
|
422
|
+
}
|
|
423
|
+
return `{${entries.sort().join(",")}}`;
|
|
424
|
+
}
|
|
425
|
+
if (typeof value === "string") {
|
|
426
|
+
return JSON.stringify(value.normalize("NFKC"));
|
|
427
|
+
}
|
|
428
|
+
return JSON.stringify(value);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function canonicalizeAadForEnvelope(envelope, aad) {
|
|
432
|
+
if (envelope.aadEncoding && envelope.aadEncoding !== CRYPTO_AAD_ENCODING_V2) {
|
|
433
|
+
throw new Error(`Unsupported crypto AAD encoding: ${envelope.aadEncoding}`);
|
|
434
|
+
}
|
|
435
|
+
if (envelope.aadEncoding === CRYPTO_AAD_ENCODING_V2 || envelope.v === 2) {
|
|
436
|
+
return canonicalizeCryptoAad(aad);
|
|
437
|
+
}
|
|
438
|
+
return canonicalize(aad);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function normalizeEnvelopeExpiry(expiresAt) {
|
|
442
|
+
const iso = expiresAt instanceof Date ? expiresAt.toISOString() : String(expiresAt);
|
|
443
|
+
const ts = Date.parse(iso);
|
|
444
|
+
if (!Number.isFinite(ts)) {
|
|
445
|
+
throw new Error("crypto envelope expiresAt must be a valid timestamp");
|
|
446
|
+
}
|
|
447
|
+
return new Date(ts).toISOString();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function assertEnvelopeFresh(envelope) {
|
|
451
|
+
if (!envelope.expiresAt) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const expiresAt = Date.parse(envelope.expiresAt);
|
|
455
|
+
if (!Number.isFinite(expiresAt)) {
|
|
456
|
+
throw new Error("crypto envelope expiresAt is invalid");
|
|
457
|
+
}
|
|
458
|
+
if (Date.now() >= expiresAt) {
|
|
459
|
+
throw new Error("Crypto envelope expired");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
400
463
|
function sha256(value) {
|
|
401
464
|
return createHash("sha256").update(value).digest("base64url");
|
|
402
465
|
}
|
|
@@ -229,6 +229,7 @@ export function createTokenVault({
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
const createdAt = new Date();
|
|
232
|
+
const expiresAt = addDays(createdAt, retentionDays).toISOString();
|
|
232
233
|
const aad = {
|
|
233
234
|
purpose: "token-vault",
|
|
234
235
|
token,
|
|
@@ -238,9 +239,9 @@ export function createTokenVault({
|
|
|
238
239
|
view.set(token, {
|
|
239
240
|
type,
|
|
240
241
|
createdAt: createdAt.toISOString(),
|
|
241
|
-
expiresAt
|
|
242
|
+
expiresAt,
|
|
242
243
|
metadata: sanitizeMetadata(metadata),
|
|
243
|
-
envelope: await cryptoProvider.encrypt({ plaintext, aad }),
|
|
244
|
+
envelope: await cryptoProvider.encrypt({ plaintext, aad, expiresAt }),
|
|
244
245
|
aad
|
|
245
246
|
});
|
|
246
247
|
return { token, type };
|