Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
82 changes: 82 additions & 0 deletions Docs/ADR/001-centralize-signing-key-management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous]() | [Next](./002-encrypt-keystore-at-rest.md)

# [ADR-001] Centralize Signing Key Management Through A Core Key Store Abstraction

*2026-08* | Status: accepted

**Tag:** #adr_001

**Date:** 2026-08-26

**Scope:** AuthKit.Core.KeyManagement

## Context

The AuthKit core owns the cryptographic material used to issue and verify JWTs. It exposes a small set of primitives - `IKeyGenerator`, `IKeyEncryptor`, `IJwtKeyStore`, and `IKeyStoreRepository` - that together cover key creation, at rest encryption, and persistence. The host layer (REST, gRPC, plugins) must be able to mint, rotate, and publish signing keys without taking on knowledge of how those keys are generated, encrypted, or stored.

## Problem

The host should not depend on concrete key storage formats, encryption algorithms, or generation parameters. Without central key management abstraction owned by the core, that knowledge leaks into the host and into individual endpoints: each consumer reinvents how keys are produced, how they are protected on disk, and how the active signing key is resolved. Renaming key, changing the encryption scheme, or swapping the storage backend then becomes cross cutting change with no single contract to anchor it.

## Decision

Signing key management is centralized behind core abstraction that forms a contract between key generation, encryption, and persistence on one side, and the host on the other. A signing key becomes a stable core entity (`SigningKey`) resolved through the key store, rather than a raw file or repository record known only to single endpoint.

### IKeyGenerator

**Responsibilities:**

- Produce new asymmetric key pairs for signing (`GenerateAsync`).
- Expose the algorithm and parameters used so the rest of the core can reason about key strength.
- Remain agnostic of storage and encryption concerns.

### IKeyEncryptor

**Responsibilities:**

- Encrypt raw key material for at rest protection (`EncryptAsync` / `DecryptAsync`).
- Keep the encryption algorithm (eg. AES) isolated from the store and generator.
- Never expose plaintext material outside the core boundary.

### IJwtKeyStore

**Responsibilities:**

- Resolve the active signing key and the set of published public keys (`GetActiveKeyAsync`, `GetPublicJwksAsync`).
- Coordinate the generator and encryptor so that persisted keys are always encrypted.
- Present keys to the host as `SigningKey` entities and public JWKs, hiding storage details.

### IKeyStoreRepository

**Responsibilities:**

- Persist and load the encrypted keystore record (`KeystoreOnDisk`, `KeyEntry`, `KeyMetadata`).
- Remain pure persistence boundary with no cryptographic or domain logic.

### Design Rationale

- A single core contract keeps cryptographic decisions testable, auditable, and swappable without touching the host.
- Separating generation, encryption, and storage lets each concern evolve independently (algorithm upgrade, storage backend change, rotation policy) behind stable interfaces.
- Exposing keys as domain entities and public JWKs prevents the host from coupling to on disk formats and reduces the risk of key handling mistakes.

## Rejected

- Letting the host generate, encrypt, and persist keys directly with ad hoc calls.
- Coupling signing key and encryption knowledge into concrete endpoint code.
- A single monolithic key service that merges generation, encryption, and storage in one class.
- Storing keys in plaintext or relying on host managed secrets rather than the core encryptor.

## Consequences

The core becomes the single, predictable owner of signing key lifecycle and it is easier to control key completeness, rotation, and algorithm consistency across the host. The cost is maintaining the core abstraction as a contract and updating it whenever new key management capability is introduced.

## Related

- [ADR-002](./002-encrypt-keystore-at-rest.md) - encryption of keystore bytes at rest
- [ADR-003](./003-signing-key-lifecycle-immutable-transitions.md) - signing key lifecycle transitions
- [ADR-005](./005-public-keys-via-jwks.md) - public key discovery via JWKS
- [ADR-006](./006-kid-as-generated-guid.md) - key identifier strategy
- [ADR-007](./007-default-signing-algorithm-rsa-4096.md) - default signing algorithm
- [ADR-011](./011-keystore-persisted-as-singleton-marten-document.md) - keystore persistence

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous]() | [Next](./002-encrypt-keystore-at-rest.md)
55 changes: 55 additions & 0 deletions Docs/ADR/002-encrypt-keystore-at-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./001-centralize-signing-key-management.md) | [Next](./003-signing-key-lifecycle-immutable-transitions.md)

# [ADR-002] Encrypt Persisted Keystore Material At Rest Through A Pluggable Encryptor

*2026-08* | Status: accepted

**Tag:** #adr_002

**Date:** 2026-08-26

**Scope:** AuthKit.Core.KeyManagement

## Context

The key store persists sensitive RSA private key material so that signing keys survive restarts. The repository boundary (`IKeyStoreRepository`) is intentionally pure persistence sink - it stores and returns opaque bytes without understanding their content.

## Problem

Private key material must never be written to disk in plaintext. At the same time, the persistence layer must remain unaware of cryptography so it can be swapped (file, blob, database) without touching security logic. The system also needs a single, replaceable place to change the encryption algorithm if the threat model changes.

## Decision

All keystore bytes are encrypted before persistence and decrypted after load through the `IKeyEncryptor` boundary. The default implementation (`AesKeyEncryptor`) uses AES-256-CBC with 256-bit master key supplied at construction, generates cryptographically random IV per encryption call, and prefixes the IV to the ciphertext so it can be recovered on decryption.

### IKeyEncryptor

**Responsibilities:**

- Encrypt UTF-8 plaintext into an opaque byte blob (`Encrypt`).
- Reverse the operation and return plaintext (`Decrypt`).
- Remain stateless and independent of the persistence repository.

### Design Rationale

- Keeping encryption in dedicated, injectable boundary lets the algorithm be changed (e.g. to AES-GCM) without modifying the store or repository.
- A per call random IV avoids key/IV reuse and keeps the on disk format non deterministic.
- The repository stays dumb byte store, so storage backends are interchangeable.

## Rejected

- Persisting keys in plaintext or relying on filesystem permissions alone.
- Letting `IKeyStoreRepository` perform encryption or decryption.
- A fixed, embedded key compiled into the assembly.
- Reusing single static IV across all encryptions.

## Consequences

Key material is confidential at rest and the encryption algorithm is isolated behind a contract. The documentation explicitly notes that AES-CBC provides confidentiality without authenticated integrity, so the encrypted blob must be protected from tampering by another mechanism or migrated to an authenticated mode.

## Related

- [ADR-001](./001-centralize-signing-key-management.md) - key store abstraction owning the contract
- [ADR-011](./011-keystore-persisted-as-singleton-marten-document.md) - where the encrypted bytes are persisted

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./001-centralize-signing-key-management.md) | [Next](./003-signing-key-lifecycle-immutable-transitions.md)
54 changes: 54 additions & 0 deletions Docs/ADR/003-signing-key-lifecycle-immutable-transitions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./002-encrypt-keystore-at-rest.md) | [Next](./004-token-key-bindings-domain.md)

# [ADR-003] Model Signing Key Lifecycle As Immutable State Transitions

*2026-08* | Status: accepted

**Tag:** #adr_003

**Date:** 2026-08-26

**Scope:** AuthKit.Core.KeyManagement.Entity

## Context

A signing key moves through a lifecycle: it is generated, becomes valid for signing, may be rotated, expires, and can be revoked. Many parts of the system need to ask a single, reliable question — "is this key allowed to sign right now?" — without reimplementing lifecycle rules.

## Problem

If lifecycle state lives only in storage or is mutated in place by services, the rules for activation, expiration, and revocation get duplicated and drift. Mutable entities also make concurrent key operations and auditing harder to reason about.

## Decision

The signing key is modeled as an immutable `SigningKey` record. Lifecycle changes - `Activate`, `Revoke`, `Expire` - retur new instance via `with` expressions rather than mutating state, and a single `IsValidForSigning(now)` predicate evaluates activation, revocation, and expiration against supplied timestamp.

### SigningKey

**Responsibilities:**

- Carry the key identifier, public key (PEM), encrypted private material, algorithm, and lifecycle timestamps.
- Expose `Activate`, `Revoke`, and `Expire` as pure transitions that produce a new record.
- Provide `IsValidForSigning(DateTime now)` as the authoritative validity check.

### Design Rationale

- Immutability makes transitions easy to test and free of hidden side effects.
- One predicate for validity prevents lifecycle logic from being copied across the store, host, and bindings.
- Timestamp based evaluation keeps the rule deterministic and independent of wall clock assumptions inside the entity.

## Rejected

- A mutable entity with public setters changed directly by services.
- Storing lifecycle state only in the database and recomputing rules in each consumer.
- Embedding activation/expiration/revocation logic inside `JwtKeyStore` instead of the entity.

## Consequences

Lifecycle behavior is centralized and consistent, and transitions are auditable as value copies. The cost is richer record shape and the discipline of always using the returned instance rather than the original.

## Related

- [ADR-001](./001-centralize-signing-key-management.md) - key store that consumes the lifecycle
- [ADR-005](./005-public-keys-via-jwks.md) - JWKS excludes revoked keys using this state

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./002-encrypt-keystore-at-rest.md) | [Next](./004-token-key-bindings-domain.md)
67 changes: 67 additions & 0 deletions Docs/ADR/004-token-key-bindings-domain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./003-signing-key-lifecycle-immutable-transitions.md) | [Next](./005-public-keys-via-jwks.md)

# [ADR-004] Treat Developer Token To Signing Key Bindings As Core Domain

*2026-08* | Status: accepted

**Tag:** #adr_004

**Date:** 2026-08-26

**Scope:** AuthKit.Core.TokenKeyBindings

## Context

Developer tokens must be associated with the specific RSA signing key (and its public key) used to sign their JWTs, so that verification and rotation can be tracked per token. This association is domain concept, not an implementation detail of single endpoint.

## Problem

Without first class binding concept, the mapping between tokens and signing keys is scattered across hosts and key material, making rotation, public key updates, and revocation hard to trace and inconsistent across API surfaces.

## Decision

Token to key bindings are core domain: the `TokenKeyBinding` record captures `TokenId`, `SigningKeyId`, the public key, `BoundAt` timestamp, and `Revoked` flag. `IKeyBindingService` applies domain behavior (`CreateBindingAsync`, `RebindAsync`, `UpdatePublicKeyAsync`, `RevokeAsync`, `ListBindingsAsync`, `GetBindingAsync`), and `IKeyBindingRepository` is pure persistence boundary.

### TokenKeyBinding

**Responsibilities:**

- Represent the immutable by copy association between developer token and signing key.
- Expose `Rebind`, `UpdatePublicKey`, and `Revoke` as transitions returning a new record with refreshed `BoundAt`.

### IKeyBindingService

**Responsibilities:**

- Orchestrate binding operations through the repository.
- Invoke the entity's transitions rather than mutating binding state directly.

### IKeyBindingRepository

**Responsibilities:**

- Persist, load, update, and list bindings.
- Stay free of cryptographic and domain behavior.

### Design Rationale

- A dedicated domain keeps per token key provenance explicit and supports rotation without breaking verification.
- Separating service (behavior) from repository (persistence) mirrors the key management design and stays testable.

## Rejected

- Encoding the token -> key mapping implicitly inside the key store.
- Letting hosts manage bindings through ad hoc database access.
- Mutating binding fields directly from the service instead of using entity transitions.

## Consequences

Token key provenance, rotation, and revocation are consistent and observable. The cost is maintaining second core domain alongside key management and keeping the repository boundary clean.

## Related

- [ADR-001](./001-centralize-signing-key-management.md) - signing keys the bindings reference
- [ADR-009](./009-dynamic-plugin-discovery.md) - plugins consume bindings through this domain
- [ADR-012](./012-token-key-bindings-persisted-in-marten.md) - persistence of bindings

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./003-signing-key-lifecycle-immutable-transitions.md) | [Next](./005-public-keys-via-jwks.md)
54 changes: 54 additions & 0 deletions Docs/ADR/005-public-keys-via-jwks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./004-token-key-bindings-domain.md) | [Next](./006-kid-as-generated-guid.md)

# [ADR-005] Publish Public Keys Through JWKS Exposing Only Non Revoked Keys

*2026-08* | Status: accepted

**Tag:** #adr_005

**Date:** 2026-08-26

**Scope:** AuthKit.Core.KeyManagement

## Context

Relying parties verify JWT signatures by fetching the issuer's public keys. The key store already holds each key's RSA modulus and exponent plus its lifecycle metadata, and it keeps an in-memory view of all loaded keys.

## Problem

Consumers need standard, cache friendly way to discover valid public keys. If revoked or expired keys are published, or if the response is recomputed on every request without invalidation, verifiers may accept bad signatures or the endpoint wastes CPU reconstructing the key set.

## Decision

The key store exposes public keys via `GetPublicJwks()` as `PublicJwkDto` instances in JWKS compatible shape (`kty=RSA`, `use=sig`, `kid`, `alg`, `n`, `e`). Only keys whose `Revoked` metadata is false are included, and the result is cached, rebuilt only when the key set changes (rotation or revocation).

### PublicJwkDto

**Responsibilities:**

- Carry the JWKS fields needed by external verifiers.
- Remain read only projection of the in-memory key entry.

### Design Rationale

- JWKS is an industry standard discovery format, so off the shelf verifiers integrate without custom code.
- Excluding revoked keys prevents accepting signatures from retired keys.
- Caching with explicit invalidation balances performance against freshness after rotation.

## Rejected

- Publishing revoked or expired keys in the JWKS response.
- Regenerating the key set on every request without cache.
- Inventing custom public key discovery format instead of JWKS.

## Consequences

External verifiers get correct, standard key set and the endpoint stays efficient. The cost is explicitly invalidating the cache on every lifecycle change and keeping the projection in sync with key metadata.

## Related

- [ADR-001](./001-centralize-signing-key-management.md) - key store exposing the JWKS
- [ADR-003](./003-signing-key-lifecycle-immutable-transitions.md) - lifecycle drives revocation filtering
- [ADR-006](./006-kid-as-generated-guid.md) - `kid` values published in the key set

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./004-token-key-bindings-domain.md) | [Next](./006-kid-as-generated-guid.md)
47 changes: 47 additions & 0 deletions Docs/ADR/006-kid-as-generated-guid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./005-public-keys-via-jwks.md) | [Next](./007-default-signing-algorithm-rsa-4096.md)

# [ADR-006] Derive The JWT Key Identifier As Generated GUID

*2026-08* | Status: accepted

**Tag:** #adr_006

**Date:** 2026-08-26

**Scope:** AuthKit.Core.KeyManagement

## Context

Every signing key needs stable identifier that appears as the JWT `kid` header, indexes the in-memory key dictionary, and links persistence records and public JWK entries. The generator creates keys with no external identifier assigned.

## Problem

If the identifier is meaningful (eg. encodes the public key or sequence), it can break when a key is rotated or its material regenerated, and it can leak internal structure. The system needs one identifier that is stable for the life of the key across all representations.

## Decision

The key generator assigns each new key `kid` produced by `Guid.NewGuid().ToString("N")`. That same value is used as the JWT `kid`, the `ConcurrentDictionary` key in `JwtKeyStore`, the `KeyMetadata.Kid`, and the JWKS `kid` field, so all representations are anchored to one identifier.

### Design Rationale

- A random GUID is unique, opaque, and stable for the key's lifetime regardless of rotation or re-export.
- Reusing one identifier everywhere removes translation logic between persistence, memory, JWTs, and JWKS.
- No internal structure is exposed to relying parties.

## Rejected

- Sequential or meaningful identifiers that reveal ordering or internals.
- Deriving the `kid` from a hash of the public key, which complicates rotation and re-exports.
- Letting callers or the host assign the `kid` externally.

## Consequences

Key identity is consistent and collision free across the system. The cost is that the `kid` carries no human-readable meaning, so debugging relies on metadata rather than the identifier itself.

## Related

- [ADR-001](./001-centralize-signing-key-management.md) - key store indexing by `kid`
- [ADR-005](./005-public-keys-via-jwks.md) - `kid` in the published key set
- [ADR-007](./007-default-signing-algorithm-rsa-4096.md) - algorithm paired with the identifier

[ADR Home](../../README.md) | [Category Index](./README.md) | [Previous](./005-public-keys-via-jwks.md) | [Next](./007-default-signing-algorithm-rsa-4096.md)
Loading
Loading