dotnet-cqrs/Svrnty.CQRS.Altcha.Grpc/Protos/altcha.proto
Mathias Beaulieu-Duncan 4446288bb6 feat(altcha): add Svrnty.CQRS.Altcha.Grpc with default verifier + proto
The default transport for IAltchaVerifier / IAltchaChallengeProvider —
calls a self-hosted altcha service over gRPC.

Wire contract
- Protos/altcha.proto defines svrnty.cqrs.altcha.v1.AltchaService with
  CreateChallenge + VerifyChallenge RPCs. Shipped in this package as
  source-of-truth; Go (and other) implementations vendor a copy.
- Challenge.challenge_hash is named (not "challenge") to avoid a C#
  property/class name collision; the MinimalApi widget JSON remaps.

Runtime
- AltchaGrpcVerifier maps RpcException → AltchaVerifyResult.Fail with
  a diagnostic reason ("verify-timeout", "service-unavailable", etc.)
  so the auth check surfaces a clean Unauthorized without leaking
  transport detail.
- AltchaGrpcChallengeProvider lets create-challenge failures bubble
  (challenge endpoint should 5xx if altcha is down — clients retry).
- AltchaGrpcOptions.TokenProvider hook for consumer-supplied HMAC
  service-token minting (plan-b will plug in ServiceTokenIssuer).
- AddGrpcClient<AltchaServiceClient> registered with HttpClientFactory.

AddSvrntyAltchaGrpcVerifier(Action<...>) and overload binding from
IConfiguration cover both wiring styles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:25:59 -04:00

70 lines
2.7 KiB
Protocol Buffer

syntax = "proto3";
package svrnty.cqrs.altcha.v1;
option go_package = "svrnty.cqrs.altcha.v1;altchapb";
option csharp_namespace = "Svrnty.CQRS.Altcha.Grpc";
// AltchaService is the wire contract between any backend that gates
// commands/queries with the [Altcha] attribute and a self-hosted Altcha
// implementation. The default .NET client lives in this package; a
// reference Go server lives in plan-b's projects/altcha (which vendors
// this proto). Keep this file the source of truth for both sides.
service AltchaService {
// Mint a fresh challenge for the widget to solve. Server-stateless:
// the response embeds an HMAC signature that VerifyChallenge re-checks.
rpc CreateChallenge(CreateChallengeRequest) returns (Challenge);
// Verify a widget-produced solution payload. The server checks
// signature + expiry + PoW correctness, then atomically claims the
// challenge in a replay-protection cache (e.g. Redis SETNX) so each
// solution is single-use across all server replicas.
rpc VerifyChallenge(VerifyChallengeRequest) returns (VerifyChallengeResponse);
}
message CreateChallengeRequest {
// Optional per-request complexity override (PoW search-space upper
// bound). Higher = slower client solve. Server clamps to its
// configured min/max. Omit to use the server default.
optional uint32 complexity = 1;
}
message Challenge {
// Hashing algorithm — e.g. "SHA-256" (v2 default). Future versions may
// upgrade to PBKDF2 / Argon2; the field lets the widget pick the
// right solver.
string algorithm = 1;
// Hex-encoded hash the client must find a preimage for. Field is
// named "challenge_hash" rather than "challenge" to avoid a C#
// property/class name collision (the generated message class is
// also named Challenge); JSON projection for the widget remaps it.
string challenge_hash = 2;
// Hex-encoded random salt. Typically embeds the expiry timestamp so
// verifiers can re-derive the TTL without server state.
string salt = 3;
// HMAC-SHA256(secret, algorithm|challenge|salt|maxnumber). Lets
// VerifyChallenge confirm the challenge was issued by this server.
string signature = 4;
// PoW search-space upper bound. Equals the complexity used.
uint32 maxnumber = 5;
}
message VerifyChallengeRequest {
// Base64-encoded JSON payload produced by the Altcha widget — the
// value carried over the wire on IHasAltchaSolution.AltchaSolution.
string payload = 1;
}
message VerifyChallengeResponse {
bool ok = 1;
// Diagnostic only; not surfaced to end users. Suggested values:
// "signature-invalid", "expired", "pow-incorrect", "replayed",
// "redis-unreachable", "malformed".
string reason = 2;
}