feat!: probes generate their own keys and enroll with a join token; node and peer keys are separate [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
This commit is contained in:
1 parent
1ff847e45a
commit
a9f562441f
51 files changed
+505
-749
No files matched your search
@@ -63,18 +63,19 @@ public sealed class AgentHarness : IAsyncDisposable
|
||||
return harness;
|
||||
}
|
||||
|
||||
/// <summary>在后端生成 Probe 身份(私钥只返回一次),并构造连到这个 Agent 的假 Probe。</summary>
|
||||
/// <summary>注册一个 Probe(密钥由它自己生成),并构造连到这个 Agent 的假 Probe:对等面用它的对等面密钥。</summary>
|
||||
public async Task<ProbeSimulator> CreateProbeAsync()
|
||||
{
|
||||
JsonNode identity = await Backend.CreateProbeAsync();
|
||||
NodeIdentity key = NodeIdentity.FromPkcs8(Convert.FromBase64String(identity["private_key"]!.GetValue<string>()));
|
||||
using EnrolledProbe probe = await Backend.CreateProbeAsync();
|
||||
// 假 Probe 要在注册目录被释放之后继续用这把密钥:导出再导入一份
|
||||
NodeIdentity key = NodeIdentity.FromPkcs8(probe.Identity.PeerIdentity.ExportPkcs8());
|
||||
string publicKey = Convert.ToBase64String(key.PublicKeySpki);
|
||||
|
||||
// 等 Agent 的期望状态里有了这把公钥(否则握手会被拒绝)
|
||||
await BackendFixture.EventuallyAsync(
|
||||
() => Task.FromResult(App.IsPeerKnown(identity["probe_id"]!.GetValue<string>(), publicKey)),
|
||||
because: "the agent should learn the probe's key");
|
||||
return new ProbeSimulator(identity["probe_id"]!.GetValue<string>(), key);
|
||||
() => Task.FromResult(App.IsPeerKnown(probe.ProbeId, publicKey)),
|
||||
because: "the agent should learn the probe's peer key");
|
||||
return new ProbeSimulator(probe.ProbeId, key);
|
||||
}
|
||||
|
||||
public async Task<ProbeSimulator> ConnectProbeAsync()
|
||||
|
||||
@@ -37,9 +37,15 @@ public sealed class AotSmokeTests
|
||||
|
||||
using HttpResponseMessage settingsResponse = await http.PatchAsync("/api/admin/settings", BackendFixture.Json(new JsonObject { ["site.public"] = true }, "application/merge-patch+json"));
|
||||
settingsResponse.EnsureSuccessStatusCode();
|
||||
using HttpResponseMessage probeResponse = await http.PostAsync("/api/admin/probes", BackendFixture.Json(new JsonObject { ["name"] = "aot-probe-" + Guid.NewGuid().ToString("N")[..6] }));
|
||||
probeResponse.EnsureSuccessStatusCode();
|
||||
string probeId = JsonNode.Parse(await probeResponse.Content.ReadAsStringAsync())!["probe_id"]!.GetValue<string>();
|
||||
// Probe:probe 角色的加入令牌 → 节点自己生成密钥并注册
|
||||
using HttpResponseMessage probeJoin = await http.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "probe", ["max_uses"] = 1 }));
|
||||
probeJoin.EnsureSuccessStatusCode();
|
||||
string probeToken = JsonNode.Parse(await probeJoin.Content.ReadAsStringAsync())!["plaintext"]!.GetValue<string>();
|
||||
using var probeHttp = new HttpClient();
|
||||
Fleet.Core.Result<Fleet.Enrollment.Client.EnrollmentResult> probeEnrolled = await Fleet.Enrollment.Client.EnrollmentClient.EnrollAsync(
|
||||
probeHttp, baseAddress, probeToken, "aot-probe-" + Guid.NewGuid().ToString("N")[..6], Path.Combine(Path.GetTempPath(), "moshiro-aot-probe-" + Guid.NewGuid().ToString("N")), "0.0.0", null, TimeProvider.System, default);
|
||||
Assert.True(probeEnrolled.IsSuccess, probeEnrolled.IsSuccess ? "" : probeEnrolled.Error.ToString());
|
||||
string probeId = probeEnrolled.Value.NodeId;
|
||||
|
||||
// Agent:加入令牌 → 注册(ECDSA 持有证明)→ WebSocket 签名认证 → 期望状态
|
||||
using HttpResponseMessage joinResponse = await http.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["max_uses"] = 1 }));
|
||||
|
||||
@@ -53,13 +53,26 @@ public class BackendFixture : IAsyncLifetime
|
||||
|
||||
public string Line(string agentId) => $"{agentId}~{DefaultProbeId}";
|
||||
|
||||
/// <summary>在后端创建一个新的 Probe 身份,返回响应里的 JSON(含私钥,只此一次)。</summary>
|
||||
public async Task<JsonNode> CreateProbeAsync(string? name = null)
|
||||
/// <summary>
|
||||
/// 注册一个新的 Probe:和真实部署一样,先在管理端创建 <c>probe</c> 角色的加入令牌,再由节点自己生成密钥、用 <c>fleet-enroll</c> 同样的流程注册。
|
||||
/// 返回的身份目录里有两把密钥:连面板的 <c>Identity</c> 和对等面的 <c>PeerIdentity</c>。调用方负责释放。
|
||||
/// </summary>
|
||||
public async Task<EnrolledProbe> CreateProbeAsync(string? name = null)
|
||||
{
|
||||
using HttpResponseMessage response = await Admin.PostAsync("/api/admin/probes", Json(new JsonObject { ["name"] = name ?? UniqueName("probe") }));
|
||||
string body = await response.Content.ReadAsStringAsync();
|
||||
Assert.True(response.IsSuccessStatusCode, body);
|
||||
return JsonNode.Parse(body)!;
|
||||
string token = await CreateJoinTokenAsync(role: "probe");
|
||||
string directory = Path.Combine(Path.GetTempPath(), "moshiro-probe-id-" + Guid.NewGuid().ToString("N"));
|
||||
using var http = new HttpClient();
|
||||
Fleet.Core.Result<Fleet.Enrollment.Client.EnrollmentResult> enrolled = await Fleet.Enrollment.Client.EnrollmentClient.EnrollAsync(
|
||||
http, BaseAddress, token, name ?? UniqueName("probe"), directory, "0.0.0-test", null, TimeProvider.System, default);
|
||||
Assert.True(enrolled.IsSuccess, enrolled.IsSuccess ? "" : enrolled.Error.ToString());
|
||||
return new EnrolledProbe(enrolled.Value.NodeId, directory, Fleet.Hosting.LoadedNodeIdentity.Load(directory));
|
||||
}
|
||||
|
||||
/// <summary>注册一个 Probe,只要它的 id(密钥用完即释放)。</summary>
|
||||
public async Task<string> CreateProbeIdAsync(string? name = null)
|
||||
{
|
||||
using EnrolledProbe probe = await CreateProbeAsync(name);
|
||||
return probe.ProbeId;
|
||||
}
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
@@ -68,7 +81,10 @@ public class BackendFixture : IAsyncLifetime
|
||||
await StartAsync();
|
||||
_adminToken = await CreateAdminTokenAsync();
|
||||
CreateClients();
|
||||
DefaultProbeId = (await CreateProbeAsync("default"))["probe_id"]!.GetValue<string>();
|
||||
using (EnrolledProbe probe = await CreateProbeAsync("default"))
|
||||
{
|
||||
DefaultProbeId = probe.ProbeId;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
@@ -123,9 +139,9 @@ public class BackendFixture : IAsyncLifetime
|
||||
|
||||
public string UniqueName(string prefix) => $"{prefix}-{Interlocked.Increment(ref _counter)}";
|
||||
|
||||
public async Task<string> CreateJoinTokenAsync(bool autoApprove = true, int maxUses = 1, int expiresInHours = 1)
|
||||
public async Task<string> CreateJoinTokenAsync(bool autoApprove = true, int maxUses = 1, int expiresInHours = 1, string role = "agent")
|
||||
{
|
||||
using HttpResponseMessage response = await Admin.PostAsync("/api/admin/join-tokens", Json(new JsonObject { ["auto_approve"] = autoApprove, ["max_uses"] = maxUses, ["expires_in_hours"] = expiresInHours }));
|
||||
using HttpResponseMessage response = await Admin.PostAsync("/api/admin/join-tokens", Json(new JsonObject { ["role"] = role, ["auto_approve"] = autoApprove, ["max_uses"] = maxUses, ["expires_in_hours"] = expiresInHours }));
|
||||
response.EnsureSuccessStatusCode();
|
||||
return JsonNode.Parse(await response.Content.ReadAsStringAsync())!["plaintext"]!.GetValue<string>();
|
||||
}
|
||||
@@ -235,3 +251,9 @@ public sealed class StrictLoginFixture : BackendFixture
|
||||
{
|
||||
protected override int LoginAttemptsPerMinute => 10;
|
||||
}
|
||||
|
||||
/// <summary>注册好的 Probe:身份目录(节点自己生成的两把密钥)和加载出来的身份。</summary>
|
||||
public sealed record EnrolledProbe(string ProbeId, string Directory, Fleet.Hosting.LoadedNodeIdentity Identity) : IDisposable
|
||||
{
|
||||
public void Dispose() => Identity.Dispose();
|
||||
}
|
||||
@@ -93,6 +93,31 @@ public sealed class EnrollmentAndTaskSetTests
|
||||
Assert.DoesNotContain(tokens["items"]!.AsArray(), t => t!["id"]!.GetValue<string>() == id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Enrollment_RejectsATlsPublicKeyThatDoesNotMatchItsFingerprint_AndAnUnknownRole()
|
||||
{
|
||||
string token = await _backend.CreateJoinTokenAsync(role: "probe");
|
||||
using var mine = Fleet.Identity.NodeIdentity.Generate();
|
||||
using var tls = Fleet.Identity.NodeIdentity.Generate();
|
||||
using var impostor = Fleet.Identity.NodeIdentity.Generate();
|
||||
string pubkey = Convert.ToBase64String(mine.PublicKeySpki);
|
||||
string hash = Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(token)));
|
||||
// 指纹是 tls 的,公钥却换成了别的:Agent 将来按指纹校验证书,两者必须对得上
|
||||
using HttpResponseMessage response = await PostEnrollAsync(new JsonObject
|
||||
{
|
||||
["join_token"] = token,
|
||||
["name"] = _backend.UniqueName("tls"),
|
||||
["pubkey"] = pubkey,
|
||||
["tls_spki"] = Convert.ToBase64String(Fleet.Identity.NodeIdentity.Fingerprint(tls.PublicKeySpki)),
|
||||
["tls_pubkey"] = Convert.ToBase64String(impostor.PublicKeySpki),
|
||||
["proof"] = Convert.ToBase64String(mine.Sign(Fleet.Identity.SignaturePurpose.Enroll, hash, pubkey)),
|
||||
});
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
|
||||
using HttpResponseMessage badRole = await _backend.Admin.PostAsync("/api/admin/join-tokens", BackendFixture.Json(new JsonObject { ["role"] = "root" }));
|
||||
Assert.True(badRole.StatusCode is HttpStatusCode.BadRequest or HttpStatusCode.UnprocessableEntity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Enrollment_RejectsAProofSignedByAnotherKey()
|
||||
{
|
||||
@@ -107,7 +132,8 @@ public sealed class EnrollmentAndTaskSetTests
|
||||
["join_token"] = token,
|
||||
["name"] = _backend.UniqueName("forged"),
|
||||
["pubkey"] = pubkey,
|
||||
["tls_spki"] = Convert.ToBase64String(new byte[32]),
|
||||
["tls_spki"] = Convert.ToBase64String(Fleet.Identity.NodeIdentity.Fingerprint(mine.PublicKeySpki)),
|
||||
["tls_pubkey"] = pubkey,
|
||||
["proof"] = forged,
|
||||
});
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
@@ -327,47 +353,52 @@ public sealed class EnrollmentAndTaskSetTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProbeLifecycle_CreateRotateRevokeDelete_IsReflectedInEveryAgentsPeerList()
|
||||
public async Task ProbeLifecycle_EnrollRenameRevokeDelete_IsReflectedInEveryAgentsPeerList()
|
||||
{
|
||||
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
|
||||
await agent.WaitForTaskSetAsync(s => s.Tasks.Count == Defaults.TaskCount);
|
||||
|
||||
using HttpResponseMessage created = await _backend.Admin.PostAsync("/api/admin/probes", BackendFixture.Json(new JsonObject { ["name"] = _backend.UniqueName("home") }));
|
||||
Assert.Equal(HttpStatusCode.Created, created.StatusCode);
|
||||
Assert.Equal("no-store", created.Headers.CacheControl!.ToString());
|
||||
JsonNode identity = JsonNode.Parse(await created.Content.ReadAsStringAsync())!;
|
||||
string id = identity["probe_id"]!.GetValue<string>();
|
||||
using var key1 = Fleet.Identity.NodeIdentity.FromPkcs8(Convert.FromBase64String(identity["private_key"]!.GetValue<string>()));
|
||||
Assert.True(Uri.TryCreate(identity["server"]!.GetValue<string>(), UriKind.Absolute, out _)); // Probe 直接连面板:probe.json 里是面板的地址
|
||||
await agent.WaitForTaskSetAsync(s => s.Peers.Any(p => p.ProbeId.Value == id && p.PublicKey == Convert.ToBase64String(key1.PublicKeySpki)));
|
||||
// 创建 Probe 就是创建一个 probe 角色的加入令牌;密钥由节点自己生成,Backend 从不接触私钥
|
||||
string name = _backend.UniqueName("home");
|
||||
using EnrolledProbe probe = await _backend.CreateProbeAsync(name);
|
||||
string id = probe.ProbeId;
|
||||
Assert.StartsWith(ProbeId.Prefix, id, StringComparison.Ordinal);
|
||||
string peerKey = Convert.ToBase64String(probe.Identity.PeerIdentity.PublicKeySpki);
|
||||
string backendKey = Convert.ToBase64String(probe.Identity.Identity.PublicKeySpki);
|
||||
Assert.NotEqual(backendKey, peerKey); // 连面板的和连 Agent 的是两把独立的密钥
|
||||
|
||||
// The panel lists it; two probes can coexist and a duplicate name is refused
|
||||
// Agent 的名单里放的是对等面的公钥,不是连面板的那把
|
||||
TaskSet withProbe = await agent.WaitForTaskSetAsync(s => s.Peers.Any(p => p.ProbeId.Value == id && p.PublicKey == peerKey));
|
||||
Assert.DoesNotContain(withProbe.Peers, p => p.PublicKey == backendKey);
|
||||
|
||||
// 令牌带角色;面板能列出这个 Probe;重名的注册被拒绝
|
||||
JsonNode tokens = await _backend.GetJsonAsync(_backend.Admin, "/api/admin/join-tokens");
|
||||
Assert.Contains(tokens["items"]!.AsArray(), t => t!["role"]!.GetValue<string>() == "probe");
|
||||
JsonNode listed = await _backend.GetJsonAsync(_backend.Admin, "/api/admin/probes");
|
||||
Assert.Contains(listed["items"]!.AsArray(), p => p!["id"]!.GetValue<string>() == id && !p["revoked"]!.GetValue<bool>());
|
||||
string name = (await _backend.GetJsonAsync(_backend.Admin, "/api/admin/probes/" + id))["name"]!.GetValue<string>();
|
||||
using HttpResponseMessage duplicate = await _backend.Admin.PostAsync("/api/admin/probes", BackendFixture.Json(new JsonObject { ["name"] = name }));
|
||||
Assert.Equal(HttpStatusCode.Conflict, duplicate.StatusCode);
|
||||
string duplicateToken = await _backend.CreateJoinTokenAsync(role: "probe");
|
||||
using var http = new HttpClient();
|
||||
Fleet.Core.Result<Fleet.Enrollment.Client.EnrollmentResult> duplicate = await Fleet.Enrollment.Client.EnrollmentClient.EnrollAsync(
|
||||
http, _backend.BaseAddress, duplicateToken, name, Path.Combine(Path.GetTempPath(), "moshiro-dup-" + Guid.NewGuid().ToString("N")), "0.0.0", null, TimeProvider.System, default);
|
||||
Assert.False(duplicate.IsSuccess);
|
||||
Assert.Equal("fleet_name_conflict", duplicate.Error.Code.Value);
|
||||
|
||||
// Rename
|
||||
// 创建身份和轮换密钥的接口已经没有了:换密钥就是吊销之后重新注册
|
||||
using HttpResponseMessage oldCreate = await _backend.Admin.PostAsync("/api/admin/probes", BackendFixture.Json(new JsonObject { ["name"] = "x" }));
|
||||
Assert.True(oldCreate.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.MethodNotAllowed);
|
||||
using HttpResponseMessage oldRotate = await _backend.Admin.PostAsync($"/api/admin/probes/{id}/rotate", null);
|
||||
Assert.True(oldRotate.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.MethodNotAllowed);
|
||||
|
||||
// 改名
|
||||
using HttpResponseMessage renamed = await _backend.Admin.PatchAsync("/api/admin/probes/" + id, BackendFixture.Json(new JsonObject { ["name"] = name + "-b" }));
|
||||
Assert.Equal(name + "-b", JsonNode.Parse(await renamed.Content.ReadAsStringAsync())!["name"]!.GetValue<string>());
|
||||
|
||||
// Rotate: same id, new key; the old key leaves every agent's list
|
||||
using HttpResponseMessage rotated = await _backend.Admin.PostAsync($"/api/admin/probes/{id}/rotate", null);
|
||||
JsonNode regenerated = JsonNode.Parse(await rotated.Content.ReadAsStringAsync())!;
|
||||
Assert.Equal(id, regenerated["probe_id"]!.GetValue<string>());
|
||||
Assert.NotEqual(identity["private_key"]!.GetValue<string>(), regenerated["private_key"]!.GetValue<string>());
|
||||
using var key2 = Fleet.Identity.NodeIdentity.FromPkcs8(Convert.FromBase64String(regenerated["private_key"]!.GetValue<string>()));
|
||||
await agent.WaitForTaskSetAsync(s => s.Peers.Any(p => p.ProbeId.Value == id && p.PublicKey == Convert.ToBase64String(key2.PublicKeySpki)));
|
||||
|
||||
// Revoke: gone from the peer list, kept in the panel as revoked; it cannot be rotated any more
|
||||
// 吊销:从名单里消失,面板里留着记录
|
||||
using HttpResponseMessage revoked = await _backend.Admin.PostAsync($"/api/admin/probes/{id}/revoke", null);
|
||||
Assert.True(JsonNode.Parse(await revoked.Content.ReadAsStringAsync())!["revoked"]!.GetValue<bool>());
|
||||
await agent.WaitForTaskSetAsync(s => s.Peers.All(p => p.ProbeId.Value != id));
|
||||
using HttpResponseMessage rotateRevoked = await _backend.Admin.PostAsync($"/api/admin/probes/{id}/rotate", null);
|
||||
Assert.Equal(HttpStatusCode.Conflict, rotateRevoked.StatusCode);
|
||||
|
||||
// Delete
|
||||
// 删除
|
||||
using HttpResponseMessage deleted = await _backend.Admin.DeleteAsync("/api/admin/probes/" + id);
|
||||
Assert.Equal(HttpStatusCode.NoContent, deleted.StatusCode);
|
||||
using HttpResponseMessage gone = await _backend.Admin.GetAsync("/api/admin/probes/" + id);
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed class LinkTests
|
||||
public async Task Links_FollowTheAutomaticSwitch_AndManualChoicesAlwaysWin()
|
||||
{
|
||||
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
|
||||
string probe = (await _backend.CreateProbeAsync())["probe_id"]!.GetValue<string>();
|
||||
string probe = await _backend.CreateProbeIdAsync();
|
||||
string agentId = agent.NodeId.Value;
|
||||
try
|
||||
{
|
||||
@@ -72,7 +72,7 @@ public sealed class LinkTests
|
||||
public async Task Links_RefuseUnknownNodes()
|
||||
{
|
||||
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
|
||||
string probe = (await _backend.CreateProbeAsync())["probe_id"]!.GetValue<string>();
|
||||
string probe = await _backend.CreateProbeIdAsync();
|
||||
using HttpResponseMessage noProbe = await SetLinkAsync("prb_missing", agent.NodeId.Value, false);
|
||||
Assert.Equal(HttpStatusCode.NotFound, noProbe.StatusCode);
|
||||
using HttpResponseMessage noAgent = await SetLinkAsync(probe, "agt_missing", false);
|
||||
|
||||
@@ -5,16 +5,18 @@ using MoShiro.Probe;
|
||||
|
||||
namespace MoShiro.Backend.Tests;
|
||||
|
||||
/// <summary>进程内的真实 Probe:用后端生成的身份文件(和面板下载的 probe.json 同一份)启动 <see cref="ProbeApp"/>。</summary>
|
||||
/// <summary>进程内的真实 Probe:用加入令牌注册出来的身份(节点自己生成的两把密钥)启动 <see cref="ProbeApp"/>。</summary>
|
||||
public sealed class ProbeHarness : IAsyncDisposable
|
||||
{
|
||||
private readonly CancellationTokenSource _stop = new();
|
||||
private readonly string _directory;
|
||||
private readonly Task _run;
|
||||
private readonly EnrolledProbe _enrolled;
|
||||
|
||||
private ProbeHarness(string directory, ProbeApp app)
|
||||
private ProbeHarness(string directory, ProbeApp app, EnrolledProbe enrolled)
|
||||
{
|
||||
_directory = directory;
|
||||
_enrolled = enrolled;
|
||||
App = app;
|
||||
_run = Task.Run(() => app.RunAsync(_stop.Token), CancellationToken.None);
|
||||
}
|
||||
@@ -25,21 +27,19 @@ public sealed class ProbeHarness : IAsyncDisposable
|
||||
|
||||
public string ProbeId { get; init; } = "";
|
||||
|
||||
/// <summary>向后端申请身份(<c>POST /api/admin/probe/identity</c>),写成 <c>probe.json</c> 并启动。</summary>
|
||||
/// <summary>用加入令牌注册(节点自己生成密钥),把面板地址和身份目录写进 <c>probe.json</c>,然后启动。</summary>
|
||||
public static async Task<ProbeHarness> StartAsync(BackendFixture backend, Action<ProbeFile>? configure = null)
|
||||
{
|
||||
JsonNode created = await backend.CreateProbeAsync();
|
||||
string json = created.ToJsonString();
|
||||
|
||||
string directory = Path.Combine(Path.GetTempPath(), "moshiro-probe-" + Guid.NewGuid().ToString("N"));
|
||||
System.IO.Directory.CreateDirectory(directory);
|
||||
EnrolledProbe enrolled = await backend.CreateProbeAsync();
|
||||
string directory = enrolled.Directory;
|
||||
string path = Path.Combine(directory, "probe.json");
|
||||
await File.WriteAllTextAsync(path, json, Encoding.UTF8);
|
||||
var written = new ProbeFile { Fleet = { Server = backend.BaseAddress.ToString(), IdentityDir = directory } };
|
||||
await File.WriteAllTextAsync(path, System.Text.Json.JsonSerializer.Serialize(written, ProbeConfigContext.Default.ProbeFile), Encoding.UTF8);
|
||||
|
||||
ProbeFile file = Fleet.Hosting.NodeConfig.Load(path, ProbeConfigContext.Default.ProbeFile);
|
||||
configure?.Invoke(file);
|
||||
var app = new ProbeApp(file, directory, TimeProvider.System, new Fleet.Hosting.ConsoleNodeLog(TimeProvider.System, Console.Out));
|
||||
return new ProbeHarness(directory, app) { ProbeId = created["probe_id"]!.GetValue<string>() };
|
||||
var app = new ProbeApp(file, enrolled.Identity, directory, TimeProvider.System, new Fleet.Hosting.ConsoleNodeLog(TimeProvider.System, Console.Out));
|
||||
return new ProbeHarness(directory, app, enrolled) { ProbeId = enrolled.ProbeId };
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
@@ -55,6 +55,7 @@ public sealed class ProbeHarness : IAsyncDisposable
|
||||
}
|
||||
|
||||
App.Dispose();
|
||||
_enrolled.Dispose();
|
||||
_stop.Dispose();
|
||||
try
|
||||
{
|
||||
|
||||
@@ -302,7 +302,7 @@ public sealed class StorageTests : IDisposable
|
||||
var tokens = new SqliteJoinTokenStore(_db);
|
||||
var fleet = new SqliteAgentStore(_db);
|
||||
byte[] hash = TokenHash.Sha256("jt_race");
|
||||
JoinTokenInfo created = await tokens.CreateAsync("jtk_1", hash, "note", ["a"], true, maxUses: 3, expiresAtMs: 10_000, nowMs: 1_000, default);
|
||||
JoinTokenInfo created = await tokens.CreateAsync("jtk_1", hash, "note", ["a"], true, "", maxUses: 3, expiresAtMs: 10_000, nowMs: 1_000, default);
|
||||
Assert.Equal(0, created.Uses);
|
||||
|
||||
bool[] results = await Task.WhenAll(Enumerable.Range(0, 20).Select(_ => Task.Run(async () => await fleet.TryConsumeJoinTokenAsync("jtk_1", 2_000, default))));
|
||||
@@ -310,7 +310,7 @@ public sealed class StorageTests : IDisposable
|
||||
Assert.Equal(3, (await fleet.FindJoinTokenAsync(hash, default))!.Uses);
|
||||
|
||||
Assert.False(await fleet.TryConsumeJoinTokenAsync("jtk_1", 2_000, default));
|
||||
JoinTokenInfo expiring = await tokens.CreateAsync("jtk_2", TokenHash.Sha256("jt_exp"), null, [], true, 5, expiresAtMs: 5_000, nowMs: 1_000, default);
|
||||
JoinTokenInfo expiring = await tokens.CreateAsync("jtk_2", TokenHash.Sha256("jt_exp"), null, [], true, "", 5, expiresAtMs: 5_000, nowMs: 1_000, default);
|
||||
Assert.False(await fleet.TryConsumeJoinTokenAsync(expiring.Id, 5_000, default), "expired at the boundary");
|
||||
Assert.True(await tokens.RevokeAsync("jtk_2", 3_000, default));
|
||||
Assert.False(await tokens.RevokeAsync("jtk_2", 3_000, default));
|
||||
|
||||
@@ -80,12 +80,14 @@ public sealed class AgentSimulator : IAsyncDisposable
|
||||
string pubkey = Convert.ToBase64String(identity.PublicKeySpki);
|
||||
byte[] tokenHash = SHA256.HashData(Encoding.UTF8.GetBytes(joinToken));
|
||||
string proof = Convert.ToBase64String(identity.Sign(SignaturePurpose.Enroll, Convert.ToHexStringLower(tokenHash), pubkey));
|
||||
using NodeIdentity tls = NodeIdentity.Generate();
|
||||
var body = new JsonObject
|
||||
{
|
||||
["join_token"] = joinToken,
|
||||
["name"] = name,
|
||||
["pubkey"] = pubkey,
|
||||
["tls_spki"] = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)),
|
||||
["tls_spki"] = Convert.ToBase64String(NodeIdentity.Fingerprint(tls.PublicKeySpki)),
|
||||
["tls_pubkey"] = Convert.ToBase64String(tls.PublicKeySpki),
|
||||
["proof"] = proof,
|
||||
["node_version"] = "0.0.1-sim",
|
||||
["meta"] = new JsonObject
|
||||
|
||||
Reference in new issue
Block a user