release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -0,0 +1,431 @@
|
||||
namespace StellaOps.Cryptography.Plugin.Fips;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using StellaOps.Plugin.Abstractions;
|
||||
using StellaOps.Plugin.Abstractions.Capabilities;
|
||||
using StellaOps.Plugin.Abstractions.Context;
|
||||
using StellaOps.Plugin.Abstractions.Health;
|
||||
using StellaOps.Plugin.Abstractions.Lifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// FIPS 140-2 compliant cryptography plugin.
|
||||
/// Uses .NET's FIPS-validated cryptographic implementations.
|
||||
/// </summary>
|
||||
public sealed class FipsPlugin : CryptoPluginBase
|
||||
{
|
||||
private FipsOptions? _options;
|
||||
private RSA? _rsaKey;
|
||||
private ECDsa? _ecdsaKey;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PluginInfo Info => new(
|
||||
Id: "com.stellaops.crypto.fips",
|
||||
Name: "FIPS 140-2 Cryptography Provider",
|
||||
Version: "1.0.0",
|
||||
Vendor: "Stella Ops",
|
||||
Description: "US FIPS 140-2 compliant cryptographic algorithms",
|
||||
LicenseId: "AGPL-3.0-or-later");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> SupportedAlgorithms => new[]
|
||||
{
|
||||
"RSA-SHA256",
|
||||
"RSA-SHA384",
|
||||
"RSA-SHA512",
|
||||
"RSA-PSS-SHA256",
|
||||
"RSA-PSS-SHA384",
|
||||
"RSA-PSS-SHA512",
|
||||
"ECDSA-P256-SHA256",
|
||||
"ECDSA-P384-SHA384",
|
||||
"ECDSA-P521-SHA512",
|
||||
"AES-128-CBC",
|
||||
"AES-256-CBC",
|
||||
"AES-128-GCM",
|
||||
"AES-256-GCM",
|
||||
"SHA-256",
|
||||
"SHA-384",
|
||||
"SHA-512",
|
||||
"HMAC-SHA256",
|
||||
"HMAC-SHA384",
|
||||
"HMAC-SHA512"
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task InitializeCryptoServiceAsync(IPluginContext context, CancellationToken ct)
|
||||
{
|
||||
_options = context.Configuration.Bind<FipsOptions>() ?? new FipsOptions();
|
||||
|
||||
// Verify FIPS mode if required
|
||||
if (_options.RequireFipsMode && !IsFipsModeEnabled())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"FIPS mode is required but not enabled. Set CryptoConfig.AllowOnlyFipsAlgorithms = true.");
|
||||
}
|
||||
|
||||
// Initialize RSA key
|
||||
if (!string.IsNullOrEmpty(_options.RsaKeyXml))
|
||||
{
|
||||
_rsaKey = RSA.Create();
|
||||
_rsaKey.FromXmlString(_options.RsaKeyXml);
|
||||
Context?.Logger.Info("FIPS provider initialized with configured RSA key");
|
||||
}
|
||||
else if (_options.GenerateKeysOnInit)
|
||||
{
|
||||
_rsaKey = RSA.Create(_options.RsaKeySize);
|
||||
Context?.Logger.Info("FIPS provider initialized with generated {KeySize}-bit RSA key", _options.RsaKeySize);
|
||||
}
|
||||
|
||||
// Initialize ECDSA key
|
||||
if (!string.IsNullOrEmpty(_options.EcdsaKeyXml))
|
||||
{
|
||||
_ecdsaKey = ECDsa.Create();
|
||||
_ecdsaKey.FromXmlString(_options.EcdsaKeyXml);
|
||||
}
|
||||
else if (_options.GenerateKeysOnInit)
|
||||
{
|
||||
_ecdsaKey = ECDsa.Create(GetECCurve(_options.EcdsaCurve));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanHandle(CryptoOperation operation, string algorithm)
|
||||
{
|
||||
return SupportedAlgorithms.Contains(algorithm, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<byte[]> SignAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var algorithm = options.Algorithm;
|
||||
byte[] signature;
|
||||
|
||||
if (algorithm.StartsWith("ECDSA", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (_ecdsaKey == null)
|
||||
{
|
||||
throw new InvalidOperationException("No ECDSA key available.");
|
||||
}
|
||||
|
||||
var hashAlgorithm = GetHashAlgorithmName(algorithm);
|
||||
signature = _ecdsaKey.SignData(data.ToArray(), hashAlgorithm);
|
||||
}
|
||||
else if (algorithm.Contains("PSS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (_rsaKey == null)
|
||||
{
|
||||
throw new InvalidOperationException("No RSA key available.");
|
||||
}
|
||||
|
||||
var hashAlgorithm = GetHashAlgorithmName(algorithm);
|
||||
signature = _rsaKey.SignData(data.ToArray(), hashAlgorithm, RSASignaturePadding.Pss);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_rsaKey == null)
|
||||
{
|
||||
throw new InvalidOperationException("No RSA key available.");
|
||||
}
|
||||
|
||||
var hashAlgorithm = GetHashAlgorithmName(algorithm);
|
||||
signature = _rsaKey.SignData(data.ToArray(), hashAlgorithm, RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
|
||||
Context?.Logger.Debug("Signed {DataLength} bytes with {Algorithm}", data.Length, algorithm);
|
||||
|
||||
return Task.FromResult(signature);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CryptoVerifyOptions options, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var algorithm = options.Algorithm;
|
||||
bool isValid;
|
||||
|
||||
if (algorithm.StartsWith("ECDSA", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (_ecdsaKey == null)
|
||||
{
|
||||
throw new InvalidOperationException("No ECDSA key available.");
|
||||
}
|
||||
|
||||
var hashAlgorithm = GetHashAlgorithmName(algorithm);
|
||||
isValid = _ecdsaKey.VerifyData(data.ToArray(), signature.ToArray(), hashAlgorithm);
|
||||
}
|
||||
else if (algorithm.Contains("PSS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (_rsaKey == null)
|
||||
{
|
||||
throw new InvalidOperationException("No RSA key available.");
|
||||
}
|
||||
|
||||
var hashAlgorithm = GetHashAlgorithmName(algorithm);
|
||||
isValid = _rsaKey.VerifyData(data.ToArray(), signature.ToArray(), hashAlgorithm, RSASignaturePadding.Pss);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_rsaKey == null)
|
||||
{
|
||||
throw new InvalidOperationException("No RSA key available.");
|
||||
}
|
||||
|
||||
var hashAlgorithm = GetHashAlgorithmName(algorithm);
|
||||
isValid = _rsaKey.VerifyData(data.ToArray(), signature.ToArray(), hashAlgorithm, RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
|
||||
Context?.Logger.Debug("Verified signature: {IsValid}", isValid);
|
||||
|
||||
return Task.FromResult(isValid);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<byte[]> EncryptAsync(ReadOnlyMemory<byte> data, CryptoEncryptOptions options, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var algorithm = options.Algorithm;
|
||||
var keyBytes = GetSymmetricKey(options.KeyId, algorithm);
|
||||
|
||||
if (algorithm.Contains("GCM", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(AesGcmEncrypt(data.ToArray(), keyBytes, options.Iv, options.Aad));
|
||||
}
|
||||
else
|
||||
{
|
||||
return Task.FromResult(AesCbcEncrypt(data.ToArray(), keyBytes, options.Iv));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<byte[]> DecryptAsync(ReadOnlyMemory<byte> data, CryptoDecryptOptions options, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var algorithm = options.Algorithm;
|
||||
var keyBytes = GetSymmetricKey(options.KeyId, algorithm);
|
||||
|
||||
if (algorithm.Contains("GCM", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(AesGcmDecrypt(data.ToArray(), keyBytes, options.Iv, options.Aad));
|
||||
}
|
||||
else
|
||||
{
|
||||
return Task.FromResult(AesCbcDecrypt(data.ToArray(), keyBytes, options.Iv));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<byte[]> HashAsync(ReadOnlyMemory<byte> data, string algorithm, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
byte[] hash;
|
||||
|
||||
if (algorithm.Contains("HMAC", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// For HMAC, we need a key - use a default for demonstration
|
||||
var hmacKey = System.Text.Encoding.UTF8.GetBytes("default-hmac-key");
|
||||
hash = algorithm.ToUpperInvariant() switch
|
||||
{
|
||||
var a when a.Contains("512") => HMACSHA512.HashData(hmacKey, data.Span),
|
||||
var a when a.Contains("384") => HMACSHA384.HashData(hmacKey, data.Span),
|
||||
_ => HMACSHA256.HashData(hmacKey, data.Span)
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
hash = algorithm.ToUpperInvariant() switch
|
||||
{
|
||||
var a when a.Contains("512") => SHA512.HashData(data.Span),
|
||||
var a when a.Contains("384") => SHA384.HashData(data.Span),
|
||||
_ => SHA256.HashData(data.Span)
|
||||
};
|
||||
}
|
||||
|
||||
Context?.Logger.Debug("Computed {Algorithm} hash of {DataLength} bytes", algorithm, data.Length);
|
||||
|
||||
return Task.FromResult(hash);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ValueTask DisposeAsync()
|
||||
{
|
||||
_rsaKey?.Dispose();
|
||||
_ecdsaKey?.Dispose();
|
||||
_rsaKey = null;
|
||||
_ecdsaKey = null;
|
||||
State = PluginLifecycleState.Stopped;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private static bool IsFipsModeEnabled()
|
||||
{
|
||||
// Check if FIPS mode is enabled at OS level
|
||||
try
|
||||
{
|
||||
// This will throw if FIPS mode is required but algorithm is not FIPS-compliant
|
||||
using var _ = SHA256.Create();
|
||||
return true;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static ECCurve GetECCurve(string curveName)
|
||||
{
|
||||
return curveName.ToUpperInvariant() switch
|
||||
{
|
||||
"P-521" or "SECP521R1" => ECCurve.NamedCurves.nistP521,
|
||||
"P-384" or "SECP384R1" => ECCurve.NamedCurves.nistP384,
|
||||
_ => ECCurve.NamedCurves.nistP256
|
||||
};
|
||||
}
|
||||
|
||||
private static HashAlgorithmName GetHashAlgorithmName(string algorithm)
|
||||
{
|
||||
return algorithm.ToUpperInvariant() switch
|
||||
{
|
||||
var a when a.Contains("512") => HashAlgorithmName.SHA512,
|
||||
var a when a.Contains("384") => HashAlgorithmName.SHA384,
|
||||
_ => HashAlgorithmName.SHA256
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] GetSymmetricKey(string keyId, string algorithm)
|
||||
{
|
||||
// Determine key size from algorithm
|
||||
var keySize = algorithm.Contains("256") ? 32 : 16;
|
||||
|
||||
// Derive key from key ID using SHA-256
|
||||
var hash = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(keyId));
|
||||
return hash.Take(keySize).ToArray();
|
||||
}
|
||||
|
||||
private static byte[] AesCbcEncrypt(byte[] data, byte[] key, byte[]? iv)
|
||||
{
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = key;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
|
||||
iv ??= aes.IV;
|
||||
aes.IV = iv;
|
||||
|
||||
using var encryptor = aes.CreateEncryptor();
|
||||
var encrypted = encryptor.TransformFinalBlock(data, 0, data.Length);
|
||||
|
||||
// Prepend IV to ciphertext
|
||||
var result = new byte[iv.Length + encrypted.Length];
|
||||
Array.Copy(iv, 0, result, 0, iv.Length);
|
||||
Array.Copy(encrypted, 0, result, iv.Length, encrypted.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] AesCbcDecrypt(byte[] data, byte[] key, byte[]? iv)
|
||||
{
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = key;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
|
||||
if (iv == null)
|
||||
{
|
||||
iv = data.Take(16).ToArray();
|
||||
data = data.Skip(16).ToArray();
|
||||
}
|
||||
aes.IV = iv;
|
||||
|
||||
using var decryptor = aes.CreateDecryptor();
|
||||
return decryptor.TransformFinalBlock(data, 0, data.Length);
|
||||
}
|
||||
|
||||
private static byte[] AesGcmEncrypt(byte[] data, byte[] key, byte[]? iv, byte[]? aad)
|
||||
{
|
||||
iv ??= RandomNumberGenerator.GetBytes(12);
|
||||
var tag = new byte[16];
|
||||
|
||||
using var aesGcm = new AesGcm(key, 16);
|
||||
var ciphertext = new byte[data.Length];
|
||||
aesGcm.Encrypt(iv, data, ciphertext, tag, aad);
|
||||
|
||||
// Format: IV (12) + Tag (16) + Ciphertext
|
||||
var result = new byte[iv.Length + tag.Length + ciphertext.Length];
|
||||
Array.Copy(iv, 0, result, 0, iv.Length);
|
||||
Array.Copy(tag, 0, result, iv.Length, tag.Length);
|
||||
Array.Copy(ciphertext, 0, result, iv.Length + tag.Length, ciphertext.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] AesGcmDecrypt(byte[] data, byte[] key, byte[]? iv, byte[]? aad)
|
||||
{
|
||||
if (iv == null)
|
||||
{
|
||||
iv = data.Take(12).ToArray();
|
||||
var tag = data.Skip(12).Take(16).ToArray();
|
||||
var ciphertext = data.Skip(28).ToArray();
|
||||
|
||||
using var aesGcm = new AesGcm(key, 16);
|
||||
var plaintext = new byte[ciphertext.Length];
|
||||
aesGcm.Decrypt(iv, ciphertext, tag, plaintext, aad);
|
||||
return plaintext;
|
||||
}
|
||||
else
|
||||
{
|
||||
var tag = data.Take(16).ToArray();
|
||||
var ciphertext = data.Skip(16).ToArray();
|
||||
|
||||
using var aesGcm = new AesGcm(key, 16);
|
||||
var plaintext = new byte[ciphertext.Length];
|
||||
aesGcm.Decrypt(iv, ciphertext, tag, plaintext, aad);
|
||||
return plaintext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for FIPS cryptography plugin.
|
||||
/// </summary>
|
||||
public sealed class FipsOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Require FIPS mode to be enabled at OS level.
|
||||
/// </summary>
|
||||
public bool RequireFipsMode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// RSA key in XML format.
|
||||
/// </summary>
|
||||
public string? RsaKeyXml { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// RSA key size in bits (2048, 3072, 4096).
|
||||
/// </summary>
|
||||
public int RsaKeySize { get; init; } = 2048;
|
||||
|
||||
/// <summary>
|
||||
/// ECDSA key in XML format.
|
||||
/// </summary>
|
||||
public string? EcdsaKeyXml { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// ECDSA curve name (P-256, P-384, P-521).
|
||||
/// </summary>
|
||||
public string EcdsaCurve { get; init; } = "P-256";
|
||||
|
||||
/// <summary>
|
||||
/// Generate keys on initialization if not configured.
|
||||
/// </summary>
|
||||
public bool GenerateKeysOnInit { get; init; } = true;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin\StellaOps.Cryptography.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="plugin.yaml" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,64 @@
|
||||
plugin:
|
||||
id: com.stellaops.crypto.fips
|
||||
name: FIPS 140-2 Cryptography Provider
|
||||
version: 1.0.0
|
||||
vendor: Stella Ops
|
||||
description: US FIPS 140-2 compliant cryptographic algorithms
|
||||
license: AGPL-3.0-or-later
|
||||
|
||||
entryPoint: StellaOps.Cryptography.Plugin.Fips.FipsPlugin
|
||||
|
||||
minPlatformVersion: 1.0.0
|
||||
|
||||
capabilities:
|
||||
- type: crypto
|
||||
id: fips
|
||||
algorithms:
|
||||
- RSA-SHA256
|
||||
- RSA-SHA384
|
||||
- RSA-SHA512
|
||||
- RSA-PSS-SHA256
|
||||
- RSA-PSS-SHA384
|
||||
- RSA-PSS-SHA512
|
||||
- ECDSA-P256-SHA256
|
||||
- ECDSA-P384-SHA384
|
||||
- ECDSA-P521-SHA512
|
||||
- AES-128-CBC
|
||||
- AES-256-CBC
|
||||
- AES-128-GCM
|
||||
- AES-256-GCM
|
||||
- SHA-256
|
||||
- SHA-384
|
||||
- SHA-512
|
||||
- HMAC-SHA256
|
||||
- HMAC-SHA384
|
||||
- HMAC-SHA512
|
||||
|
||||
configSchema:
|
||||
type: object
|
||||
properties:
|
||||
requireFipsMode:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Require FIPS mode to be enabled at OS level
|
||||
rsaKeyXml:
|
||||
type: string
|
||||
description: RSA key in XML format
|
||||
rsaKeySize:
|
||||
type: integer
|
||||
enum: [2048, 3072, 4096]
|
||||
default: 2048
|
||||
description: RSA key size in bits
|
||||
ecdsaKeyXml:
|
||||
type: string
|
||||
description: ECDSA key in XML format
|
||||
ecdsaCurve:
|
||||
type: string
|
||||
enum: [P-256, P-384, P-521]
|
||||
default: P-256
|
||||
description: ECDSA curve name
|
||||
generateKeysOnInit:
|
||||
type: boolean
|
||||
default: true
|
||||
description: Generate keys on initialization
|
||||
required: []
|
||||
Reference in New Issue
Block a user