Miscelanious trials

This commit is contained in:
eelke 2026-02-06 19:58:01 +01:00
commit f99c97f392
33 changed files with 881 additions and 0 deletions

View file

@ -0,0 +1,6 @@
namespace IdentityShroud.Core.Contracts;
public interface ISecretProvider
{
Task<string> GetSecretAsync(string name);
}

38
IdentityShroud.Core/Db.cs Normal file
View file

@ -0,0 +1,38 @@
using IdentityShroud.Core.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace IdentityShroud.Core;
public class DbConfiguration
{
public string ConnectionString { get; set; } = "";
public bool LogSensitiveData { get; set; } = false;
}
public class Db(
IOptions<DbConfiguration> configuration,
ILoggerFactory? loggerFactory)
: DbContext
{
public virtual DbSet<Realm> Realms { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql("<connection string>");
optionsBuilder.UseNpgsql(
configuration.Value.ConnectionString,
o => o.MigrationsAssembly("IdentityShroud.Migrations")); // , o => o.UseNodaTime().UseVector().MigrationsAssembly("Migrations.KnowledgeBaseDB"));
optionsBuilder.UseSnakeCaseNamingConvention();
if (configuration.Value.LogSensitiveData)
optionsBuilder.EnableSensitiveDataLogging();
if (loggerFactory is { } )
{
optionsBuilder.UseLoggerFactory(loggerFactory);
}
}
}

View file

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="10.0.1" />
<PackageReference Include="jose-jwt" Version="5.2.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.WebUtilities">
<HintPath>..\..\..\.nuget\packages\microsoft.aspnetcore.webutilities\10.0.2\lib\net10.0\Microsoft.AspNetCore.WebUtilities.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,34 @@
using System.Text.Json.Serialization;
namespace IdentityShroud.Core.Messages;
public class JsonWebKey
{
[JsonPropertyName("kty")]
public string KeyType { get; set; } = "RSA";
[JsonPropertyName("use")]
public string Use { get; set; } = "sig"; // "sig" for signature, "enc" for encryption
[JsonPropertyName("alg")]
public string Algorithm { get; set; } = "RS256";
[JsonPropertyName("kid")]
public string KeyId { get; set; }
// RSA Public Key Components
[JsonPropertyName("n")]
public string Modulus { get; set; }
[JsonPropertyName("e")]
public string Exponent { get; set; }
// Optional fields
[JsonPropertyName("x5c")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<string> X509CertificateChain { get; set; }
[JsonPropertyName("x5t")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string X509CertificateThumbprint { get; set; }
}

View file

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace IdentityShroud.Core.Messages;
public class JsonWebKeySet
{
[JsonPropertyName("keys")]
public List<JsonWebKey> Keys { get; set; } = new List<JsonWebKey>();
}

View file

@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
namespace IdentityShroud.Core.Messages;
public class JsonWebTokenHeader
{
[JsonPropertyName("alg")]
public string Algorithm { get; set; } = "HS256";
[JsonPropertyName("typ")]
public string Type { get; set; } = "JWT";
[JsonPropertyName("kid")]
public string KeyId { get; set; }
}
public class JsonWebTokenPayload
{
[JsonPropertyName("iss")]
public string Issuer { get; set; }
[JsonPropertyName("aud")]
public string[] Audience { get; set; }
[JsonPropertyName("sub")]
public string Subject { get; set; }
[JsonPropertyName("exp")]
public long Expires { get; set; }
[JsonPropertyName("iat")]
public long IssuedAt { get; set; }
[JsonPropertyName("nbf")]
public long NotBefore { get; set; }
[JsonPropertyName("jti")]
public Guid JwtId { get; set; }
}
public class JsonWebToken
{
public JsonWebTokenHeader Header { get; set; } = new();
public JsonWebTokenPayload Payload { get; set; } = new();
public byte[] Signature { get; set; } = [];
}

View file

@ -0,0 +1,72 @@
using System.Text.Json.Serialization;
namespace IdentityShroud.Core.Messages;
/// <summary>
/// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
/// </summary>
public class OpenIdConfiguration
{
/// <summary>
/// REQUIRED. URL using the https scheme with no query or fragment components that the OP asserts as its
/// Issuer Identifier. If Issuer discovery is supported (see Section 2), this value MUST be identical to the
/// issuer value returned by WebFinger. This also MUST be identical to the iss Claim value in ID Tokens issued
/// from this Issuer.
/// </summary>
[JsonPropertyName("issuer")]
public required string Issuer { get; set; }
/// <summary>
/// REQUIRED. URL of the OP's OAuth 2.0 Authorization Endpoint [OpenID.Core]. This URL MUST use the https scheme
/// and MAY contain port, path, and query parameter components.
/// </summary>
[JsonPropertyName("authorization_endpoint")]
public required string AuthorizationEndpoint { get; set; }
/// <summary>
/// URL of the OP's OAuth 2.0 Token Endpoint [OpenID.Core]. This is REQUIRED unless only the Implicit Flow is used.
/// This URL MUST use the https scheme and MAY contain port, path, and query parameter components.
/// </summary>
[JsonPropertyName("token_endpoint")]
public string? TokenEndpoint { get; set; }
/// <summary>
/// RECOMMENDED. URL of the OP's UserInfo Endpoint [OpenID.Core]. This URL MUST use the https scheme and MAY contain
/// port, path, and query parameter components.
/// </summary>
[JsonPropertyName("userinfo_endpoint")]
public string? UserInfoEndpoint { get; set; }
/// <summary>
/// REQUIRED. URL of the OP's JWK Set [JWK] document, which MUST use the https scheme. This contains the signing
/// key(s) the RP uses to validate signatures from the OP. The JWK Set MAY also contain the Server's encryption
/// key(s), which are used by RPs to encrypt requests to the Server. When both signing and encryption keys are made
/// available, a use (public key use) parameter value is REQUIRED for all keys in the referenced JWK Set to indicate
/// each key's intended usage. Although some algorithms allow the same key to be used for both signatures and
/// encryption, doing so is NOT RECOMMENDED, as it is less secure. The JWK x5c parameter MAY be used to provide
/// X.509 representations of keys provided. When used, the bare key values MUST still be present and MUST match
/// those in the certificate. The JWK Set MUST NOT contain private or symmetric key values.
/// </summary>
[JsonPropertyName("jwks_uri")]
public required string JwksUri { get; set; }
/// <summary>
/// REQUIRED. JSON array containing a list of the OAuth 2.0 response_type values that this OP supports. Dynamic
/// OpenID Providers MUST support the code, id_token, and the id_token token Response Type values.
/// </summary>
[JsonPropertyName("response_types_supported")]
public string[] ResponseTypesSupported { get; set; } = [ "code", "id_token", "id_token token"];
/// <summary>
/// REQUIRED. JSON array containing a list of the Subject Identifier types that this OP supports. Valid types
/// include pairwise and public.
/// </summary>
[JsonPropertyName("subject_types_supported")]
public string[] SubjectTypesSupported { get; set; } = [ "public" ];
/// <summary>
/// REQUIRED. JSON array containing a list of the JWS signing algorithms (alg values) supported by the OP for the
/// ID Token to encode the Claims in a JWT [JWT]. The algorithm RS256 MUST be included. The value none MAY be
/// supported but MUST NOT be used unless the Response Type used returns no ID Token from the Authorization
/// Endpoint (such as when using the Authorization Code Flow).
/// </summary>
[JsonPropertyName("id_token_signing_alg_values_supported")]
public string[] IdTokenSigningAlgValuesSupported { get; set; } = [ "RS256" ];
}

View file

@ -0,0 +1,7 @@
namespace IdentityShroud.Core.Model;
public class Client
{
public Guid Id { get; set; }
public string Name { get; set; }
}

View file

@ -0,0 +1,10 @@
namespace IdentityShroud.Core.Model;
public class Realm
{
public Guid Id { get; set; }
public string Slug { get; set; } = "";
public string Description { get; set; } = "";
public List<Client> Clients { get; set; } = [];
public byte[] PrivateKey { get; set; }
}

View file

@ -0,0 +1,64 @@
using System.Security.Cryptography;
namespace IdentityShroud.Core.Security;
public static class AesGcmHelper
{
public static byte[] EncryptAesGcm(byte[] plaintext, byte[] key)
{
using var aes = new AesGcm(key);
byte[] nonce = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize);
byte[] ciphertext = new byte[plaintext.Length];
byte[] tag = new byte[AesGcm.TagByteSizes.MaxSize];
aes.Encrypt(nonce, plaintext, ciphertext, tag);
// Return concatenated nonce|ciphertext|tag (or store separately)
return nonce.Concat(ciphertext).Concat(tag).ToArray();
}
// --------------------------------------------------------------------
// DecryptAesGcm
// • key 32byte (256bit) secret key (same key used for encryption)
// • payload byte[] containing nonce‖ciphertext‖tag
// • returns the original plaintext bytes
// --------------------------------------------------------------------
public static byte[] DecryptAesGcm(byte[] payload, byte[] key)
{
if (payload == null) throw new ArgumentNullException(nameof(payload));
if (key == null) throw new ArgumentNullException(nameof(key));
if (key.Length != 32) // 256bit key
throw new ArgumentException("Key must be 256bits (32 bytes) for AES256GCM.", nameof(key));
// ----------------------------------------------------------------
// 1⃣ Extract the three components.
// ----------------------------------------------------------------
// AesGcm.NonceByteSizes.MaxSize = 12 bytes (standard GCM nonce length)
// AesGcm.TagByteSizes.MaxSize = 16 bytes (128bit authentication tag)
int nonceSize = AesGcm.NonceByteSizes.MaxSize; // 12
int tagSize = AesGcm.TagByteSizes.MaxSize; // 16
if (payload.Length < nonceSize + tagSize)
throw new ArgumentException("Payload is too short to contain nonce, ciphertext, and tag.", nameof(payload));
ReadOnlySpan<byte> nonce = new(payload, 0, nonceSize);
ReadOnlySpan<byte> ciphertext = new(payload, nonceSize, payload.Length - nonceSize - tagSize);
ReadOnlySpan<byte> tag = new(payload, payload.Length - tagSize, tagSize);
byte[] plaintext = new byte[ciphertext.Length];
using var aes = new AesGcm(key);
try
{
aes.Decrypt(nonce, ciphertext, tag, plaintext);
}
catch (CryptographicException ex)
{
// Tag verification failed → tampering or wrong key/nonce.
throw new InvalidOperationException("Decryption failed authentication tag mismatch.", ex);
}
return plaintext;
}
}

View file

@ -0,0 +1,38 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.WebUtilities;
namespace IdentityShroud.Core;
public class JwtSignatureGenerator
{
/// <summary>
/// Generates a JWT signature using RS256 algorithm
/// </summary>
/// <param name="headerBase64Url">Base64Url encoded header</param>
/// <param name="payloadBase64Url">Base64Url encoded payload</param>
/// <param name="privateKey">RSA private key (PEM format or RSA parameters)</param>
/// <returns>Base64Url encoded signature</returns>
public static string GenerateRS256Signature(string headerBase64Url, string payloadBase64Url, RSA privateKey)
{
// Combine header and payload with a period
string dataToSign = $"{headerBase64Url}.{payloadBase64Url}";
// Convert to bytes
byte[] dataBytes = Encoding.UTF8.GetBytes(dataToSign);
// Sign the data using RSA-SHA256
byte[] signatureBytes = privateKey.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// Convert signature to Base64Url encoding
string signature = WebEncoders.Base64UrlEncode(signatureBytes);
return signature;
}
public static string GenerateCompleteJwt(string headerBase64Url, string payloadBase64Url, RSA privateKey)
{
string signature = GenerateRS256Signature(headerBase64Url, payloadBase64Url, privateKey);
return $"{headerBase64Url}.{payloadBase64Url}.{signature}";
}
}