Skip to content

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.


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.


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”
ControlRequirementImplementationEvidenceStatus
CC6.1Logical access to sensitive data/systems is restricted/api/v1/kms/tenant-key/* gated by tenant_authorize (kms:write/kms:admin scopes); kms.rskms_rotate_is_routine_but_destroy_needs_a_higher_tier (lib.rs)Partially Met — see §7 item 1, the anonymous-default-tenant bypass
CC6.6Data is protected via encryption in line with its risk classificationwovyr-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 secretscrates/wovyr-kms/tests/adversarial.rs (tamper/forgery tests); wovyr-secrets/wovyr-memory/wovyr-events round-trip + on-disk-never-plaintext testsMet
CC6.7Data-in-use/transmission is restricted to authorized parties via key managementRoot→tenant→DEK hierarchy (LocalKms); DEKs are per-call, tenant keys are versioned and tenant-scopedtenant_isolation_a_dek_wrapped_for_one_tenant_will_not_unwrap_under_another, attacker_cannot_launder_a_dek_across_tenants_via_rewrapMet for the app-layer boundary; root key itself is a single-process stand-in (§7 item 4)
CC6.8Unauthorized/malicious data disposal is prevented; authorized disposal is completeKms::destroy_tenant_key crypto-shreds all key versions; every downstream ciphertext becomes permanently unrecoverablecrypto_shredding_a_tenant_makes_every_operation_fail_closed; a_dek_captured_before_crypto_shredding_is_useless_after_even_via_envelopeMet
CC7.2Security events are logged and monitoredkms.tenant_key.rotate/.destroy recorded in the tamper-evident audit log, by tenant reference, never key materialkms_tenant_key_mutations_are_auditedMet

ControlRequirementImplementationEvidenceStatus
A.8.24Use of cryptographyAES-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_tenantMet
A.5.15Access controlRBAC 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.10Information deletionCrypto-shredding (destroy_tenant_key) as the deletion mechanism for sealed data — permanent, verifiable, no residual recoverabilitycrypto_shredding_a_tenant_makes_every_operation_fail_closedMet
A.8.15LoggingEvery key-management mutation audited, hash-chained, tamper-evident (wovyr-audit)kms_tenant_key_mutations_are_auditedMet
A.5.31Legal/regulatory/contractual requirements (incl. cryptography)Documented key hierarchy (encryption.md §5), this mapping doc itselfPartially Met — no legal/regulatory review has actually occurred; this is architecture documentation, not a compliance opinion

ArticleRequirementImplementationEvidenceStatus
Art. 32Security of processing (incl. “encryption of personal data”)EncryptingMemoryStore/EncryptedFileSecretStore/EncryptedFileWebhookStore seal content at rest through wovyr-kmscrates/wovyr-kms/tests/adversarial.rs; wovyr-memory/wovyr-secrets/wovyr-events encrypting-store testsMet for data explicitly flagged sensitive/opted into WOVYR_SECRETS_ENCRYPT_AT_REST/WOVYR_WEBHOOKS_ENCRYPT_AT_RESTnot applied platform-wide by default (opt-in, see encryption.md)
Art. 17Right 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 unrecoverablecrypto_shredding_a_tenant_makes_every_operation_fail_closedMet 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 — confirms rewrap_data_key cannot 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 KmsStore access, 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_version after 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 SealedData captured before destroy_tenant_key, replayed through the higher-level envelope::open API (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.


Found during this pass, not fixed here (see rationale per item):

  1. 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 no X-Wovyr-Principal against the default tenant whenever AppState.anonymous_allowed (WOVYR_ALLOW_ANONYMOUS=1) — meaning an anonymous caller got every scope, including kms:admin (crypto-shredding), with zero grant. That short-circuit is now deleted: anonymous_allowed governs only whether auth::authenticate’s disabled-loopback mode lets an unauthenticated request reach a handler at all (still off by default, still refused by serve() 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::grants guarantees an empty role set denies every scope, so the same request that used to succeed now 403s regardless of WOVYR_ALLOW_ANONYMOUS. Proven by anonymous_default_tenant_caller_reaches_kms_admin_only_up_to_the_auth_layer_now (flag on → 403, not the old 200/“destroyed”) and anonymous_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: raw X-Wovyr-Principal/bearer values are no longer trusted outright once WOVYR_AUTH_MODE=jwt|apikey is configured — the verified auth middleware (auth.rs) overwrites the header with the verified principal before any handler runs. The remaining, unavoidable characteristic of the disabled-loopback default (when WOVYR_AUTH_MODE is 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.
  2. Closed. kms.json (the wrapped tenant-key catalog) is now restricted to owner-only access after every write — FileKmsStore::persist’s restrict_permissions (store.rs), the same treatment root::from_file already gave root.key. Proven by store::tests::file_store_restricts_kms_json_to_owner_only.
  3. Closed (2026-07-09, authored and verified on a real Windows host). Root-key and kms.json file permissions used to be a no-op on non-Unix — root::restrict_permissions/store.rs’s equivalent only ever called chmod under #[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’s root.rs and store.rs, the CLI’s credentials.json handling in config.rs) are now one shared primitive, wovyr_common::fs::restrict_to_owner (fs.rs): chmod 0600 on Unix unchanged, and on Windows a real ACL restriction via icacls /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-Command pattern wovyr-tools’ egress lockdown already uses for iptables/nsenter, rather than adding a Windows-ACL crate dependency for one call site). Proven live on Windows — not just reasoned about — by fs::tests::restrict_to_owner_locks_the_file_down, root::tests::from_file_generates_once_then_persists_across_calls, and store::tests::file_store_restricts_kms_json_to_owner_only, each of which asserts via icacls output that no inherited ACE survives and only the invoking user is granted access.
  4. No cloud-KMS-/HSM-backed root. LocalKms holds the root key in-process by design (kms.rs doc comment) — a documented, intentional single-host stand-in. The Kms trait is the boundary a real backend would implement instead; this is unstarted.
  5. No PII/config field encryption. wovyr-kms is wired into wovyr-secrets and wovyr-memory only; no field-level encryption exists for tenancy/config records.
  6. 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.


VersionDateDescription
1.4.02026-07-09Closed §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.02026-07-09Merged 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.02026-07-05Added 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.02026-07-05Initial 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)