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
18 changes: 15 additions & 3 deletions docs/Docs/Content/docs/services/kms.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
---
title: KMS
description: KMS emulation — symmetric and RSA keys, encrypt/decrypt, sign/verify, aliases, data keys.
description: KMS emulation — symmetric, RSA, and ECC keys; encrypt/decrypt; sign/verify; aliases; data keys.
order: 12
section: Services
---

# KMS

MicroStack's KMS handler supports symmetric (AES-256) and RSA (2048/4096) keys with full encrypt/decrypt and sign/verify operations. Key aliases, rotation status, and key policies are all supported.
MicroStack's KMS handler supports symmetric (AES-256), RSA (2048/3072/4096), and NIST ECC keys. Symmetric and RSA keys support encryption/decryption, while RSA and ECC keys support signing/verification. Key aliases, rotation status, and key policies are all supported.

## Supported Operations

Expand Down Expand Up @@ -89,6 +89,18 @@ var verified = await kms.VerifyAsync(new VerifyRequest
Console.WriteLine(verified.SignatureValid); // True
```

## ECC Sign and Verify

ECC signing keys use the AWS KMS algorithm associated with their curve:

| Key spec | Curve | Signing algorithm |
| --- | --- | --- |
| `ECC_NIST_P256` | secp256r1 | `ECDSA_SHA_256` |
| `ECC_NIST_P384` | secp384r1 | `ECDSA_SHA_384` |
| `ECC_NIST_P521` | secp521r1 | `ECDSA_SHA_512` |

ECC keys require `SIGN_VERIFY` usage and cannot encrypt or decrypt. ECDSA signatures are returned as DER-encoded ANSI X9.62/RFC 3279 sequences, matching AWS KMS rather than the IEEE P1363 `r || s` format.

## Aliases

```csharp
Expand All @@ -108,5 +120,5 @@ var encrypted = await kms.EncryptAsync(new EncryptRequest
```

:::aside{type="note" title="Supported key types"}
Supported key specs: `SYMMETRIC_DEFAULT` (AES-256-GCM), `RSA_2048`, `RSA_4096`. Signing algorithms: `RSASSA_PKCS1_V1_5_SHA_256`, `RSASSA_PSS_SHA_256`, `RSASSA_PKCS1_V1_5_SHA_384`, `RSASSA_PSS_SHA_384`, `RSASSA_PKCS1_V1_5_SHA_512`, `RSASSA_PSS_SHA_512`.
Supported key specs: `SYMMETRIC_DEFAULT` (AES-256-GCM), `RSA_2048`, `RSA_3072`, `RSA_4096`, `ECC_NIST_P256`, `ECC_NIST_P384`, and `ECC_NIST_P521`. RSA signing supports `RSASSA_PKCS1_V1_5_SHA_256/384/512` and `RSASSA_PSS_SHA_256/384/512`; ECC signing uses the curve-specific algorithms listed above.
:::
882 changes: 0 additions & 882 deletions progress.txt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" Version="13.2.2" />
<PackageReference Include="Aspire.Hosting" Version="13.5.3" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Amazon.Lambda.Core" Version="2.*" />
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.*" />
<PackageReference Include="Amazon.Lambda.Core" Version="3.3.0" />
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="3.0.1" />
</ItemGroup>

</Project>
145 changes: 118 additions & 27 deletions src/MicroStack/Services/Kms/KmsServiceHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,17 +251,31 @@ private static byte[] XorBytes(byte[] a, byte[] b)
return result;
}

private static (RSASignaturePadding? Padding, HashAlgorithmName Hash) GetSigningParams(string algorithm)
private static HashAlgorithmName GetSigningHash(string algorithm)
{
return algorithm switch
{
"RSASSA_PKCS1_V1_5_SHA_256" => (RSASignaturePadding.Pkcs1, HashAlgorithmName.SHA256),
"RSASSA_PKCS1_V1_5_SHA_384" => (RSASignaturePadding.Pkcs1, HashAlgorithmName.SHA384),
"RSASSA_PKCS1_V1_5_SHA_512" => (RSASignaturePadding.Pkcs1, HashAlgorithmName.SHA512),
"RSASSA_PSS_SHA_256" => (RSASignaturePadding.Pss, HashAlgorithmName.SHA256),
"RSASSA_PSS_SHA_384" => (RSASignaturePadding.Pss, HashAlgorithmName.SHA384),
"RSASSA_PSS_SHA_512" => (RSASignaturePadding.Pss, HashAlgorithmName.SHA512),
_ => (null, default),
"RSASSA_PKCS1_V1_5_SHA_256" or "RSASSA_PSS_SHA_256" or "ECDSA_SHA_256"
=> HashAlgorithmName.SHA256,
"RSASSA_PKCS1_V1_5_SHA_384" or "RSASSA_PSS_SHA_384" or "ECDSA_SHA_384"
=> HashAlgorithmName.SHA384,
"RSASSA_PKCS1_V1_5_SHA_512" or "RSASSA_PSS_SHA_512" or "ECDSA_SHA_512"
=> HashAlgorithmName.SHA512,
_ => default,
};
}

private static RSASignaturePadding? GetRsaSignaturePadding(string algorithm)
{
return algorithm switch
{
"RSASSA_PKCS1_V1_5_SHA_256" or
"RSASSA_PKCS1_V1_5_SHA_384" or
"RSASSA_PKCS1_V1_5_SHA_512" => RSASignaturePadding.Pkcs1,
"RSASSA_PSS_SHA_256" or
"RSASSA_PSS_SHA_384" or
"RSASSA_PSS_SHA_512" => RSASignaturePadding.Pss,
_ => null,
};
}

Expand Down Expand Up @@ -305,9 +319,14 @@ private ServiceResponse ActCreateKey(JsonElement data)
rec.EncryptionAlgorithms = ["SYMMETRIC_DEFAULT"];
rec.SigningAlgorithms = [];
}
else if (keySpec is "RSA_2048" or "RSA_4096")
else if (keySpec is "RSA_2048" or "RSA_3072" or "RSA_4096")
{
var bits = keySpec == "RSA_2048" ? 2048 : 4096;
var bits = keySpec switch
{
"RSA_2048" => 2048,
"RSA_3072" => 3072,
_ => 4096,
};
var rsa = RSA.Create(bits);
rec.RsaKey = rsa;
rec.PublicKeyDer = rsa.ExportSubjectPublicKeyInfo();
Expand Down Expand Up @@ -335,6 +354,36 @@ private ServiceResponse ActCreateKey(JsonElement data)
rec.SigningAlgorithms = [];
}
}
else if (keySpec is "ECC_NIST_P256" or "ECC_NIST_P384" or "ECC_NIST_P521")
{
if (keyUsage != "SIGN_VERIFY")
{
return AwsResponseHelpers.ErrorResponseJson(
"ValidationException",
$"KeySpec {keySpec} requires KeyUsage SIGN_VERIFY",
400);
}

var curve = keySpec switch
{
"ECC_NIST_P256" => ECCurve.NamedCurves.nistP256,
"ECC_NIST_P384" => ECCurve.NamedCurves.nistP384,
_ => ECCurve.NamedCurves.nistP521,
};
var ecdsa = ECDsa.Create(curve);
rec.EcdsaKey = ecdsa;
rec.PublicKeyDer = ecdsa.ExportSubjectPublicKeyInfo();
rec.EncryptionAlgorithms = [];
rec.SigningAlgorithms =
[
keySpec switch
{
"ECC_NIST_P256" => "ECDSA_SHA_256",
"ECC_NIST_P384" => "ECDSA_SHA_384",
_ => "ECDSA_SHA_512",
},
];
}
else
{
return AwsResponseHelpers.ErrorResponseJson(
Expand Down Expand Up @@ -443,7 +492,7 @@ private ServiceResponse ActSign(JsonElement data)
return AwsResponseHelpers.ErrorResponseJson("NotFoundException", $"Key {keyId} not found", 400);
}

if (rec.RsaKey is null)
if (rec.RsaKey is null && rec.EcdsaKey is null)
{
return AwsResponseHelpers.ErrorResponseJson(
"UnsupportedOperationException",
Expand All @@ -454,18 +503,30 @@ private ServiceResponse ActSign(JsonElement data)
var messageB64 = GetString(data, "Message") ?? "";
var algorithm = GetString(data, "SigningAlgorithm") ?? "RSASSA_PKCS1_V1_5_SHA_256";

var message = Convert.FromBase64String(messageB64);

var (padding, hash) = GetSigningParams(algorithm);
if (padding is null)
if (!rec.SigningAlgorithms.Contains(algorithm, StringComparer.Ordinal))
{
return AwsResponseHelpers.ErrorResponseJson(
"UnsupportedOperationException",
$"Signing algorithm {algorithm} is not supported",
"ValidationException",
$"Signing algorithm {algorithm} is not valid for key spec {rec.KeySpec}",
400);
}

var signature = rec.RsaKey.SignData(message, hash, padding);
var message = Convert.FromBase64String(messageB64);
var hash = GetSigningHash(algorithm);
byte[] signature;

if (rec.RsaKey is not null)
{
var padding = GetRsaSignaturePadding(algorithm)!;
signature = rec.RsaKey.SignData(message, hash, padding);
}
else
{
signature = rec.EcdsaKey!.SignData(
message,
hash,
DSASignatureFormat.Rfc3279DerSequence);
}

return AwsResponseHelpers.JsonResponse(new Dictionary<string, object?>
{
Expand All @@ -487,7 +548,7 @@ private ServiceResponse ActVerify(JsonElement data)
return AwsResponseHelpers.ErrorResponseJson("NotFoundException", $"Key {keyId} not found", 400);
}

if (rec.RsaKey is null)
if (rec.RsaKey is null && rec.EcdsaKey is null)
{
return AwsResponseHelpers.ErrorResponseJson(
"UnsupportedOperationException",
Expand All @@ -499,19 +560,32 @@ private ServiceResponse ActVerify(JsonElement data)
var signatureB64 = GetString(data, "Signature") ?? "";
var algorithm = GetString(data, "SigningAlgorithm") ?? "RSASSA_PKCS1_V1_5_SHA_256";

var message = Convert.FromBase64String(messageB64);
var signature = Convert.FromBase64String(signatureB64);

var (padding, hash) = GetSigningParams(algorithm);
if (padding is null)
if (!rec.SigningAlgorithms.Contains(algorithm, StringComparer.Ordinal))
{
return AwsResponseHelpers.ErrorResponseJson(
"UnsupportedOperationException",
$"Signing algorithm {algorithm} is not supported",
"ValidationException",
$"Signing algorithm {algorithm} is not valid for key spec {rec.KeySpec}",
400);
}

var valid = rec.RsaKey.VerifyData(message, signature, hash, padding);
var message = Convert.FromBase64String(messageB64);
var signature = Convert.FromBase64String(signatureB64);
var hash = GetSigningHash(algorithm);
bool valid;

if (rec.RsaKey is not null)
{
var padding = GetRsaSignaturePadding(algorithm)!;
valid = rec.RsaKey.VerifyData(message, signature, hash, padding);
}
else
{
valid = rec.EcdsaKey!.VerifyData(
message,
signature,
hash,
DSASignatureFormat.Rfc3279DerSequence);
}

return AwsResponseHelpers.JsonResponse(new Dictionary<string, object?>
{
Expand All @@ -533,6 +607,14 @@ private ServiceResponse ActEncrypt(JsonElement data)
return AwsResponseHelpers.ErrorResponseJson("NotFoundException", $"Key {keyId} not found", 400);
}

if (rec.EcdsaKey is not null)
{
return AwsResponseHelpers.ErrorResponseJson(
"ValidationException",
"ECC keys cannot be used for encryption",
400);
}

var plaintextB64 = GetString(data, "Plaintext") ?? "";
var plaintext = Convert.FromBase64String(plaintextB64);
var encContext = GetEncryptionContext(data);
Expand Down Expand Up @@ -615,6 +697,14 @@ private ServiceResponse ActDecrypt(JsonElement data)
400);
}

if (rec.EcdsaKey is not null)
{
return AwsResponseHelpers.ErrorResponseJson(
"ValidationException",
"ECC keys cannot be used for decryption",
400);
}

byte[] plaintext;

if (rec.SymmetricKey is not null)
Expand Down Expand Up @@ -1231,6 +1321,7 @@ internal sealed class KmsKeyRecord
internal double? DeletionDate { get; set; }
internal byte[]? SymmetricKey { get; set; }
internal RSA? RsaKey { get; set; }
internal ECDsa? EcdsaKey { get; set; }
internal byte[]? PublicKeyDer { get; set; }
internal List<string> EncryptionAlgorithms { get; set; } = [];
internal List<string> SigningAlgorithms { get; set; } = [];
Expand Down
4 changes: 2 additions & 2 deletions tests/MicroStack.Aspire.Tests/MicroStack.Aspire.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="13.2.2" />
<PackageReference Include="xunit.v3.core.mtp-v2" Version="3.2.2" />
<PackageReference Include="Aspire.Hosting.Testing" Version="13.5.3" />
<PackageReference Include="xunit.v3.core.mtp-v2" Version="4.0.0" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="AWSSDK.S3" Version="4.*" />
<PackageReference Include="AWSSDK.SQS" Version="4.*" />
Expand Down
Loading
Loading