-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
172 lines (144 loc) · 7.7 KB
/
Copy pathProgram.cs
File metadata and controls
172 lines (144 loc) · 7.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
using System.Text;
using VerifyBlind.TestPortal.Services;
DotNetEnv.Env.Load();
var builder = WebApplication.CreateBuilder(args);
// ── Sentry ──────────────────────────────────────────────────────────────────
// 3 example portal (nextjs/dotnet/php) tek "verifyblind-examples" projesine raporlar;
// example_stack tag'i hangisi olduğunu ayırır. DSN yoksa Sentry no-op kalır.
builder.WebHost.UseSentry(o =>
{
o.Dsn = Environment.GetEnvironmentVariable("EXAMPLES_SENTRY_DSN") ?? "";
o.Environment = builder.Environment.EnvironmentName;
o.TracesSampleRate = 0.1;
o.SendDefaultPii = false;
o.SetBeforeSend((Sentry.SentryEvent e) =>
{
e.SetTag("example_stack", "dotnet");
return e;
});
});
// ──────────────────────────────────────────────────────────────────────────
builder.Services.AddRazorPages();
builder.Services.AddHttpClient();
// Demo nonce store for PoP login replay protection (Redis/DB in production — see
// Documents/Production_Checklist.md item 4.8). In-memory only works single-instance.
builder.Services.AddSingleton<InMemoryTtlStore>();
var app = builder.Build();
// ── Sentry açılış ping'i (sağlık kontrolü) ─────────────────────────────────
// Her açılışta tek Info event'i; example_stack=dotnet tag'i BeforeSend ile eklenir.
Sentry.SentrySdk.CaptureMessage("[startup] example-web-dotnet up", Sentry.SentryLevel.Info);
// /net alt yolu altında sunulduğunda doğru URL üretimi için
app.UsePathBase("/net");
// Statik index.html'i / icin default file olarak serve et (widget)
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapRazorPages();
// ─── POST /api/verify ───────────────────────────────────────────────────────
// token (base64 { payload, signature }) → Enclave RSA-PSS imzasını doğrular
app.MapPost("/api/verify", async (HttpContext ctx, IHttpClientFactory httpClientFactory, InMemoryTtlStore nonceStore) =>
{
var apiUrl = Environment.GetEnvironmentVariable("VERIFYBLIND_API_URL") ?? "https://api.verifyblind.com";
try
{
var body = await new StreamReader(ctx.Request.Body).ReadToEndAsync();
using var doc = System.Text.Json.JsonDocument.Parse(body);
if (!doc.RootElement.TryGetProperty("token", out var tokenEl))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "token gerekli" });
return;
}
var tokenBytes = Convert.FromBase64String(tokenEl.GetString()!);
using var signedDoc = System.Text.Json.JsonDocument.Parse(tokenBytes);
var payload = signedDoc.RootElement.GetProperty("payload").GetString()!;
var sigBase64 = signedDoc.RootElement.GetProperty("signature").GetString()!;
// Enclave public key'i al
var httpClient = httpClientFactory.CreateClient();
var keyRes = await httpClient.GetAsync($"{apiUrl}/api/public/enclave-key");
if (!keyRes.IsSuccessStatusCode)
{
ctx.Response.StatusCode = 502;
await ctx.Response.WriteAsJsonAsync(new { error = "Enclave public key alınamadı" });
return;
}
var pubKeyBase64 = (await keyRes.Content.ReadAsStringAsync()).Trim();
var pubKeyDer = Convert.FromBase64String(pubKeyBase64);
using var rsa = System.Security.Cryptography.RSA.Create();
rsa.ImportSubjectPublicKeyInfo(pubKeyDer, out _);
var isValid = rsa.VerifyData(
Encoding.UTF8.GetBytes(payload),
Convert.FromBase64String(sigBase64),
System.Security.Cryptography.HashAlgorithmName.SHA256,
System.Security.Cryptography.RSASignaturePadding.Pss
);
if (!isValid)
{
ctx.Response.StatusCode = 401;
await ctx.Response.WriteAsJsonAsync(new { error = "Geçersiz imza" });
return;
}
var data = System.Text.Json.JsonDocument.Parse(payload).RootElement;
// Replay protection: the enclave echoes the QR session nonce into the signed
// payload. Bind it to a session this portal generated and consume it once.
if (!data.TryGetProperty("nonce", out var nonceEl) || nonceEl.GetString() is not { Length: > 0 } sessionNonce)
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "Geçersiz oturum (nonce yok)" });
return;
}
if (nonceStore.GetAndRemove($"popnonce:{sessionNonce}") is null)
{
ctx.Response.StatusCode = 401;
await ctx.Response.WriteAsJsonAsync(new { error = "Oturum süresi dolmuş veya zaten kullanılmış" });
return;
}
app.Logger.LogInformation("[Verify] ✅ İmza doğrulandı");
await ctx.Response.WriteAsJsonAsync(new { success = true, data });
}
catch (Exception ex)
{
app.Logger.LogError(ex, "[Verify] Hata");
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { error = ex.Message });
}
});
// ─── POST /api/generate ─────────────────────────────────────────────────────
// /api/pop/generate proxy — tarayıcıdan gelen { public_key, validations } isteğini
// X-API-Key ekleyerek VerifyBlind API'ye iletir.
app.MapPost("/api/generate", async (HttpContext ctx, IHttpClientFactory httpClientFactory, InMemoryTtlStore nonceStore) =>
{
var apiKey = Environment.GetEnvironmentVariable("TEST_VERIFYBLIND_API_KEY") ?? "";
var apiUrl = Environment.GetEnvironmentVariable("VERIFYBLIND_API_URL") ?? "https://api.verifyblind.com";
if (string.IsNullOrEmpty(apiKey))
{
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { error = "TEST_VERIFYBLIND_API_KEY yapılandırılmamış" });
return;
}
var body = await new StreamReader(ctx.Request.Body).ReadToEndAsync();
var httpClient = httpClientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Post, $"{apiUrl}/api/pop/generate");
request.Headers.Add("X-API-Key", apiKey);
// Forward the browser's language so VerifyBlind localizes errors (tr/en).
var acceptLang = ctx.Request.Headers.AcceptLanguage.ToString();
request.Headers.TryAddWithoutValidation("Accept-Language", string.IsNullOrEmpty(acceptLang) ? "tr" : acceptLang);
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
var response = await httpClient.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();
// On success, remember the nonce so /api/verify can bind & one-time-consume it.
if (response.IsSuccessStatusCode)
{
try
{
using var doc = System.Text.Json.JsonDocument.Parse(responseBody);
if (doc.RootElement.TryGetProperty("nonce", out var nEl) && nEl.GetString() is { Length: > 0 } nonce)
nonceStore.Set($"popnonce:{nonce}", "1", 600);
}
catch { /* non-JSON error body — ignore */ }
}
app.Logger.LogInformation("[Generate] API yanıtı: {StatusCode}", (int)response.StatusCode);
ctx.Response.StatusCode = (int)response.StatusCode;
ctx.Response.ContentType = "application/json";
await ctx.Response.WriteAsync(responseBody);
});
app.Run();