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;
}
}
}