Files
justme ad5efc62ff agent: unify listening and dialing with directed mutual measurements
Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry.

Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
2026-10-21 11:55:36 +08:00

87 lines
4.5 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
public sealed class ReleaseOfferTests : IClassFixture<BackendFixture>
{
// 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 OfferWithAValidSignature_IsAccepted_WithoutAnyToken()
{
long seq = TimeProvider.System.GetUtcNow().ToUnixTimeSeconds() * 1000; // backend 的 release 通道:不和别的用例抢 agent 的 seq
string manifest = $$"""{"schema":1,"component":"backend","channel":"release","image":"registry.example/moshiro/moshiro-backend","digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","tag":"dev-x","version":"0.1.1","seq":{{seq}}}""";
using HttpClient anonymous = _backend.NewClient();
using HttpResponseMessage offered = await anonymous.PostAsync("/api/release/offer", BackendFixture.Json(new JsonObject { ["manifest"] = manifest, ["signature"] = TestReleaseKey.Sign(manifest) }));
Assert.Equal(HttpStatusCode.Created, offered.StatusCode);
}
}