86 lines
4.1 KiB
C#
86 lines
4.1 KiB
C#
using System.Net;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using System.Text.Json.Nodes;
|
||
using MoShiro.Backend.Api;
|
||
using MoShiro.Backend.Application;
|
||
|
||
namespace MoShiro.Backend.Tests;
|
||
|
||
/// <summary>
|
||
/// CI 登记清单不需要令牌,但清单必须带 CI 发布密钥的有效签名:令牌存在面板的数据库里,清库、重装或还没有 passkey 的时候就拿不到,
|
||
/// 而清单本来就是签名的,面板直接验签更简单,也没有长期令牌可偷。
|
||
/// </summary>
|
||
[Collection("probe-e2e")]
|
||
public sealed class ReleaseOfferTests
|
||
{
|
||
// openssl 生成的测试密钥对和签名(`openssl dgst -sha256 -sign`,DER):证明验签接受 CI 脚本实际产出的格式
|
||
private const string TestKey = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQwlE2KjYWszeaTsng6CfPxhOqeIV\n+7dm940f7XfHJr9A6/OTiZ2YnhFWWipZ4Z4poPtb6hMW04tIARO8+Km34w==\n-----END PUBLIC KEY-----";
|
||
private const string Manifest = "{\"schema\":1,\"component\":\"agent\",\"seq\":7}";
|
||
private const string OpenSslSignature = "MEUCIDrOloi3otWBiCFcXEsPg6kHCS7G09PrO3hW+3AKXAiZAiEAvXgt9K+iZXnWiEgKdanBKbEoL0LyT/xuaEW6dAG0jDY=";
|
||
|
||
private readonly BackendFixture _backend;
|
||
|
||
public ReleaseOfferTests(BackendFixture backend)
|
||
{
|
||
_backend = backend;
|
||
}
|
||
|
||
[Fact]
|
||
public void ASignatureFromOpenSsl_Verifies_AndAnyChangeDoesNot()
|
||
{
|
||
Assert.True(ReleaseSignature.Verify(TestKey, Manifest, OpenSslSignature));
|
||
|
||
Assert.False(ReleaseSignature.Verify(TestKey, Manifest.Replace("7", "8", StringComparison.Ordinal), OpenSslSignature));
|
||
Assert.False(ReleaseSignature.Verify(TestKey, Manifest + " ", OpenSslSignature));
|
||
|
||
byte[] tampered = Convert.FromBase64String(OpenSslSignature);
|
||
tampered[10] ^= 1;
|
||
Assert.False(ReleaseSignature.Verify(TestKey, Manifest, Convert.ToBase64String(tampered)));
|
||
}
|
||
|
||
[Fact]
|
||
public void AnotherKey_GarbageAndEmptyInputs_AreRejected()
|
||
{
|
||
using var other = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||
string signature = Convert.ToBase64String(other.SignData(Encoding.UTF8.GetBytes(Manifest), HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence));
|
||
|
||
Assert.False(ReleaseSignature.Verify(TestKey, Manifest, signature));
|
||
Assert.False(ReleaseSignature.Verify(TestKey, Manifest, "not base64!!"));
|
||
Assert.False(ReleaseSignature.Verify(TestKey, Manifest, ""));
|
||
Assert.False(ReleaseSignature.Verify("not a key", Manifest, OpenSslSignature));
|
||
// P1363(节点和 WebAuthn 用的格式)不是 CI 签名的格式
|
||
Assert.False(ReleaseSignature.Verify(TestKey, Manifest, Convert.ToBase64String(new byte[64])));
|
||
}
|
||
|
||
[Fact]
|
||
public void TheEmbeddedReleaseKey_IsAUsableP256PublicKey()
|
||
{
|
||
using var key = ECDsa.Create();
|
||
key.ImportFromPem(InstallScripts.ReleaseKey());
|
||
|
||
Assert.Equal(256, key.KeySize);
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData("{\"component\":\"agent\",\"channel\":\"dev\",\"image\":\"r/a\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"seq\":99999999999}", "AAAA")]
|
||
[InlineData("", "")]
|
||
[InlineData("not json", "MEUCIDrOloi3otWBiCFcXEsPg6kHCS7G09PrO3hW+3AKXAiZAiEAvXgt9K+iZXnWiEgKdanBKbEoL0LyT/xuaEW6dAG0jDY=")]
|
||
public async Task OfferWithoutAValidCiSignature_IsForbidden_EvenWithoutAnyToken(string manifest, string signature)
|
||
{
|
||
using HttpClient anonymous = _backend.NewClient();
|
||
using HttpResponseMessage response = await anonymous.PostAsync("/api/release/offer", BackendFixture.Json(new JsonObject { ["manifest"] = manifest, ["signature"] = signature }));
|
||
|
||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task TheAdminEndpoint_StillNeedsTheCapability()
|
||
{
|
||
using HttpClient anonymous = _backend.NewClient();
|
||
using HttpResponseMessage response = await anonymous.PostAsync("/api/admin/releases", BackendFixture.Json(new JsonObject { ["manifest"] = Manifest, ["signature"] = OpenSslSignature }));
|
||
|
||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||
}
|
||
}
|