using System.Security.Cryptography; using Fleet.Core; namespace MoShiro.Backend.Application; /// 一把发布公钥的指纹(SPKI 的 SHA-256,小写十六进制)和 PEM。 public sealed record ReleaseKeyInfo(string Fingerprint, string PublicKey); /// 可信资产来源:官方 CI 发布密钥(内嵌,始终信任)和可选的自有发布公钥(默认没有)。 public sealed record ReleaseKeys(ReleaseKeyInfo Official, ReleaseKeyInfo? Custom); /// /// 面板信任哪些发布密钥:内嵌的官方 CI 公钥始终信任,不能关;部署的人可以另外导入自己的发布公钥(默认没有), /// 用自己的私钥给自己构建的清单签名。验签(登记清单、取镜像仓库里的清单)通过官方或自有任意一把即可。 /// 私钥只在部署的人手里,面板只存公钥,拒绝带私钥的 PEM。 /// public sealed class ReleaseKeyService { internal const string SettingKey = "release.custom_key"; private const int MaxPemBytes = 1024; private const string P256Oid = "1.2.840.10045.3.1.7"; private readonly IReleaseKey _official; private readonly ISettingsStore _settings; public ReleaseKeyService(IReleaseKey official, ISettingsStore settings) { _official = official; _settings = settings; } public async ValueTask GetAsync(CancellationToken ct) => new(Describe(_official.Pem)!, Describe(await CustomAsync(ct))); /// 清单的签名由官方密钥或自有密钥(开启时)验过。 public async ValueTask VerifyAsync(string manifest, string signature, CancellationToken ct) => ReleaseSignature.Verify(_official.Pem, manifest, signature) || (await CustomAsync(ct) is { } custom && ReleaseSignature.Verify(custom, manifest, signature)); public async ValueTask> SetCustomAsync(string? publicKey, CancellationToken ct) { if (!TryNormalize(publicKey, out string? pem, out string? problem)) { return ValidationError.Fail("public_key", problem); } await _settings.SetAsync(SettingKey, pem, ct); return Result.Ok(await GetAsync(ct)); } public async ValueTask ClearCustomAsync(CancellationToken ct) => await _settings.SetAsync(SettingKey, string.Empty, ct); private async ValueTask CustomAsync(CancellationToken ct) => await _settings.GetAsync(SettingKey, ct) is { Length: > 0 } pem ? pem : null; private static ReleaseKeyInfo? Describe(string? pem) { if (pem is null) { return null; } using var key = ECDsa.Create(); key.ImportFromPem(pem); return new ReleaseKeyInfo(Convert.ToHexStringLower(SHA256.HashData(key.ExportSubjectPublicKeyInfo())), key.ExportSubjectPublicKeyInfoPem().Trim()); } /// 只收 P-256 的公钥:带私钥的 PEM 直接拒绝(私钥不该离开你的机器)。 private static bool TryNormalize(string? text, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? pem, [System.Diagnostics.CodeAnalysis.NotNullWhen(false)] out string? problem) { pem = null; problem = null; string trimmed = (text ?? string.Empty).Trim(); if (trimmed.Length is 0 or > MaxPemBytes) { problem = "must be a PEM public key"; return false; } if (trimmed.Contains("PRIVATE", StringComparison.OrdinalIgnoreCase)) { problem = "must be a public key: never upload the private key, keep it on your own machine"; return false; } try { using var key = ECDsa.Create(); key.ImportFromPem(trimmed); if (key.ExportParameters(false).Curve.Oid.Value != P256Oid) { problem = "must be an ECDSA P-256 public key"; return false; } pem = key.ExportSubjectPublicKeyInfoPem().Trim(); return true; } catch (Exception ex) when (ex is CryptographicException or ArgumentException) { problem = "is not a valid PEM public key"; return false; } } }