Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
1cf8643
docs(dotnet-agent-identity): add the authority library intent
CBenoit Oct 20, 2026
49ac927
build(dotnet-agent-identity): scaffold the authority library
CBenoit Oct 20, 2026
11119ff
feat(dotnet-agent-identity): add strict base64 codecs
CBenoit Oct 20, 2026
fd67d75
feat(dotnet-agent-identity): add the protocol error model
CBenoit Oct 20, 2026
82c7bff
ci(dotnet-agent-identity): test and publish the authority library
CBenoit Oct 20, 2026
f97f80c
build(dotnet-agent-identity): keep NuGet advisories as warnings
CBenoit Oct 20, 2026
385b89e
feat(dotnet-agent-identity): add the certificate registry and nonce s…
CBenoit Oct 20, 2026
82219c1
feat(dotnet-agent-identity): authenticate signed agent requests
CBenoit Oct 20, 2026
26b5459
feat(dotnet-agent-identity): validate agent metadata
CBenoit Oct 20, 2026
b146c24
feat(dotnet-agent-identity): validate certificate signing requests
CBenoit Oct 20, 2026
be73911
feat(dotnet-agent-identity): parse enroll, renew and check-in bodies
CBenoit Oct 20, 2026
c46d178
feat(dotnet-agent-identity): issue identity certificates
CBenoit Oct 20, 2026
6f4376e
feat(dotnet-agent-identity): create enrollment tokens
CBenoit Oct 20, 2026
f244737
feat(dotnet-agent-identity): read the enroll bearer token
CBenoit Oct 20, 2026
3789a7a
feat(dotnet-agent-identity): decide enrollments by token and key
CBenoit Oct 20, 2026
eaeabf6
feat(dotnet-agent-identity): evaluate friendly-name formats
CBenoit Oct 20, 2026
474a957
fix(dotnet-agent-identity): start certificate statuses at 1
CBenoit Oct 20, 2026
5273548
fix(dotnet-agent-identity): keep format text out of logs
CBenoit Oct 20, 2026
8638587
test(dotnet-agent-identity): refuse the default certificate status
CBenoit Oct 20, 2026
1bbe991
fix(dotnet-agent-identity): name identities from validated metadata
CBenoit Oct 20, 2026
fa13c07
docs(dotnet-agent-identity): qualify the format text guarantee
CBenoit Oct 20, 2026
c78c61c
test(dotnet-agent-identity): accept an empty extensionRequest
CBenoit Oct 20, 2026
6b36803
feat(dotnet-agent-identity): verify the agent channel handshake
CBenoit Oct 20, 2026
6409683
feat(dotnet-agent-identity): enforce handshake deadlines
CBenoit Oct 20, 2026
91e234e
feat(dotnet-agent-identity): re-check the registry in the handshake
CBenoit Oct 20, 2026
d909f6d
test(dotnet-agent-identity): read the new vector file members
CBenoit Oct 20, 2026
1966cd6
refactor(dotnet-agent-identity): share the base URL rules
CBenoit Oct 20, 2026
52c6ca3
feat(dotnet-agent-identity): add the identity config
CBenoit Oct 20, 2026
8f8e0b7
docs(dotnet-agent-identity): clarify two error outcomes
CBenoit Oct 20, 2026
88ee3e6
feat(dotnet-agent-identity): write the response bodies
CBenoit Oct 20, 2026
76f8a7d
feat(dotnet-agent-identity): build the channel push messages
CBenoit Oct 20, 2026
9ac06ef
feat(dotnet-agent-identity): issue renewals for the proved identity
CBenoit Oct 20, 2026
e1e3354
feat(dotnet-agent-identity): decide renewals by the CSR key
CBenoit Oct 20, 2026
54743ee
refactor(dotnet-agent-identity): simplify the body writer checks
CBenoit Oct 20, 2026
2ab5936
test(dotnet-agent-identity): pin the copies of configs and chains
CBenoit Oct 20, 2026
9895c30
test(dotnet-agent-identity): verify test handshakes asynchronously
CBenoit Oct 20, 2026
518d37e
fix(dotnet-agent-identity): apply the JSON rules inside config values
CBenoit Oct 20, 2026
c01363b
docs(dotnet-agent-identity): add the package README
CBenoit Oct 20, 2026
b7612f8
build(dotnet-agent-identity): pack the README
CBenoit Oct 20, 2026
362fb7d
docs(dotnet-agent-identity): cover responses, renewals and pushes
CBenoit Oct 20, 2026
3ed4614
docs(dotnet-agent-identity): reconcile every authenticated stream
CBenoit Oct 20, 2026
9550ada
docs(dotnet-agent-identity): link the contract by absolute URL
CBenoit Oct 20, 2026
aff64b8
feat(dotnet-agent-identity): apply stream closures to the Hello timeout
CBenoit Oct 20, 2026
0627e52
docs(dotnet-agent-identity): document the async Hello timeout
CBenoit Oct 20, 2026
503d4a0
refactor(dotnet-agent-identity): group the sources by contract area
CBenoit Oct 20, 2026
942049c
test(dotnet-agent-identity): consolidate tests that check one rule
CBenoit Oct 20, 2026
45eb76d
test(dotnet-agent-identity): strengthen tests and cover untested rules
CBenoit Oct 20, 2026
cfb17f2
test(dotnet-agent-identity): delete self-derived assertions
CBenoit Oct 20, 2026
747b239
test(dotnet-agent-identity): accept CSR attributes out of DER order
CBenoit Oct 20, 2026
95af4da
test(dotnet-agent-identity): delete unspecified single-read checks
CBenoit Oct 20, 2026
22820b1
test(dotnet-agent-identity): select the registry fixture by case data
CBenoit Oct 21, 2026
12aec06
docs(dotnet-agent-identity): cite the contract by its new name
CBenoit Oct 21, 2026
03686c3
docs(dotnet-agent-identity): give the intent a citable section
CBenoit Oct 21, 2026
0a8965c
docs(dotnet-agent-identity): state the failure-detail and token rules
CBenoit Oct 21, 2026
a3bec94
test(dotnet-agent-identity): mark the source of every test
CBenoit Oct 21, 2026
ca9fddf
test(dotnet-agent-identity): drop assertions that no source decides
CBenoit Oct 21, 2026
d51eba0
feat(dotnet-agent-identity): reject a non-empty confirm body
CBenoit Oct 21, 2026
2132373
fix(dotnet-agent-identity): keep metadata keys out of failure details
CBenoit Oct 21, 2026
4a42ad1
test(dotnet-agent-identity): narrow assertions to what their sources …
CBenoit Oct 21, 2026
f9c5b7b
feat(dotnet-agent-identity): end the handshake on an undecodable firs…
CBenoit Oct 21, 2026
23057af
feat(dotnet-agent-identity): keep every config within one channel env…
CBenoit Oct 21, 2026
24021f7
test(dotnet-agent-identity): mark the theories whose rows carry the m…
CBenoit Oct 22, 2026
f80dbb0
test(dotnet-agent-identity): pin the channel URL slash rule and revoc…
CBenoit Oct 22, 2026
d54ddea
test(dotnet-agent-identity): cite contract sections by their anchors
CBenoit Oct 22, 2026
d766d85
docs(dotnet-agent-identity): link contract sections by their anchors
CBenoit Oct 22, 2026
4392418
fix(dotnet-agent-identity): accept any encoding of the CSR subject
CBenoit Oct 22, 2026
8ae6f69
test(dotnet-agent-identity): build the specified-curve key by hand
CBenoit Oct 22, 2026
cf46488
feat(dotnet-agent-identity)!: issue a new identity's certificate only…
CBenoit Oct 22, 2026
5657a80
docs(dotnet-agent-identity): guide hosts on handshake races, last_see…
CBenoit Oct 22, 2026
ce4d20a
test(dotnet-agent-identity): pin the handshake's registry read inside…
CBenoit Oct 22, 2026
537b31b
refactor(dotnet-agent-identity): share one base among the result types
CBenoit Oct 22, 2026
789d086
test(dotnet-agent-identity): reuse the shared clock and generator dou…
CBenoit Oct 22, 2026
5b8bbd5
test(dotnet-agent-identity): merge the CSR rejection tests into one t…
CBenoit Oct 22, 2026
22b8b57
test(dotnet-agent-identity): share the table of unacceptable base URLs
CBenoit Oct 22, 2026
9423204
test(dotnet-agent-identity): merge the handshake failure tests into t…
CBenoit Oct 22, 2026
28fe38f
test(dotnet-agent-identity): share the channel opening setup
CBenoit Oct 22, 2026
fa63390
test(dotnet-agent-identity): remove unused vector theory data
CBenoit Oct 22, 2026
221041f
test(dotnet-agent-identity): tighten the shared test setup
CBenoit Oct 23, 2026
5d70eb1
fix(dotnet-agent-identity)!: hash the bytes an enrollment secret encodes
CBenoit Oct 23, 2026
4c57ab4
test(dotnet-agent-identity): pin the refusal of a token URL password
CBenoit Oct 23, 2026
21df0f0
fix(dotnet-agent-identity)!: call an enrollment token revoked, not de…
CBenoit Oct 24, 2026
8fb95b7
fix(dotnet-agent-identity): refuse any @ in a token or channel URL
CBenoit Oct 24, 2026
a400c56
docs(dotnet-agent-identity): mark the untested UTC reading of validity
CBenoit Oct 24, 2026
3148d35
fix(dotnet-agent-identity): refuse a token URL that is not ASCII
CBenoit Oct 24, 2026
f63a6ff
fix(dotnet-agent-identity)!: issue only under a root of the chain pro…
CBenoit Oct 24, 2026
b0434a2
feat(dotnet-agent-identity): add the token_url_mismatch error code
CBenoit Oct 24, 2026
6877876
feat(dotnet-agent-identity): read the enrollment URL a CSR requests
CBenoit Oct 24, 2026
b8d76bf
feat(dotnet-agent-identity)!: refuse an enrollment at another URL
CBenoit Oct 24, 2026
6f35c21
feat(dotnet-agent-identity)!: name the enrollment URL in every certif…
CBenoit Oct 24, 2026
71072e8
fix(dotnet-agent-identity): tighten the enrollment URL and issuer checks
CBenoit Oct 24, 2026
b293e68
docs(dotnet-agent-identity): say where the enrollment URL OID sits
CBenoit Oct 25, 2026
796ccbd
test(dotnet-agent-identity): pin each CSR vector's outcome per endpoint
CBenoit Oct 25, 2026
83e18b7
docs(dotnet-agent-identity): say that a token's URL may differ from t…
CBenoit Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,14 @@ jobs:
run: ./crates/agent-channel-proto/dotnet/build.ps1
shell: pwsh

- name: Agent identity tests
run: dotnet test dotnet/AgentIdentity/AgentIdentity.sln
shell: pwsh

- name: Build agent identity package
run: ./dotnet/AgentIdentity/build.ps1
shell: pwsh

agent-identity-vectors:
name: Agent Identity test vectors
runs-on: ubuntu-latest
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/publish-libraries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
strategy:
fail-fast: false
matrix:
library: [ dotnet-client, dotnet-subscriber, utils, pedm-dotnet-client, agent-channel ]
library: [ dotnet-client, dotnet-subscriber, utils, pedm-dotnet-client, agent-channel, agent-identity ]
include:
- library: dotnet-client
libpath: ./devolutions-gateway/openapi/dotnet-client
Expand All @@ -52,6 +52,8 @@ jobs:
libpath: ./crates/devolutions-pedm/openapi/dotnet-client
- library: agent-channel
libpath: ./crates/agent-channel-proto/dotnet
- library: agent-identity
libpath: ./dotnet/AgentIdentity

steps:
- name: Check out ${{ github.repository }}
Expand Down
363 changes: 363 additions & 0 deletions dotnet/AgentIdentity/.editorconfig

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions dotnet/AgentIdentity/AgentIdentity.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Devolutions.AgentIdentity", "Devolutions.AgentIdentity\Devolutions.AgentIdentity.csproj", "{A1DC1136-5E93-4B2C-8147-1008ECD4204B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Devolutions.AgentIdentity.Tests", "Devolutions.AgentIdentity.Tests\Devolutions.AgentIdentity.Tests.csproj", "{60CED46A-EE0C-4C84-82BA-60DE4D518EC4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1DC1136-5E93-4B2C-8147-1008ECD4204B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1DC1136-5E93-4B2C-8147-1008ECD4204B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1DC1136-5E93-4B2C-8147-1008ECD4204B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1DC1136-5E93-4B2C-8147-1008ECD4204B}.Release|Any CPU.Build.0 = Release|Any CPU
{60CED46A-EE0C-4C84-82BA-60DE4D518EC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{60CED46A-EE0C-4C84-82BA-60DE4D518EC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60CED46A-EE0C-4C84-82BA-60DE4D518EC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{60CED46A-EE0C-4C84-82BA-60DE4D518EC4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Reflection;

namespace Devolutions.AgentIdentity.Tests;

/// <summary>
/// The types that only request authentication, the channel handshake and the enrollment rules create.
/// </summary>
/// <remarks>
/// INTENT.md: code that acts for an identity takes it as a type only the signature check can create,
/// so that an identity is never taken from a value the agent or a caller sent.
/// </remarks>
public class AuthenticationTypeTests
{
private static readonly Type[] AuthenticationTypes =
[
// oracle: intent dotnet/AgentIdentity/INTENT.md §Authenticated identity
typeof(AuthenticatedIdentity),
// The handshake turns an opening into an identity,
// so an opening built by a caller would yield an identity that no signature check produced.
// oracle: intent dotnet/AgentIdentity/INTENT.md §Authenticated identity
typeof(ChannelOpening),
// A new identity's certificate is issued from the decision that creates it,
// so a decision built by a caller would certify a key for an identity that no enrollment created.
// oracle: intent dotnet/AgentIdentity/INTENT.md §Authenticated identity
typeof(EnrollmentDecision),
// oracle: mutant a public AuthenticationResult constructor
typeof(AuthenticationResult),
// oracle: mutant a public ChannelOpeningResult constructor
typeof(ChannelOpeningResult),
// oracle: mutant a public ChannelHandshakeResult constructor
typeof(ChannelHandshakeResult),
];

public static TheoryData<Type> AuthenticationTypeData => new(AuthenticationTypes);

// Code that acts for an identity takes it as a type only the signature check can create.
// A public or protected constructor would let a caller build an identity from any values,
// or a subclass of one with its own constructor.
// The other types carry an identity, the opening that becomes one, or the decision that creates one,
// out of a check.
// Each row's marker names what decides it.
// oracle: intent dotnet/AgentIdentity/INTENT.md §Authenticated identity
[Theory]
[MemberData(nameof(AuthenticationTypeData))]
public void TypeHasNoPublicOrProtectedConstructor(Type type)
{
IEnumerable<ConstructorInfo> reachable = type
.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)
.Where(IsReachableFromAnotherAssembly);

Assert.Empty(reachable);
}

// A new public member producing one of these types could be a factory that bypasses the check that creates it.
// The intent forbids creating an identity outside that check but not forwarding one that it created,
// so the list of members allowed to produce one is the library's own.
// oracle: mutant a public member that builds an AuthenticatedIdentity outside the signature check,
// or an EnrollmentDecision outside the enrollment rules
[Fact]
public void NoOtherPublicMemberProducesTheseTypes()
{
const BindingFlags declared = BindingFlags.Public | BindingFlags.NonPublic
| BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;

IEnumerable<string> producers = typeof(RequestAuthenticator).Assembly
.GetExportedTypes()
.SelectMany(type => type.GetMethods(declared)
.Where(IsReachableFromAnotherAssembly)
.Where(method => Produces(method.ReturnType))
.Select(method => $"{type.Name}.{method.Name}")
.Concat(type.GetFields(declared)
.Where(field => field.IsPublic || field.IsFamily || field.IsFamilyOrAssembly)
.Where(field => Produces(field.FieldType))
.Select(field => $"{type.Name}.{field.Name}")));

Assert.Equal(
[
"AuthenticationResult.get_Identity",
"ChannelHandshakeResult.get_Identity",
"ChannelOpening.FirstMessageUnreadableAsync",
"ChannelOpening.HelloTimedOutAsync",
"ChannelOpening.VerifyHelloAsync",
"ChannelOpeningResult.get_Opening",
"EnrollmentRules.Decide",
"RequestAuthenticator.AuthenticateAsync",
"RequestAuthenticator.AuthenticateChannelOpeningAsync",
],
producers.Order(StringComparer.Ordinal));
}

private static bool IsReachableFromAnotherAssembly(MethodBase method)
{
return method.IsPublic || method.IsFamily || method.IsFamilyOrAssembly;
}

private static bool Produces(Type type)
{
Type? definition = type.IsGenericType ? type.GetGenericTypeDefinition() : null;
Type produced = definition == typeof(ValueTask<>) || definition == typeof(Task<>)
? type.GetGenericArguments()[0]
: type;

return AuthenticationTypes.Contains(produced);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Devolutions.AgentIdentity.Tests;

public class RegisteredCertificateTests
{
// An undefined status from the host's storage must fail as a host bug,
// rather than draw identity_unknown, which ends the identity for the agent.
// The default is the value of an uninitialized status, and 4 follows the last of the three statuses.
// CONTRACT.agent-identity.md Definitions: a certificate is pending, current or retired;
// §5.4: identity_unknown is terminal.
// Framework Design Guidelines, Parameter Design: validate enum parameters, since an enum may hold any integer.
// oracle: spec Framework Design Guidelines §Validating Arguments
// oracle: spec Framework Design Guidelines §Using Standard Exception Types
[Theory]
[InlineData(default(CertificateStatus))]
[InlineData((CertificateStatus)4)]
public void RefusesAnUndefinedStatus(CertificateStatus undefined)
{
KeyVector key = TestVectors.Instance.Key("device-a");

Assert.Throws<ArgumentOutOfRangeException>(
() => new RegisteredCertificate(key.IdentityId, key.Certificate, undefined, identityRevoked: false));
}
}
Loading
Loading