feat(altcha): add Svrnty.CQRS.Altcha.Abstractions package
Abstractions for the Altcha-based proof-of-work module: - AltchaAttribute (AllowMobileAttestationBypass param) - IHasAltchaSolution — marker interface for request POCOs carrying the widget's solution payload over HTTP/gRPC transports - IAltchaVerifier / IAltchaChallengeProvider — transport-agnostic interfaces; default gRPC implementations ship in Svrnty.CQRS.Altcha.Grpc - IMobileAttestationProvider — Phase 3 placeholder; concrete impls stamp ctx.Items["mobile_attested"] for the Altcha check to read as a bypass when AllowMobileAttestationBypass is true - AltchaChallenge / AltchaVerifyResult DTOs Lean dependencies — only references Svrnty.CQRS.Abstractions for the auth-check pipeline types. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
86d87424ab
commit
118d12a3db
27
Svrnty.CQRS.Altcha.Abstractions/AltchaAttribute.cs
Normal file
27
Svrnty.CQRS.Altcha.Abstractions/AltchaAttribute.cs
Normal file
@ -0,0 +1,27 @@
|
||||
namespace Svrnty.CQRS.Altcha.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Marks a command or query as requiring proof-of-work (or equivalent
|
||||
/// anti-abuse evidence) before the framework will dispatch it to the handler.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The accompanying request type should implement <see cref="IHasAltchaSolution"/>
|
||||
/// to carry the widget's solution payload. The framework's Altcha
|
||||
/// authorization check (registered via <c>AddSvrntyAltcha()</c>) reads the
|
||||
/// solution off the request and calls the configured <see cref="IAltchaVerifier"/>.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public sealed class AltchaAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// When <c>true</c> (default), a valid mobile-attestation token on the
|
||||
/// request satisfies the requirement without needing a proof-of-work
|
||||
/// solution. The Altcha check reads
|
||||
/// <see cref="Svrnty.CQRS.Abstractions.Security.AuthorizationCheckContext.Items"/>[<c>"mobile_attested"</c>]
|
||||
/// — when stamped <c>true</c> by an earlier check (e.g. an Apple
|
||||
/// App Attest / Play Integrity verifier), the PoW check is skipped.
|
||||
/// Set to <c>false</c> on commands where PoW must always run regardless
|
||||
/// of caller.
|
||||
/// </summary>
|
||||
public bool AllowMobileAttestationBypass { get; set; } = true;
|
||||
}
|
||||
28
Svrnty.CQRS.Altcha.Abstractions/AltchaChallenge.cs
Normal file
28
Svrnty.CQRS.Altcha.Abstractions/AltchaChallenge.cs
Normal file
@ -0,0 +1,28 @@
|
||||
namespace Svrnty.CQRS.Altcha.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Server-issued Altcha challenge. Shape matches what the
|
||||
/// <a href="https://altcha.org/docs/v2/widget-v3/">altcha widget v3</a>
|
||||
/// expects from its <c>challengeurl</c>.
|
||||
/// </summary>
|
||||
public sealed class AltchaChallenge
|
||||
{
|
||||
/// <summary>Hashing algorithm name (e.g. <c>SHA-256</c>).</summary>
|
||||
public required string Algorithm { get; init; }
|
||||
|
||||
/// <summary>Hex-encoded hash the client must find a preimage for.</summary>
|
||||
public required string Challenge { get; init; }
|
||||
|
||||
/// <summary>Hex-encoded salt (embeds an expiry timestamp).</summary>
|
||||
public required string Salt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// HMAC-SHA256 of <c>algorithm|challenge|salt|maxnumber</c> using the
|
||||
/// server's signing secret. Lets the verify step trust the issued
|
||||
/// challenge without keeping per-challenge state.
|
||||
/// </summary>
|
||||
public required string Signature { get; init; }
|
||||
|
||||
/// <summary>Upper bound of the PoW search space (equals the requested complexity).</summary>
|
||||
public required uint MaxNumber { get; init; }
|
||||
}
|
||||
20
Svrnty.CQRS.Altcha.Abstractions/AltchaVerifyResult.cs
Normal file
20
Svrnty.CQRS.Altcha.Abstractions/AltchaVerifyResult.cs
Normal file
@ -0,0 +1,20 @@
|
||||
namespace Svrnty.CQRS.Altcha.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Outcome of an Altcha solution verification.
|
||||
/// </summary>
|
||||
public sealed class AltchaVerifyResult
|
||||
{
|
||||
public required bool Ok { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic only — not surfaced to end users. Suggested values:
|
||||
/// <c>signature-invalid</c>, <c>expired</c>, <c>pow-incorrect</c>,
|
||||
/// <c>replayed</c>, <c>redis-unreachable</c>, <c>malformed</c>.
|
||||
/// </summary>
|
||||
public string? Reason { get; init; }
|
||||
|
||||
public static AltchaVerifyResult Success { get; } = new() { Ok = true };
|
||||
|
||||
public static AltchaVerifyResult Fail(string reason) => new() { Ok = false, Reason = reason };
|
||||
}
|
||||
13
Svrnty.CQRS.Altcha.Abstractions/IAltchaChallengeProvider.cs
Normal file
13
Svrnty.CQRS.Altcha.Abstractions/IAltchaChallengeProvider.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Svrnty.CQRS.Altcha.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Mints an Altcha challenge for the widget to solve. The default
|
||||
/// implementation in <c>Svrnty.CQRS.Altcha.Grpc</c> talks to a self-hosted
|
||||
/// altcha service; the <c>Svrnty.CQRS.Altcha.MinimalApi</c> package exposes
|
||||
/// <c>GET /api/altcha/challenge</c> that returns this DTO as the JSON
|
||||
/// shape the widget expects.
|
||||
/// </summary>
|
||||
public interface IAltchaChallengeProvider
|
||||
{
|
||||
Task<AltchaChallenge> CreateAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
18
Svrnty.CQRS.Altcha.Abstractions/IAltchaVerifier.cs
Normal file
18
Svrnty.CQRS.Altcha.Abstractions/IAltchaVerifier.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace Svrnty.CQRS.Altcha.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies an Altcha solution payload. The default implementation in
|
||||
/// <c>Svrnty.CQRS.Altcha.Grpc</c> talks to a self-hosted altcha service
|
||||
/// over gRPC; consumers may register their own implementation (e.g. an
|
||||
/// in-process variant or a different transport) and the
|
||||
/// <see cref="ICommandAuthorizationCheck"/>-based pipeline picks it up
|
||||
/// automatically.
|
||||
/// </summary>
|
||||
public interface IAltchaVerifier
|
||||
{
|
||||
/// <param name="payload">
|
||||
/// Base64-encoded JSON payload produced by the Altcha widget — the same
|
||||
/// string carried on <see cref="IHasAltchaSolution.AltchaSolution"/>.
|
||||
/// </param>
|
||||
Task<AltchaVerifyResult> VerifyAsync(string payload, CancellationToken cancellationToken = default);
|
||||
}
|
||||
16
Svrnty.CQRS.Altcha.Abstractions/IHasAltchaSolution.cs
Normal file
16
Svrnty.CQRS.Altcha.Abstractions/IHasAltchaSolution.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace Svrnty.CQRS.Altcha.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Implemented by command and query POCOs that carry an Altcha widget
|
||||
/// solution. The framework's Altcha check reads <see cref="AltchaSolution"/>
|
||||
/// off the materialized request, so the value travels naturally over HTTP
|
||||
/// (JSON body field) and gRPC (proto field) without any extra plumbing.
|
||||
/// </summary>
|
||||
public interface IHasAltchaSolution
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64-encoded JSON payload produced by the Altcha widget. Null /
|
||||
/// empty causes the check to reject the request.
|
||||
/// </summary>
|
||||
string? AltchaSolution { get; set; }
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
namespace Svrnty.CQRS.Altcha.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 3 placeholder — when a future module implements Apple App Attest /
|
||||
/// Google Play Integrity verification, it stamps
|
||||
/// <see cref="Svrnty.CQRS.Abstractions.Security.AuthorizationCheckContext.Items"/>[<c>"mobile_attested"</c>]
|
||||
/// based on the verification result, and the Altcha check reads that flag
|
||||
/// to short-circuit when <see cref="AltchaAttribute.AllowMobileAttestationBypass"/>
|
||||
/// is true.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Intentionally left abstract and unwired in this phase. The interface
|
||||
/// exists so Phase 3 can drop in an implementation without touching command
|
||||
/// definitions or the Altcha check.
|
||||
/// </remarks>
|
||||
public interface IMobileAttestationProvider
|
||||
{
|
||||
/// <param name="attestationToken">Platform-specific attestation token from the request.</param>
|
||||
/// <returns><c>true</c> if attestation passes; <c>false</c> otherwise.</returns>
|
||||
Task<bool> VerifyAsync(string attestationToken, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<LangVersion>14</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<Company>Svrnty</Company>
|
||||
<Authors>David Lebee, Mathias Beaulieu-Duncan</Authors>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://git.openharbor.io/svrnty/dotnet-cqrs</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
||||
<DebugType>portable</DebugType>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<IncludeSource>true</IncludeSource>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\icon.png" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="" CopyToOutputDirectory="Always" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Svrnty.CQRS.Abstractions\Svrnty.CQRS.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -43,6 +43,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Events.Abstract
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Events.RabbitMQ", "Svrnty.CQRS.Events.RabbitMQ\Svrnty.CQRS.Events.RabbitMQ.csproj", "{3C7412EF-13C2-41F3-9D4C-D2BEC4843C8C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Altcha.Abstractions", "Svrnty.CQRS.Altcha.Abstractions\Svrnty.CQRS.Altcha.Abstractions.csproj", "{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -257,6 +259,18 @@ Global
|
||||
{3C7412EF-13C2-41F3-9D4C-D2BEC4843C8C}.Release|x64.Build.0 = Release|Any CPU
|
||||
{3C7412EF-13C2-41F3-9D4C-D2BEC4843C8C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{3C7412EF-13C2-41F3-9D4C-D2BEC4843C8C}.Release|x86.Build.0 = Release|Any CPU
|
||||
{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Release|x64.Build.0 = Release|Any CPU
|
||||
{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Loading…
Reference in New Issue
Block a user