using System.Net; using System.Security.Cryptography; using System.Text; using System.Text.Json.Nodes; using Microsoft.Extensions.DependencyInjection; using MoShiro.Backend.Application; namespace MoShiro.Backend.Tests; /// /// 可信资产来源:官方发布密钥始终信任;部署的人可以另外导入自己的发布公钥(默认没有),用自己的私钥给自己构建的清单签名。 /// 面板只收公钥,拒绝私钥;删除之后只有它签过的清单不再被接受,已登记的也丢掉。 /// public sealed class ReleaseKeyTests : IClassFixture { private static long s_seq = TimeProvider.System.GetUtcNow().ToUnixTimeSeconds() * 1000 + 500_000; private readonly BackendFixture _backend; public ReleaseKeyTests(BackendFixture backend) { _backend = backend; } private static string Manifest(long seq) => $$"""{"schema":1,"component":"backend","channel":"dev","image":"registry.example/moshiro-dev/moshiro-backend","digest":"sha256:1111111111111111111111111111111111111111111111111111111111111111","tag":"own-{{seq}}","version":"0.1.1","seq":{{seq}}}"""; private static string Sign(ECDsa key, string manifest) => Convert.ToBase64String(key.SignData(Encoding.UTF8.GetBytes(manifest), HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence)); private async Task PutKeyAsync(string? pem) => await _backend.Admin.PutAsync("/api/admin/release-key", BackendFixture.Json(new JsonObject { ["public_key"] = pem })); private async Task OfferAsync(string manifest, string signature) { using HttpClient anonymous = _backend.NewClient(); return await anonymous.PostAsync("/api/release/offer", BackendFixture.Json(new JsonObject { ["manifest"] = manifest, ["signature"] = signature })); } [Fact] public async Task ByDefaultOnlyTheOfficialKeyIsTrusted_AndItCannotBeRemoved() { using HttpResponseMessage cleared = await _backend.Admin.DeleteAsync("/api/admin/release-key"); Assert.Equal(HttpStatusCode.OK, cleared.StatusCode); JsonNode keys = JsonNode.Parse(await cleared.Content.ReadAsStringAsync())!; Assert.Null(keys["custom"]); Assert.Equal(64, keys["official"]!["fingerprint"]!.GetValue().Length); Assert.StartsWith("-----BEGIN PUBLIC KEY-----", keys["official"]!["public_key"]!.GetValue(), StringComparison.Ordinal); } [Theory] [InlineData("")] [InlineData("not a key")] [InlineData(null)] public async Task ABadKey_IsRefused(string? pem) { using HttpResponseMessage response = await PutKeyAsync(pem); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task ThePrivateKey_IsNeverAccepted_AndOnlyP256IsAllowed() { using var p256 = ECDsa.Create(ECCurve.NamedCurves.nistP256); using var p384 = ECDsa.Create(ECCurve.NamedCurves.nistP384); using HttpResponseMessage privateKey = await PutKeyAsync(p256.ExportPkcs8PrivateKeyPem()); using HttpResponseMessage ecPrivateKey = await PutKeyAsync(p256.ExportECPrivateKeyPem()); using HttpResponseMessage otherCurve = await PutKeyAsync(p384.ExportSubjectPublicKeyInfoPem()); Assert.Equal(HttpStatusCode.BadRequest, privateKey.StatusCode); Assert.Equal(HttpStatusCode.BadRequest, ecPrivateKey.StatusCode); Assert.Equal(HttpStatusCode.BadRequest, otherCurve.StatusCode); Assert.Contains("private key", await privateKey.Content.ReadAsStringAsync(), StringComparison.OrdinalIgnoreCase); } [Fact] public async Task AManifestSignedByTheOwnKey_IsAccepted_UntilTheKeyIsRemoved() { using var own = ECDsa.Create(ECCurve.NamedCurves.nistP256); long first = Interlocked.Increment(ref s_seq); long second = Interlocked.Increment(ref s_seq); // 还没有导入:自己的密钥签的清单不被接受 Assert.Equal(HttpStatusCode.Forbidden, (await OfferAsync(Manifest(first), Sign(own, Manifest(first)))).StatusCode); using HttpResponseMessage set = await PutKeyAsync(own.ExportSubjectPublicKeyInfoPem()); Assert.Equal(HttpStatusCode.OK, set.StatusCode); string fingerprint = Convert.ToHexStringLower(SHA256.HashData(own.ExportSubjectPublicKeyInfo())); Assert.Equal(fingerprint, JsonNode.Parse(await set.Content.ReadAsStringAsync())!["custom"]!["fingerprint"]!.GetValue()); Assert.Equal(HttpStatusCode.Created, (await OfferAsync(Manifest(first), Sign(own, Manifest(first)))).StatusCode); ReleaseService releases = _backend.Services.GetRequiredService(); Assert.Equal(first, (await releases.GetBackendAsync("dev", default))!.Seq); // 别人的密钥照样不行 using var stranger = ECDsa.Create(ECCurve.NamedCurves.nistP256); Assert.Equal(HttpStatusCode.Forbidden, (await OfferAsync(Manifest(second), Sign(stranger, Manifest(second)))).StatusCode); // 删除之后:只有它签过的清单不再被接受,已经登记的也丢掉 using HttpResponseMessage cleared = await _backend.Admin.DeleteAsync("/api/admin/release-key"); Assert.Equal(HttpStatusCode.OK, cleared.StatusCode); Assert.Null(JsonNode.Parse(await cleared.Content.ReadAsStringAsync())!["custom"]); Assert.Equal(HttpStatusCode.Forbidden, (await OfferAsync(Manifest(second), Sign(own, Manifest(second)))).StatusCode); Assert.Null(await releases.GetBackendAsync("dev", default)); } [Fact] public async Task OnlyAnAdministrator_MayChangeTheTrustedKeys() { using HttpClient anonymous = _backend.NewClient(); using var own = ECDsa.Create(ECCurve.NamedCurves.nistP256); using HttpResponseMessage read = await anonymous.GetAsync("/api/admin/release-key"); using HttpResponseMessage write = await anonymous.PutAsync("/api/admin/release-key", BackendFixture.Json(new JsonObject { ["public_key"] = own.ExportSubjectPublicKeyInfoPem() })); Assert.Equal(HttpStatusCode.Unauthorized, read.StatusCode); Assert.Equal(HttpStatusCode.Unauthorized, write.StatusCode); } }