using System.Net; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using System.Text.Json.Nodes; using Fleet.Identity; namespace MoShiro.Backend.Tests; /// 一个已经建立了信任根的操作员:自己的用户、passkey、会话,以及创世指纹。 internal sealed record Operator(HttpClient Session, SoftwarePasskey Passkey, string Genesis); /// /// 共享的后端上只有一个信任根(创世只能签一次):谁先需要,谁来建立,其余的测试用同一个操作员。 /// 信任相关的测试都从这里拿,不要各自去建立信任根,否则测试的先后顺序会决定谁失败。 /// internal static class SharedOperator { private static readonly ConditionalWeakTable s_operators = []; private static readonly SemaphoreSlim s_gate = new(1, 1); public static async Task GetAsync(BackendFixture backend) { await s_gate.WaitAsync(); try { if (s_operators.TryGetValue(backend, out Operator? existing)) { return existing; } // 操作员:有 passkey、用 passkey 登录的会话 (HttpClient session, SoftwarePasskey passkey, _) = await backend.CreateStrongSessionAsync("operator"); // 创世:用 passkey 对集合本身签名(节点据此验链) JsonNode state = JsonNode.Parse(await session.GetStringAsync("/api/admin/trust"))!; Assert.False(state["initialized"]!.GetValue()); string statement = state["proposal"]!["statement"]!.GetValue(); Assert.Equal(Base64UrlOf(statement), state["proposal"]!["challenge"]!.GetValue()); using HttpResponseMessage genesis = await session.PostAsync("/api/admin/trust", BackendFixture.Json(Sign(passkey, statement))); Assert.True(genesis.StatusCode == HttpStatusCode.Created, await genesis.Content.ReadAsStringAsync()); string fingerprint = JsonNode.Parse(await genesis.Content.ReadAsStringAsync())!["genesis"]!.GetValue(); Assert.Equal(OperatorTrust.Fingerprint(statement), fingerprint); var made = new Operator(session, passkey, fingerprint); s_operators.Add(backend, made); return made; } finally { s_gate.Release(); } } public static string Base64UrlOf(string statement) => Fleet.Core.Base64Url.Encode(SHA256.HashData(Encoding.UTF8.GetBytes(statement))); /// passkey 对语句签名(挑战是语句的 SHA-256),拼成接口要的断言对象。 public static JsonObject Sign(SoftwarePasskey passkey, string statement) { JsonObject assertion = passkey.Assert(Base64UrlOf(statement)); return new JsonObject { ["statement"] = statement, ["credential_id"] = assertion["credential_id"]!.DeepClone(), ["authenticator_data"] = assertion["authenticator_data"]!.DeepClone(), ["client_data_json"] = assertion["client_data_json"]!.DeepClone(), ["signature"] = assertion["signature"]!.DeepClone(), }; } }