Security: Compliance Mapping (Encryption & Key Management)
Document ID: SEC-007
File Path: docs/13-security/compliance-mapping.md
Version: 1.4.0
Status: Draft — first mapping pass, scoped to the encryption/key-management
control family (wovyr-kms, wovyr-secrets, wovyr-memory’s encrypting store,
wovyr-events’s encrypting webhook store, wovyr-audit). Verified against real
code and a new adversarial test suite, not a third-party attestation.
Owner: Security Team
Last Updated: 2026-07-09. §7 items 1, 2, and 3 (the anonymous default-tenant
bypass, kms.json file permissions, and the non-Unix root-key/kms.json ACL)
have since been closed — see their entries.
1. Purpose
Section titled “1. Purpose”13-security/index.md §7 states the platform is “designed to
support” SOC 2 / ISO 27001 / GDPR, with certification itself
deployment-dependent. That paragraph is a posture statement, not evidence.
This document is the first concrete slice of evidence: it maps specific
framework controls to the specific code that implements them, with file/line
citations and the test that proves each claim — for the encryption and
key-management control family only (Encryption §5,
wovyr-kms, and its three §4 consumers).
This is an internal self-assessment, not a certification, attestation, or substitute for a third-party audit. No external pen test or formal SOC 2/ISO 27001/GDPR audit has been performed against this codebase. Treat “Met” below as “the described control exists in code and is covered by an automated test that would fail if it regressed” — not as a compliance sign-off.
2. Scope & Method
Section titled “2. Scope & Method”In scope: encryption at rest (wovyr-kms, EncryptedFileSecretStore,
EncryptingMemoryStore, EncryptedFileWebhookStore), key lifecycle (rotation,
crypto-shredding), and the audit trail for key-management actions.
Out of scope (not mapped here): authentication/session management, network transport security (TLS/mTLS), sandbox/tool isolation, supply-chain signing — these have their own specs (authentication.md, security-isolation.md, distribution.md) and would need their own mapping pass.
Method: for each control, cite the implementing function/type, and the
test (unit, adversarial, or integration) that exercises it. Where the
platform’s own architecture note already states a limitation (e.g. wovyr-kms’s
LocalKms being a single-host stand-in for a real KMS/HSM), that limitation is
carried into the “Status” column rather than glossed over.
3. SOC 2 (Trust Services Criteria) Mapping
Section titled “3. SOC 2 (Trust Services Criteria) Mapping”| Control | Requirement | Implementation | Evidence | Status |
|---|---|---|---|---|
| CC6.1 | Logical access to sensitive data/systems is restricted | /api/v1/kms/tenant-key/* gated by tenant_authorize (kms:write/kms:admin scopes); kms.rs | kms_rotate_is_routine_but_destroy_needs_a_higher_tier (lib.rs) | Partially Met — see §7 item 1, the anonymous-default-tenant bypass |
| CC6.6 | Data is protected via encryption in line with its risk classification | wovyr-kms::envelope::seal/open (AES-256-GCM via ring); wraps wovyr-secrets’ current+previous values, wovyr-memory’s sensitive-flagged content, and wovyr-events’s webhook signing secrets | crates/wovyr-kms/tests/adversarial.rs (tamper/forgery tests); wovyr-secrets/wovyr-memory/wovyr-events round-trip + on-disk-never-plaintext tests | Met |
| CC6.7 | Data-in-use/transmission is restricted to authorized parties via key management | Root→tenant→DEK hierarchy (LocalKms); DEKs are per-call, tenant keys are versioned and tenant-scoped | tenant_isolation_a_dek_wrapped_for_one_tenant_will_not_unwrap_under_another, attacker_cannot_launder_a_dek_across_tenants_via_rewrap | Met for the app-layer boundary; root key itself is a single-process stand-in (§7 item 4) |
| CC6.8 | Unauthorized/malicious data disposal is prevented; authorized disposal is complete | Kms::destroy_tenant_key crypto-shreds all key versions; every downstream ciphertext becomes permanently unrecoverable | crypto_shredding_a_tenant_makes_every_operation_fail_closed; a_dek_captured_before_crypto_shredding_is_useless_after_even_via_envelope | Met |
| CC7.2 | Security events are logged and monitored | kms.tenant_key.rotate/.destroy recorded in the tamper-evident audit log, by tenant reference, never key material | kms_tenant_key_mutations_are_audited | Met |
4. ISO/IEC 27001:2022 (Annex A) Mapping
Section titled “4. ISO/IEC 27001:2022 (Annex A) Mapping”| Control | Requirement | Implementation | Evidence | Status |
|---|---|---|---|---|
| A.8.24 | Use of cryptography | AES-256-GCM envelope encryption (crates/wovyr-kms/src/crypto.rs); no ambient key reuse (generate_data_key mints a fresh DEK per call) | each_call_mints_an_independent_dek_even_for_identical_plaintext; nonce_is_never_reused_across_many_seals_for_the_same_tenant | Met |
| A.5.15 | Access control | RBAC scopes kms:write/kms:admin, tenant-scoped (wovyr-tenancy’s Role/grants) | rbac_default_deny_matrix_is_a_strict_privilege_ladder (rbac.rs) | Partially Met — see §7 item 1 |
| A.8.10 | Information deletion | Crypto-shredding (destroy_tenant_key) as the deletion mechanism for sealed data — permanent, verifiable, no residual recoverability | crypto_shredding_a_tenant_makes_every_operation_fail_closed | Met |
| A.8.15 | Logging | Every key-management mutation audited, hash-chained, tamper-evident (wovyr-audit) | kms_tenant_key_mutations_are_audited | Met |
| A.5.31 | Legal/regulatory/contractual requirements (incl. cryptography) | Documented key hierarchy (encryption.md §5), this mapping doc itself | — | Partially Met — no legal/regulatory review has actually occurred; this is architecture documentation, not a compliance opinion |
5. GDPR Mapping
Section titled “5. GDPR Mapping”| Article | Requirement | Implementation | Evidence | Status |
|---|---|---|---|---|
| Art. 32 | Security of processing (incl. “encryption of personal data”) | EncryptingMemoryStore/EncryptedFileSecretStore/EncryptedFileWebhookStore seal content at rest through wovyr-kms | crates/wovyr-kms/tests/adversarial.rs; wovyr-memory/wovyr-secrets/wovyr-events encrypting-store tests | Met for data explicitly flagged sensitive/opted into WOVYR_SECRETS_ENCRYPT_AT_REST/WOVYR_WEBHOOKS_ENCRYPT_AT_REST — not applied platform-wide by default (opt-in, see encryption.md) |
| Art. 17 | Right to erasure (“right to be forgotten”) | destroy_tenant_key crypto-shredding is a strong technical mechanism for erasure: once a tenant’s key material is destroyed, every DEK ever wrapped under it — and hence every ciphertext it protects, regardless of where that ciphertext physically lives — becomes permanently unrecoverable | crypto_shredding_a_tenant_makes_every_operation_fail_closed | Met as a mechanism; actual GDPR erasure also requires deleting (or accepting as inert ciphertext) the plaintext-adjacent metadata/backups outside wovyr-kms’s control — an operational/process step, not something this crate alone guarantees |
6. Adversarial Verification (Pen-Test Summary)
Section titled “6. Adversarial Verification (Pen-Test Summary)”crates/wovyr-kms/tests/adversarial.rs (new) attacks the LocalKms/envelope
boundary directly — no docker/network access needed, runs unconditionally in
CI:
- Cross-tenant laundering via
rewrap— confirmsrewrap_data_keycannot be used to move a DEK onto a different tenant’s key, even when that tenant happens to have a same-numbered key version. - Tenant-key-layer tampering — corrupts the wrapped tenant key itself
(not just the DEK, which the pre-existing unit tests already covered) via
direct
KmsStoreaccess, confirming the failure propagates rather than producing garbage key material that “succeeds.” - Nonce reuse at volume — 256 consecutive seals for one tenant, checked for AES-GCM nonce uniqueness at both the DEK-wrapper and payload layers (the existing unit test only checked a single pair).
- Version-number forgery — relabels a real, captured ciphertext with a
different
tenant_key_versionafter rotation; confirms AEAD rejects the mismatch rather than decrypting to silently-wrong plaintext. - Blind forgery — a wrapped value with no legitimate ciphertext at all (the “probing the endpoint” case, distinct from relabeling real data).
- Post-crypto-shred replay — a
SealedDatacaptured beforedestroy_tenant_key, replayed through the higher-levelenvelope::openAPI (the one real consumers call) after — confirms the erasure guarantee holds against a cached/captured credential, not just a fresh lookup.
All six pass against the real implementation (not mocked). See
crates/wovyr-kms/tests/adversarial.rs for the tests themselves — per this
codebase’s established convention (security-testing.md §5),
the test code is the authoritative record; this section summarizes it.
7. Residual Risk and Gaps
Section titled “7. Residual Risk and Gaps”Found during this pass, not fixed here (see rationale per item):
- Anonymous default-tenant bypass reaching
kms:admin— CLOSED (RM-GA-P4/ GA-003, narrowing RM-GA-P1 SEC-102).tenant_authorize(tenancy.rs) used to skip its RBAC check entirely for a request with noX-Wovyr-Principalagainst the default tenant wheneverAppState.anonymous_allowed(WOVYR_ALLOW_ANONYMOUS=1) — meaning an anonymous caller got every scope, includingkms:admin(crypto-shredding), with zero grant. That short-circuit is now deleted:anonymous_allowedgoverns only whetherauth::authenticate’sdisabled-loopbackmode lets an unauthenticated request reach a handler at all (still off by default, still refused byserve()on any non-loopback bind) — it no longer implies any authorization outcome once it does. An anonymous caller is now authorized exactly like any other principal with no memberships:Role::grantsguarantees an empty role set denies every scope, so the same request that used to succeed now403s regardless ofWOVYR_ALLOW_ANONYMOUS. Proven byanonymous_default_tenant_caller_reaches_kms_admin_only_up_to_the_auth_layer_now(flag on →403, not the old200/“destroyed”) andanonymous_default_tenant_caller_is_denied_when_the_flag_is_off(flag off →401, rejected even earlier, at authentication) in lib.rs. Local/dev convenience for tenant-scoped routes now requires a real credential (e.g.WOVYR_PLATFORM_ADMINS=<principal>plus that principal’s own header) — the same path a real deployment already uses; nothing tenant-scoped is reachable “for free” via anonymity alone anymore. Additionally, SEC-101 closes the deeper hole this item didn’t cover: rawX-Wovyr-Principal/bearer values are no longer trusted outright onceWOVYR_AUTH_MODE=jwt|apikeyis configured — the verified auth middleware (auth.rs) overwrites the header with the verified principal before any handler runs. The remaining, unavoidable characteristic of thedisabled-loopbackdefault (whenWOVYR_AUTH_MODEis unset) is that it still trusts raw headers for identity on a loopback bind — but identity alone no longer buys RBAC access the way anonymity used to. - Closed.
kms.json(the wrapped tenant-key catalog) is now restricted to owner-only access after every write —FileKmsStore::persist’srestrict_permissions(store.rs), the same treatmentroot::from_filealready gaveroot.key. Proven bystore::tests::file_store_restricts_kms_json_to_owner_only. - Closed (2026-07-09, authored and verified on a real Windows host).
Root-key and
kms.jsonfile permissions used to be a no-op on non-Unix —root::restrict_permissions/store.rs’s equivalent only ever calledchmodunder#[cfg(unix)], so on Windows these files kept the OS default ACL rather than being locked to the owning process. The three previously independent copies of that#[cfg(unix)]/#[cfg(not(unix))]pair (wovyr-kms’sroot.rsandstore.rs, the CLI’scredentials.jsonhandling in config.rs) are now one shared primitive,wovyr_common::fs::restrict_to_owner(fs.rs):chmod 0600on Unix unchanged, and on Windows a real ACL restriction viaicacls /inheritance:r /grant:r <user>:F(std has no ACL-editing API; this shells out to the tool bundled with every Windows install, the same external-tool-via-Commandpatternwovyr-tools’ egress lockdown already uses foriptables/nsenter, rather than adding a Windows-ACL crate dependency for one call site). Proven live on Windows — not just reasoned about — byfs::tests::restrict_to_owner_locks_the_file_down,root::tests::from_file_generates_once_then_persists_across_calls, andstore::tests::file_store_restricts_kms_json_to_owner_only, each of which asserts viaicaclsoutput that no inherited ACE survives and only the invoking user is granted access. - No cloud-KMS-/HSM-backed root.
LocalKmsholds the root key in-process by design (kms.rs doc comment) — a documented, intentional single-host stand-in. TheKmstrait is the boundary a real backend would implement instead; this is unstarted. - No PII/config field encryption.
wovyr-kmsis wired intowovyr-secretsandwovyr-memoryonly; no field-level encryption exists for tenancy/config records. - No external verification. No third-party penetration test, and no formal SOC 2/ISO 27001/GDPR audit, has been performed. This document is a self-assessment against the codebase as of 2026-07-05.
8. Dependencies
Section titled “8. Dependencies”13-security/index.md§7 (platform-wide compliance posture)13-security/encryption.md§5 (key hierarchy this maps)13-security/audit.md(the audit trail cited in §3/§4)15-testing/security-testing.md§9 (penetration testing & reviews)
9. Revision History
Section titled “9. Revision History”| Version | Date | Description |
|---|---|---|
| 1.4.0 | 2026-07-09 | Closed §7 item 3: root-key/kms.json file permissions were a no-op on non-Unix. Consolidated the three independent #[cfg(unix)]-only permission-restriction copies (wovyr-kms’s root.rs/store.rs, the CLI’s config.rs) into one shared wovyr_common::fs::restrict_to_owner, which on Windows shells out to icacls /inheritance:r /grant:r <user>:F. Authored and proven live on a real Windows host (not reasoned-about-only), closing the gap the prior 1.3.0 entry left open for lack of a Windows environment |
| 1.3.0 | 2026-07-09 | Merged two independent closures: §7 item 1 (tenant_authorize’s anonymous default-tenant RBAC bypass — WOVYR_ALLOW_ANONYMOUS=1 used to grant an anonymous caller every scope, incl. kms:admin — is deleted, anonymous_allowed now governs only whether an unauthenticated request reaches a handler at all) and §7 item 2 (kms.json, the wrapped tenant-key catalog, now chmod 0600 on Unix after every write, same treatment as root.key). Item 3 (non-Unix ACL) remains open |
| 1.1.0 | 2026-07-05 | Added wovyr-events’s new EncryptedFileWebhookStore (webhook subscription signing secrets, WOVYR_WEBHOOKS_ENCRYPT_AT_REST) as a third §4 encrypting-store consumer, updated CC6.6/Art. 32 evidence accordingly |
| 1.0.0 | 2026-07-05 | Initial mapping: SOC 2 (CC6.1/6.6/6.7/6.8/7.2), ISO 27001 Annex A (8.24/5.15/8.10/8.15/5.31), and GDPR (Art. 32, Art. 17) controls mapped to wovyr-kms/wovyr-secrets/wovyr-memory/wovyr-audit, backed by a new adversarial test suite (crates/wovyr-kms/tests/adversarial.rs) and a documented, proven residual-risk finding (anonymous default-tenant bypass reaching kms:admin) |