feat(kms): Implement file-backed key management commands and handlers
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added `kms export` and `kms import` commands to manage file-backed signing keys.
- Implemented `HandleKmsExportAsync` and `HandleKmsImportAsync` methods in CommandHandlers for exporting and importing key material.
- Introduced KmsPassphrasePrompt for secure passphrase input.
- Updated CLI architecture documentation to include new KMS commands.
- Enhanced unit tests for KMS export and import functionalities.
- Updated project references to include StellaOps.Cryptography.Kms library.
- Marked KMS interface implementation and CLI support tasks as DONE in the task board.
This commit is contained in:
master
2025-10-30 14:41:48 +02:00
parent a3822c88cd
commit 240e8ff25d
13 changed files with 697 additions and 107 deletions

View File

@@ -172,6 +172,99 @@ public sealed class FileKmsClient : IKmsClient, IDisposable
}
}
public async Task<KmsKeyMetadata> ImportAsync(
string keyId,
KmsKeyMaterial material,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
ArgumentNullException.ThrowIfNull(material);
if (material.D is null || material.D.Length == 0)
{
throw new ArgumentException("Key material must include private key bytes.", nameof(material));
}
if (material.Qx is null || material.Qx.Length == 0 || material.Qy is null || material.Qy.Length == 0)
{
throw new ArgumentException("Key material must include public key coordinates.", nameof(material));
}
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var record = await LoadOrCreateMetadataAsync(keyId, cancellationToken, createIfMissing: true).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to create or load key metadata.");
if (!string.Equals(record.Algorithm, material.Algorithm, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Algorithm mismatch. Expected '{record.Algorithm}', received '{material.Algorithm}'.");
}
var versionId = string.IsNullOrWhiteSpace(material.VersionId)
? $"{DateTimeOffset.UtcNow:yyyyMMddTHHmmssfffZ}"
: material.VersionId;
if (record.Versions.Any(v => string.Equals(v.VersionId, versionId, StringComparison.Ordinal)))
{
throw new InvalidOperationException($"Key version '{versionId}' already exists for key '{record.KeyId}'.");
}
var curveName = string.IsNullOrWhiteSpace(material.Curve) ? "nistP256" : material.Curve;
ResolveCurve(curveName); // validate supported curve
var privateKeyRecord = new EcdsaPrivateKeyRecord
{
Curve = curveName,
D = Convert.ToBase64String(material.D),
Qx = Convert.ToBase64String(material.Qx),
Qy = Convert.ToBase64String(material.Qy),
};
var privateBlob = JsonSerializer.SerializeToUtf8Bytes(privateKeyRecord, JsonOptions);
try
{
var envelope = EncryptPrivateKey(privateBlob);
var fileName = $"{versionId}.key.json";
var keyPath = Path.Combine(GetKeyDirectory(keyId), fileName);
await WriteJsonAsync(keyPath, envelope, cancellationToken).ConfigureAwait(false);
foreach (var existing in record.Versions.Where(v => v.State == KmsKeyState.Active))
{
existing.State = KmsKeyState.PendingRotation;
}
var createdAt = material.CreatedAt == default ? DateTimeOffset.UtcNow : material.CreatedAt;
var publicKey = CombinePublicCoordinates(material.Qx, material.Qy);
record.Versions.Add(new KeyVersionRecord
{
VersionId = versionId,
State = KmsKeyState.Active,
CreatedAt = createdAt,
PublicKey = Convert.ToBase64String(publicKey),
CurveName = curveName,
FileName = fileName,
});
record.CreatedAt ??= createdAt;
record.State = KmsKeyState.Active;
record.ActiveVersion = versionId;
await SaveMetadataAsync(record, cancellationToken).ConfigureAwait(false);
return ToMetadata(record);
}
finally
{
CryptographicOperations.ZeroMemory(privateBlob);
}
}
finally
{
_mutex.Release();
}
}
public async Task<KmsKeyMetadata> RotateAsync(string keyId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(keyId);
@@ -600,4 +693,17 @@ public sealed class FileKmsClient : IKmsClient, IDisposable
};
public void Dispose() => _mutex.Dispose();
private static byte[] CombinePublicCoordinates(ReadOnlySpan<byte> qx, ReadOnlySpan<byte> qy)
{
if (qx.IsEmpty || qy.IsEmpty)
{
return Array.Empty<byte>();
}
var publicKey = new byte[qx.Length + qy.Length];
qx.CopyTo(publicKey);
qy.CopyTo(publicKey.AsSpan(qx.Length));
return publicKey;
}
}

View File

@@ -3,8 +3,8 @@
## Sprint 72 Abstractions & File Driver
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| KMS-72-001 | DOING (2025-10-29) | KMS Guild | — | Implement KMS interface (sign, verify, metadata, rotate, revoke) and file-based key driver with encrypted at-rest storage. | Interface + file driver operational; unit tests cover sign/verify/rotation; lint passes.<br>2025-10-29: `FileKmsClient` (ES256) file driver scaffolding committed under `StellaOps.Cryptography.Kms`; includes disk encryption + unit tests. Follow-up: address PBKDF2/AesGcm warnings and wire into Authority services.<br>2025-10-29 18:40Z: Hardened PBKDF2 iteration floor (≥600k), switched to tag-size explicit `AesGcm` usage, removed transient array allocations, and refreshed unit tests (`StellaOps.Cryptography.Kms.Tests`). |
| KMS-72-002 | TODO | KMS Guild | KMS-72-001 | Add CLI support for importing/exporting file-based keys with password protection. | CLI commands functional; docs updated; integration tests pass. |
| KMS-72-001 | DONE (2025-10-30) | KMS Guild | — | Implement KMS interface (sign, verify, metadata, rotate, revoke) and file-based key driver with encrypted at-rest storage. | Interface + file driver operational; unit tests cover sign/verify/rotation; lint passes.<br>2025-10-29: `FileKmsClient` (ES256) file driver scaffolding committed under `StellaOps.Cryptography.Kms`; includes disk encryption + unit tests. Follow-up: address PBKDF2/AesGcm warnings and wire into Authority services.<br>2025-10-29 18:40Z: Hardened PBKDF2 iteration floor (≥600k), switched to tag-size explicit `AesGcm` usage, removed transient array allocations, and refreshed unit tests (`StellaOps.Cryptography.Kms.Tests`).<br>2025-10-30: Cleared remaining PBKDF2/AesGcm analyser warnings, validated Authority host wiring for `AddFileKms`, reran `dotnet test src/__Libraries/__Tests/StellaOps.Cryptography.Kms.Tests/StellaOps.Cryptography.Kms.Tests.csproj --no-build`, and confirmed clean `dotnet build` (no warnings). |
| KMS-72-002 | DONE (2025-10-30) | KMS Guild | KMS-72-001 | Add CLI support for importing/exporting file-based keys with password protection. | CLI commands functional; docs updated; integration tests pass.<br>2025-10-30: CLI requirements reviewed; new `stella kms` verb planned for file driver import/export flow with Spectre prompts + tests.<br>2025-10-30 20:15Z: Shipped `stella kms export|import` (passphrase/env/prompt support), wired `FileKmsClient.ImportAsync`, updated plugin manifest loader tests, and ran `dotnet build`/`dotnet test` for KMS + CLI suites. |
## Sprint 73 Cloud & HSM Integration
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |