Repository navigation
feat(dotnet-agent-identity): add the authority library - #2125
Benoît Cortier (CBenoit) wants to merge 92 commits into
Conversation
Acceptance evidence at
|
Test-pruning pass over
|
| Before | After | |
|---|---|---|
| Test methods | 346 | 285 |
| Test cases | 1480 | 1569 |
| Test lines | 7459 | 7783 |
| Stryker score (killed + timeout over tested) | 75.25% (900/1196) | 88.04% (1053/1196) |
| Stryker survived / no coverage | 234 / 62 | 129 / 14 |
| Domain mutants caught | 69/71 | 71/71 |
| Flaky tests (16 clean runs) | 0 | 0 |
The Stryker and domain rows were measured at 6cb8e577.
The last three commits add one acceptance case, delete two tests that caught nothing alone, and restructure one theory.
Scoped Stryker runs before and after them show no loss.
- Caught set: 154 mutants gained, 0 lost.
One mutant showed as lost:IdentityConfig.IsSnakeCasereadsname[i + 1]instead ofname[i - 1].
It is equivalent, because both versions reject exactly the names with two adjacent underscores.
Applied by hand, it passes on both heads; its earlier timeout came from machine load. - Settings: both runs used the same Stryker settings: Standard level,
--disable-bail, per-test coverage, the whole library.
Why the suite did not shrink more
Before the pass, the test methods divided into:
- 324 independent: the contract, the shared vectors or an RFC justifies their expected values, and the skill never deletes those on mutant evidence;
- 3 self-derived, all protected because they handle untrusted input, so they were strengthened rather than deleted;
- 22 of unknown oracle: each was the only test catching some mutant, or survived a challenge.
So the size came down through consolidation: 346 → 285 methods, with every input and expected value kept.
Deletion removed only self-derived assertions and two tests of an unspecified single enumeration.
The pass's main yield was coverage: about 90 new cases catch 154 mutants that no test caught before.
Gaps closed
- Malformed
Signature-Inputsyntax: a non-list member, a trailing comma, a truncated parameter, an unclosed string, a bad escape, and a number or byte sequence that ends early.
Seven of these mutants turned a malformed header into an unhandled exception or an endless loop; the tests now pinsignature_invalid. - A covered component carrying a parameter, signed over the list with the parameter stripped, fails step 1.
- A signature lifetime of exactly 300 s is accepted (§6 step 3).
- An invalid method can't verify a signature over empty data.
- Every RFC 9110 token character is accepted in a method.
- 15-digit integers and negative integers in
created. - An empty signature still fails the window check first (§6 steps 3 and 5).
- The RFC 8941 parser and serializer on their own: strings, tokens, decimals, byte sequences, Booleans, and dictionary punctuation.
- A CSR point with a coordinate outside the field is
invalid_request, never a key-import exception. - CSR attributes out of DER order are accepted (§4).
- snake_case boundaries in product fields, metadata membership and non-generic enumeration, and a config nested 64 levels deep in a response body.
- Every error carries a non-empty message (§5.4).
- The enrollment token's hash test now computes the SHA-256 of the secret text itself.
Oracle review after the pass
Every changed test cites its source.
The assertions that no independent source can fix, but that alone catch a fault, are marked self-derived:
- the exact serial bytes;
- the failure details of two
signature_invalidhandshake failures; - the construction restrictions on result types.
Surviving mutants after the pass
The remaining survivors are classed as follows:
- unconstrained: wording of failure details and exception messages, which no requirement fixes;
- equivalent;
- bug leads: unreachable defensive exits in the parser and the authenticator, plus two Stryker anomalies, one in a static initializer and one replacement that does not compile alone.
Note
Human-tuned, LLM-assisted content.
There was a problem hiding this comment.
🔵 Needs a closer look
The new authentication, certificate-issuance, and channel-handshake boundaries require final human security review.
1 open issue
What changed in this PR
Adds Devolutions.AgentIdentity to share agent-facing authority behavior between DVLS and PSU while leaving storage, hosting, and root-key ownership with each host.
Changes:
- Adds authentication types, enrollment and renewal rules, certificate issuance, and protocol helpers.
- Adds shared-vector tests and independent test fixtures.
- Integrates the library into .NET builds, CI, and package publishing.
| File | Description |
|---|---|
| dotnet/AgentIdentity/INTENT.md | Defines the library’s scope and identity boundary. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Protocol/StrictBase64.cs | Enforces canonical base64 encodings. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Protocol/BaseUrl.cs | Validates and normalizes HTTPS base URLs. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Protocol/AgentIdentityProtocol.cs | Defines shared limits, timestamps, and thumbprints. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Errors/AgentIdentityErrorCodeExtensions.cs | Maps errors to wire values and statuses. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Errors/AgentIdentityErrorCode.cs | Defines protocol error codes. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Errors/AgentIdentityError.cs | Builds error bodies and gRPC exceptions. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Enrollment/IssuedKeyMatch.cs | Represents previously issued key matches. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Enrollment/FriendlyNameFormat.cs | Parses and evaluates friendly-name templates. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Enrollment/EnrollmentTokenState.cs | Represents stored token state. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Enrollment/EnrollmentToken.cs | Creates tokens and hashes their secrets. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Enrollment/EnrollmentRules.cs | Applies enrollment and replay rules. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Enrollment/EnrollmentOutcome.cs | Defines enrollment outcomes. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Enrollment/EnrollmentDecision.cs | Carries enrollment decisions. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Enrollment/EnrollmentAuthorizationResult.cs | Carries token parsing results. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Enrollment/EnrollmentAuthorization.cs | Reads enrollment bearer credentials. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Devolutions.AgentIdentity.csproj | Configures the package and channel dependency. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Channel/RenewalRequestReason.cs | Defines renewal push reasons. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Channel/ReconnectReason.cs | Defines reconnect reasons. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Channel/ChannelProof.cs | Builds and verifies channel proofs. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Channel/ChannelOpeningResult.cs | Represents opening authentication results. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Channel/ChannelMessages.cs | Builds server push messages. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Channel/ChannelHandshakeResult.cs | Carries handshake results and welcome messages. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Channel/ChannelHandshake.cs | Defines the Hello timeout. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Certificates/RenewalRules.cs | Applies renewal key-reuse rules. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Certificates/RenewalOutcome.cs | Defines renewal outcomes. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Certificates/RenewalDecision.cs | Carries renewal decisions. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Certificates/IRootSigner.cs | Defines the host’s root-signing interface. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Certificates/IdentityCertificateIssuer.cs | Issues identity certificates. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Certificates/CsrValidationResult.cs | Carries CSR validation results. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Certificates/CertificateStatus.cs | Defines certificate lifecycle states. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Bodies/ResponseBodies.cs | Serializes successful endpoint responses. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Bodies/RequestBodyReader.cs | Validates JSON, CSR, and metadata bodies. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Bodies/RequestBodyParseResult.cs | Carries body parsing results. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Bodies/RenewRequestBody.cs | Represents validated renewal bodies. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Bodies/MetadataValidationResult.cs | Carries metadata validation results. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Bodies/EnrollRequestBody.cs | Represents validated enrollment bodies. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Bodies/CheckInRequestBody.cs | Represents validated check-in bodies. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Bodies/AgentMetadata.cs | Enforces metadata syntax and limits. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Authentication/VerifiedCertificate.cs | Holds verified certificate facts. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Authentication/SignedOperation.cs | Defines operation-specific signature profiles. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Authentication/SignedHttpRequest.cs | Represents request authentication inputs. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Authentication/RegisteredCertificate.cs | Represents host registry entries. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Authentication/INonceStore.cs | Defines atomic replay protection storage. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Authentication/ICertificateRegistry.cs | Defines certificate lookup by thumbprint. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Authentication/FieldLines.cs | Copies and combines HTTP field lines. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Authentication/EcdsaP256Signature.cs | Verifies raw P-256 signatures. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Authentication/AuthenticationResult.cs | Carries request authentication results. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Authentication/AuthenticatedIdentity.cs | Encapsulates a proven identity. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity/Authentication/AgentOperation.cs | Defines authenticated operations. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Usings.cs | Imports xUnit globally. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Support/VectorData.cs | Exposes named vector test cases. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Support/VectorChannel.cs | Creates vector-backed channel fixtures. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Support/VectorAuthority.cs | Runs vectors against a test authority. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Support/TestVectors.cs | Loads and validates shared vector data. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Support/TestRequest.cs | Independently builds signed test requests. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Support/TestJson.cs | Provides independent JSON assertions. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Support/TestIdentity.cs | Obtains identities through authentication. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Support/TestDoubles.cs | Supplies controlled host dependencies. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Support/TestCsr.cs | Builds valid and malformed test CSRs. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Support/TestChannelProof.cs | Independently signs channel proofs. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Support/TestAuthority.cs | Provides signed-request test fixtures. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Protocol/AgentIdentityProtocolTests.cs | Tests timestamps, thumbprints, and limits. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Errors/AgentIdentityErrorTests.cs | Tests error serialization and statuses. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Errors/AgentIdentityErrorCodeTests.cs | Tests error-code mappings. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Enrollment/EnrollmentTokenAssert.cs | Detects token fragments in output. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Devolutions.AgentIdentity.Tests.csproj | Configures tests and shared vectors. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/ConstructionTests.cs | Checks validated-type construction boundaries. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Channel/ChannelOpeningTests.cs | Tests challenge freshness and retained state. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Channel/ChannelMessagesTests.cs | Tests push-message formats. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Channel/ChannelHandshakeVectorTests.cs | Exercises shared channel-proof vectors. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Certificates/RenewalRulesTests.cs | Tests renewal key decisions. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Authentication/StructuredFieldTests.cs | Tests structured-field parsing and serialization. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Authentication/RequestAuthenticatorVectorTests.cs | Tests signature vectors and nonce handling. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Authentication/RegisteredCertificateTests.cs | Tests rejection of undefined statuses. |
| dotnet/AgentIdentity/Devolutions.AgentIdentity.Tests/Authentication/AuthenticationTypeTests.cs | Checks authentication construction boundaries. |
| dotnet/AgentIdentity/build.ps1 | Builds and packs the library. |
| dotnet/AgentIdentity/AgentIdentity.sln | Groups library and test projects. |
| .github/workflows/publish-libraries.yml | Adds package publishing. |
| .github/workflows/ci.yml | Adds library tests and package builds. |
Review effort: Balanced
bdc6b96 to
7d13c2c
Compare
Oracle markersEvery test, theory row and assertion in
That covers 289 test methods and 1649 cases. Resolving the 59
|
| Pass | Caught before | Caught after |
|---|---|---|
| Deletions | 1035 | 1035 |
| Narrowing | 1036 | 1036 |
No caught mutant was lost.
Every domain mutant whose killers include an edited test is still caught.
Hand-applied mutants confirm each new mutant marker and each narrowed helper.
Reviews
Every review below used gpt-6.1-sol, a different model family from the authors.
test-oracle-reviewerround 1: nine blocking findings, all on assertions that went past their source, all fixed.- Round 2: eight of the nine were resolved. Two findings remained, one partly resolved and one new; both were fixed and checked by a fresh post-cap reviewer: approved, with no further findings.
- Code review of the
confirmand failure-detail changes: approved after one fix.
Note
Human-tuned, LLM-assisted content.
1078eb5 to
45f16fa
Compare
dd95884 to
0fe5d6f
Compare
EvidenceHead
Known gaps
Lines changedThe change adds 111 files, about 16,660 lines:
The library code and tests are about 37% and 59% of the added lines. Note LLM-assisted content (no human feedback). |
Luc Fauvel (LucFauvel)
left a comment
There was a problem hiding this comment.
This review used several reviewers, covering security, performance, test quality, slop, and conformance with the intent files and the contract. Below are the medium-severity findings as inline comments: two intent or contract questions, one API gap, three gaps in the host guidance, one performance item, a test that fails on macOS, and the two largest simplification opportunities.
No Critical or High issues were found. The tests run every shared vector, and 43 of 48 mutations of security-relevant lines were caught by the tests.
Note
Human-tuned, LLM-assisted content.
| /// Parses RFC 8941 structured field dictionaries, and serializes the values that a signature base holds. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Parsing follows RFC 8941 §4.2, except that a repeated dictionary key or parameter key fails it. |
There was a problem hiding this comment.
This conflicts with the shared intent's reason for choosing RFC 9421.
docs/agent-identity/INTENT.agent-identity.md (Request authentication) says: "Requests are signed with RFC 9421 HTTP Message Signatures, a standard that both the agent and a .NET authority implement with maintained libraries."
This library deliberately hand-writes the RFC 8941 parser and the signature base, and the PR description explains why: NSign keeps the last of repeated parameters, which contract §6 step 1 forbids, and its verification is built for ASP.NET Core. That reasoning holds up, but the intent statement is no longer true for the .NET side.
Since intent files are human-owned, someone needs to pick one:
- reword the intent (for example: RFC 9421 is a standard that tooling understands, and the .NET authority implements the profile itself because the contract's raw-field strictness rules out NSign); or
- revisit the approach.
Note
Human-tuned, LLM-assisted content.
There was a problem hiding this comment.
Settled in the intent rather than the code: #2000's 909e0a91 rewords the shared intent's §Request authentication so that an authority "may use a library or, as the .NET authority library does because NSign keeps the last of repeated parameters, which §6 step 1 forbids, implement the profile itself from the RFC."
Note
Human-tuned, LLM-assisted content.
| // oracle: spec X.690 §10.1 | ||
| [Theory] | ||
| [MemberData(nameof(RequestWithALongerLengthNames))] | ||
| public void RejectsLengthInMoreOctetsThanNeeded(string element) |
There was a problem hiding this comment.
These tests and contract §4 disagree on how strictly to parse the CSR subject.
CONTRACT.agent-identity.md §4 says the server "ignores the CSR's subject and attributes … and it does not require their contents in canonical DER form … since nothing it decides depends on them".
But the "subject" row here (non-minimal length octets, defined around line 81) and RejectsSubjectThatIsNotASequence (line 387) require invalid_request for a malformed subject. Their markers cite X.690 §10.1 and RFC 2986, and that contract sentence overrides or at least muddies those sources.
Another authority implementation that follows §4 literally would accept these CSRs, so the two implementations would behave differently. Either the contract should say that the subject's own tag and length must still be DER (only its contents are exempt), or these rows should be relaxed.
Note
Human-tuned, LLM-assisted content.
There was a problem hiding this comment.
Fixed in c1f0b35: the subject is read as any BER SEQUENCE and the attributes element as any BER [0], since CONTRACT.agent-identity.md §certificates has the server ignore both, while a subject that isn't structurally a Name stays invalid_request and the rest of the request stays DER.
Note
LLM-assisted content (no human feedback).
| /// <exception cref="ArgumentException"> | ||
| /// The issuer certificate has no subject key identifier, or its <c>notAfter</c> is not after the current second. | ||
| /// </exception> | ||
| public static X509Certificate2 Issue( |
There was a problem hiding this comment.
This public Guid overload lets a host issue a certificate for any identity, bypassing AuthenticatedIdentity.
dotnet/AgentIdentity/INTENT.md (Authenticated identity) says: "Code that acts for an identity takes it as a type only that check can create; an identity is never taken from a value the agent or a caller sent."
The AuthenticatedIdentity overload (line 150) enforces that for renewals, but it just forwards here, and this overload is public. A host renew handler that calls Issue(identityId, …) with an ID from a route or from storage would certify a key for an identity the request never proved. The README points to the right overload, but nothing in the API prevents the wrong one. It also accepts Guid.Empty.
Suggestion: make the new-identity path take a value that only the enrollment decision produces (for example something carried by EnrollmentDecision), or at least rename it IssueForNewIdentity and reject Guid.Empty.
Note
Human-tuned, LLM-assisted content.
There was a problem hiding this comment.
Fixed in d214561: the Guid overload is now private, and a new identity's certificate is issued only from the CreateIdentity decision of EnrollmentRules.Decide, which carries the NewIdentityId the library mints, so no public issuance path takes an identity ID.
Note
LLM-assisted content (no human feedback).
| 4. On success, replace the identity's metadata with `Metadata`, send `Welcome`, and only then treat the stream as authenticated for `Identity`. | ||
| 5. Send `ChannelMessages.RenewRequested` if the identity's request-renewal flag is set, and `ChannelMessages.ConfigUpdate` if the `Revision` of its config is higher than `applied_state_versions["config"]` of the `Hello` (CONTRACT.agent-identity.md [§7.3] steps 5 and 7). | ||
| 6. If `Identity.CertificateStatus` is `CertificateStatus.Retired`, the certificate was retired during the handshake: also send `ChannelMessages.Reconnect(ReconnectReason.CertificateRotated)`, and close the stream with `OK` 60 s later, as for `confirm` (CONTRACT.agent-identity.md [§7.4]). | ||
| 7. Close the stream no later than `Identity.CertificateNotAfter` with `AgentIdentityErrorCode.CertificateExpired`, and when the identity is revoked with `AgentIdentityErrorCode.IdentityRevoked` (CONTRACT.agent-identity.md [§7.4]). |
There was a problem hiding this comment.
The guidance should close the window where a revocation during the handshake is missed.
VerifyHelloAsync reads revocation once (ChannelOpening.cs:310). If a revocation commits after that read, but before the host has added the stream to whatever list its revocation path walks, a host following these steps never closes that stream. It stays authenticated until Identity.CertificateNotAfter, which can be up to 90 days. That breaks invariant 5 ("closes live streams").
Suggestion: say that the host must register the stream in its live-stream table before sending Welcome, and then re-check revocation (or take the same lock its revocation path takes). Alternatively, the revocation path can mark the identity so that any later registration closes immediately.
Note
Human-tuned, LLM-assisted content.
There was a problem hiding this comment.
Fixed in 7897f72 and 36745fc: the host adds the stream to the live streams its revocation path closes before it ends the handshake, and a test pins that the handshake reads the registry inside that call, so a revocation is caught either by that read or by the revocation path.
Note
LLM-assisted content (no human feedback).
| Then send `ChannelMessages.Reconnect(ReconnectReason.CertificateRotated)` on each stream that the retired certificate opened, and close those streams with `OK` 60 s later (CONTRACT.agent-identity.md [§7.4]). | ||
| Answer `204`, also when the certificate was already current (CONTRACT.agent-identity.md [§5.5]). | ||
|
|
||
| ### `check-in` |
There was a problem hiding this comment.
The host guide never mentions last_seen_at.
The shared intent (Configuration and metadata) is precise about this field:
- creating the identity initializes it;
- every accepted renew, every confirm (including an already-current retry), every check-in, every authenticated
Helloand every later agent message on an authenticated stream sets it; - an enrollment replay and a stream closure do not.
The library has no storage, so this falls to the host, but none of the per-endpoint steps here say it. DVLS and PSU could then handle it differently, which is the drift this library exists to prevent.
Suggestion: one line in each of the enroll, renew, confirm, check-in and channel sections.
Note
Human-tuned, LLM-assisted content.
There was a problem hiding this comment.
Fixed in 7897f72: each section of the host guide now says when to set last_seen_at, as the shared intent's §Configuration and metadata defines it.
Note
LLM-assisted content (no human feedback).
|
|
||
| - `ICertificateRegistry` finds, by thumbprint, a certificate that the authority issued, in any status including `Retired`, or returns null when there is none or its identity was deleted. | ||
| Index certificates by `AgentIdentityProtocol.ComputeThumbprint`, not by the SHA-1 `X509Certificate2.Thumbprint`. | ||
| Each `RegisteredCertificate` holds the DER exactly as issued, and the current `Status` and `IdentityRevoked`, so that a revocation takes effect from the next request. |
There was a problem hiding this comment.
Hosts get no guidance on registry and nonce-store load.
- Every request that passes parsing, the tag and the time window costs one
FindByThumbprintAsync, and all three of those checks are attacker-controlled. That means an unauthenticated caller with randomkeyids can drive registry queries. - Each channel connection costs two registry reads (the opening, and the re-read at
Hellothat §7.4 requires) plus one nonce write. At 10k reconnects/s after an authority restart, that is 20k reads/s and 10k atomic inserts/s.
This line ("so that a revocation takes effect from the next request") also tends to discourage caching.
Suggestion: state the per-request and per-connection rates, and say what can be cached safely:
- the DER by thumbprint, since it is immutable;
- a negative cache or per-source rate limit for misses.
Optionally, split the DER lookup from a lightweight state-only lookup for the Hello re-check.
Note
Human-tuned, LLM-assisted content.
There was a problem hiding this comment.
Fixed in 7897f72: the host dependencies now state the registry and nonce-store load per request and per stream, with the indexed lookup and the bounded caching that keep revocation effective from the next request.
Note
LLM-assisted content (no human feedback).
| // which .NET writes for a key imported with explicit parameters. | ||
| ["specifiedCurve"] = key => | ||
| { | ||
| using ECDsa explicitKey = ECDsa.Create(key.ExportExplicitParameters(includePrivateParameters: false)); |
There was a problem hiding this comment.
This row fails on macOS.
ExportExplicitParameters throws PlatformNotSupportedException: Only named curves are supported on this platform there, so dotnet test reports 1678 passed and 1 failed for anyone developing on macOS. CI runs this job only on windows-2022, so it doesn't catch it.
Suggestion: build the specified-curve SubjectPublicKeyInfo by hand (with AsnWriter or the existing TestCsr builders), so the fixture doesn't depend on the platform.
Note
Human-tuned, LLM-assisted content.
There was a problem hiding this comment.
Fixed in d77a3ce: the specified-curve row now writes the RFC 3279 ECParameters itself with AsnWriter, so it no longer calls ExportExplicitParameters.
Note
LLM-assisted content (no human feedback).
| /// <summary> | ||
| /// The outcome of authenticating a signed request. | ||
| /// </summary> | ||
| public sealed class AuthenticationResult |
There was a problem hiding this comment.
There are seven result types with the same shape (simplification).
AuthenticationResult, MetadataValidationResult, RequestBodyParseResult<T>, CsrValidationResult, EnrollmentAuthorizationResult, ChannelOpeningResult, ChannelHandshakeResult and the private Verification in RequestAuthenticator all have:
Succeededwith[MemberNotNullWhen];- a value;
ErrorandFailureDetail;- internal
Success/Failurefactories.
RequestBodyParseResult<TBody> is already the generic form.
Suggestion: one AgentIdentityResult<T>, where ChannelHandshakeResult becomes AgentIdentityResult<AuthenticatedStream> with a record holding Identity, Metadata and Welcome. That is roughly 300 lines less and a smaller public API, at no compatibility cost since the package is unreleased.
- The authenticated-identity intent is unaffected, because
AuthenticatedIdentitykeeps its internal constructor. - The tradeoff is that hosts read
result.Valueinstead of named properties, and the README's call-order section needs updating.
Note
Human-tuned, LLM-assisted content.
There was a problem hiding this comment.
Done in 9476269 as a shared base, AgentIdentityResult, holding Error, FailureDetail and the failure annotation on Succeeded, while each type keeps its named value members, so hosts and the README's call order read as before.
Note
LLM-assisted content (no human feedback).
| public X509SignatureGenerator SignatureGenerator => X509SignatureGenerator.CreateForECDsa(key); | ||
| } | ||
|
|
||
| private sealed class FixedTimeProvider(DateTimeOffset now) : TimeProvider |
There was a problem hiding this comment.
Test duplication (simplification, about 600 lines across the test project).
Here, FixedTimeProvider, AdvancingTimeProvider and SequenceRandomNumberGenerator redefine what Support/TestDoubles.cs already provides (SteppingTimeProvider, ScriptedRandomNumberGenerator). EnrollmentTokenTests.cs:279 has another copy.
The larger items elsewhere:
CertificateSigningRequestTests: about 8 dictionary-plus-theory groups and about 10 facts all runAssertInvalid(Validate(bytes)), and could be oneInvalidRequeststheory (about −130 lines).- The same
BaseUrlrule table is tested twice, inEnrollmentTokenTests.cs:106-154,233-274andIdentityConfigTests.cs:60-127,374-400. ChannelHandshakeTests:EarlierFailureWinsandTwoSignatureFailuresAreSignatureInvalidhave identical bodies, and five single-failure facts could be one theory overFailures.Keys.- Channel-opening and dispatch setup appears three times (
TestAuthority,VectorAuthority,ChannelOpeningTests.OpenAsync,ChannelHandshakeTests). VectorData.KeyNamesandVectorData.RegisteredCertificateNamesare unused.
Note
Human-tuned, LLM-assisted content.
There was a problem hiding this comment.
Done in ced3d39..d60c047: the tests reuse the shared doubles, the CSR rejections are one RejectsInvalidRequest theory, the base URL table is shared, the handshake failures are two theories, the channel-opening setup is shared and the unused members are gone, keeping every case and every caught mutant (300 to 275 methods).
Note
LLM-assisted content (no human feedback).
0fe5d6f to
87a104a
Compare
The authority library holds what every Agent Identity authority must do the same way, so that DVLS and PSU cannot drift apart on the agent-facing protocol. The feature's shared intent is linked beside it. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add the Devolutions.AgentIdentity library, for the agent-facing protocol rules that every Agent Identity authority shares, and its test project, laid out like utils/dotnet. The library references Devolutions.Agent.Channel, which becomes its package dependency, and its build fails on any warning, including a missing doc comment. The test project links docs/agent-identity/test-vectors.json into its output so that vector changes apply without a copy. build.ps1 builds and packs the library alone. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The contract gives every base64 and base64url value one canonical encoding, and an authority rejects any other form. The BCL decoders skip white space and ignore the pad bits of the final quantum, so they accept several encodings of one value. Add internal decoders that accept only the canonical RFC 4648 form, optionally of an exact decoded length, and a base64url encoder. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Every authority must answer with the same error codes, statuses and bodies, because the agent decides from them whether to retry, discard a pending enrollment or stop using an identity. Add AgentIdentityErrorCode with its wire values and its HTTP and gRPC statuses, including unavailable for an overloaded or failing server, and AgentIdentityError, which writes the JSON error body and the RpcException with the error-code and server-time trailers. Each message is a fixed English sentence, so it never repeats request data. AgentIdentityProtocol holds the 64 KiB body limit, the timestamp format and the certificate thumbprint. The tests read the shared vector file into typed records and yield one theory row per vector name, so that each vector is its own test. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Run the Devolutions.AgentIdentity tests and build its package in the .NET utils job, and add the package to the NuGet publish matrix next to Devolutions.Agent.Channel, which it depends on. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The library and test projects treat warnings as errors, so an advisory published later against any direct or transitive package would fail every build of them, including unrelated pull requests and the scheduled publish run, while the rest of the repository only warns. Exclude the NuGet audit warnings NU1901 to NU1904 from warnings as errors so that advisories stay visible without breaking builds. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…tore Request authentication resolves a keyid only through a certificate that the authority registered, and accepts each nonce once, so the host provides both through interfaces over its own storage. RegisteredCertificate holds the certificate DER as issued, from which the validity and the key are read, with the certificate status and the identity's revocation. It refuses an undefined status, which would otherwise draw identity_unknown and end the identity for the agent. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Every authority must accept and refuse the same signed requests with the same errors, so the library runs the RFC 9421 profile of the contract for renew, confirm, check-in and the agent channel opening. RequestAuthenticator refuses a body over 64 KiB first. It then parses Signature-Input, Signature and Content-Digest with an RFC 8941 parser that fails on a repeated key instead of keeping the last one, checks the profile, the tag and the time window against one reading of the clock, resolves the keyid through the registry, verifies the digest and the signature, and records the nonce only once all of these pass. @signature-params is the serialization of the parsed member, so one parse drives both the checks and the signature base. Only AuthenticateAsync creates an AuthenticatedIdentity, and it refuses Connect. AuthenticateChannelOpeningAsync returns a ChannelOpening with a fresh challenge and no identity, since a stream is authenticated only once the agent signs that challenge. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The contract places an anchor before each section heading, and a marker cites the anchor when the file has one, because it survives a renamed heading or a renumbered section. Every contract marker and contract link under dotnet/ now uses the anchor, such as `§errors` for §5.4, instead of the section number or an earlier prefixed form of the anchor. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The README linked most contract sections through fragments derived from their headings, which break when a heading is renamed or renumbered. Every contract link now uses the anchor the contract places before the section. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The authority ignores the CSR's subject and attributes, and the contract does not require them in canonical DER form, since nothing it decides depends on them. The parser still read the subject and the attributes element as DER, so a subject or an attribute set whose length took more octets than DER allows failed the request with invalid_request. The subject is now read as a BER SEQUENCE and the attributes as a BER constructed [0], neither parsed further. The rest stays DER: a CSR is DER, the self-signature covers the request info as received, and each key must have a single SubjectPublicKeyInfo. Tests now also pin that the request info, the version and the SubjectPublicKeyInfo stay DER. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The specified-curve row of RejectsKeyWithoutANamedCurve took its key from ExportExplicitParameters, which throws PlatformNotSupportedException on macOS, so the suite could not pass there. The row now writes the ECParameters of RFC 3279 section 2.3.5 itself, with the secp256r1 values of SEC 2 section 2.4.2; these are the bytes .NET writes on Windows. The request is still signed by the key whose point it carries, so it fails only for its curve parameters. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… from its enrollment decision Issuing a new identity's certificate for a caller-supplied Guid let a host certify a key for an identity that no enrollment created. A new identity's certificate is now issued only from the EnrollmentDecision that creates it, and no public issuance path takes an identity ID. EnrollmentRules.Decide mints a random identity ID for each decision to create an identity and exposes it as EnrollmentDecision.NewIdentityId, which is null for every other outcome. The new Issue overload takes that decision and throws ArgumentException for a decision of another outcome. The Guid overload becomes the private implementation that the enrollment and renewal overloads share. Only the library constructs an EnrollmentDecision, so a host cannot forge one for another ID. BREAKING CHANGE: IdentityCertificateIssuer.Issue(Guid, ...) is removed; issue a new identity's certificate with IdentityCertificateIssuer.Issue(EnrollmentDecision, ...), passing the CreateIdentity decision of EnrollmentRules.Decide, and create the identity under its NewIdentityId. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…n_at and load A revocation that commits after the handshake's registry read, but before the host tracks the stream, left the stream authenticated until its certificate expired. The host now adds the stream to the live streams its revocation and deletion paths close before it ends the handshake, so a revocation is caught either by that read or by the revocation path. The stream is keyed by `ChannelOpening.CertificateThumbprint` until the handshake reveals the identity, and that property's doc says so. Each section now says when to set `last_seen_at`, as the shared intent defines it, and the host dependencies state the registry and nonce-store load per request and per stream, with the caching and indexing that keep revocation effective from the next request. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… the call A host closes a revocation race by adding the stream to its live streams before it ends the handshake, which works only if the handshake reads the registry within the call rather than reusing the opening's answer. The test sets a flag before each call that ends the handshake and checks that a registry lookup ran after it. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Seven public result types each declared the same Error and FailureDetail properties and the same failure annotation on Succeeded. They now derive from AgentIdentityResult, which holds those members once, and each type overrides Succeeded only to name its own value members. The override keeps the host's nullable flow analysis: [MemberNotNullWhen] cannot name an inherited member, but the compiler applies the base property's annotation and the override's together. A compile-only file in the test project dereferences each type's members on both branches, so losing either annotation fails the build. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…bles The certificate issuer tests defined a fixed clock, an advancing clock and a generator of whole draws, and the enrollment token tests a generator of one byte stream. They now use the doubles in `Support/TestDoubles.cs`. The stepping clock repeats its last reading where the advancing one kept moving, and the scripted generator serves one byte stream where the issuer tests' generator returned whole draws, so each test gives the code under test the readings and bytes it gave before. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…heory About twenty facts and theories each built a malformed request and asserted only that validation rejects it with invalid_request. They are now named rows of one `RejectsInvalidRequest` theory, each keeping the explanation and markers of the rule it breaks. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The enrollment token and identity config tests listed the same URLs that are not an absolute `https` URL without user information, a query or a fragment. Both now draw them from `Support/UnacceptableUrls.cs`, whose rows cite the contract section of each field. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…heories `EarlierFailureWins` and `TwoSignatureFailuresAreSignatureInvalid` had the same body over two halves of the failure pairs, so one theory now runs every pair. The facts and theories that each checked one failure's code and status are now `FailureClosesTheStreamWithItsCode`, run over every named failure, with each failure's markers where it is defined. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Several tests and helpers each authenticated a channel opening from a signed request or from the vector connect case, and two signed the matching Hello. `TestAuthority.OpenChannelAsync` and `TestAuthority.ValidHello` now open a stream of `device-a` and answer it, `VectorAuthority.OpenChannelAsync` authenticates a connect case, and `VectorChannel.OpenAsync` opens the vector stream over any registry. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
No test draws its rows from `VectorData.KeyNames` or `VectorData.RegisteredCertificateNames`. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
ReadsTheClockOnce gives the issuer three distinct readings, so an issuer that took both validity bounds from readings after the first fails it. TestAuthority.ValidHello signs over the nonce of the opening it answers rather than the default one, VectorAuthority builds the signature lines only for the requests that use them, and stray blank lines are fixed. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An authority stores SHA-256 of the 32 bytes a token's secret encodes and accepts the secret only in its canonical base64url form (CONTRACT.agent-identity.md §2), so that every authority stores the same hash for the same token. EnrollmentToken.SecretHash and the hash ReadBearerToken returns now cover the decoded bytes rather than the secret's text. ReadBearerToken answers token_invalid for a secret whose pad bits are not zero, since such a secret decodes to the bytes of a canonical one. A hash stored from an earlier build of the library no longer matches its token. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A token's product base URL carries no user name or password (CONTRACT.agent-identity.md §2), which could make an address read as one host while it reaches another. The library already refused any user information; rows with a password alone and with a user name and password now pin it, and the user name rows cite the contract. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…leted An authority revokes a token that is no longer wanted instead of deleting it (CONTRACT.agent-identity.md §2.1). A revoked token keeps its secret hash, so a lost enrollment response stays recoverable, and gets token_invalid for anything but that replay, as a deleted token did. EnrollmentTokenState's deleted parameter and Deleted property become revoked and Revoked, and the docs and failure details say revoked. BREAKING CHANGE: EnrollmentTokenState(bool deleted, ...) is now EnrollmentTokenState(bool revoked, ...), and EnrollmentTokenState.Deleted is now EnrollmentTokenState.Revoked. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A token's u and a config's agent_channel_url are read as text: each begins with https:// and contains no @, and any @ counts as a user name (CONTRACT.agent-identity.md §2 and §5.2), because URL parsers disagree on forms such as https://@host. The library refused only an @ that .NET's parser placed in the user information, so https://host/dvls/a@b made a token whose u holds an @. It now refuses any @ in the URL text. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Certificate validity reaches the library as local DateTime values from X509Certificate2, and it converts each straight to UTC. The conversion is exact in the hour that a daylight-saving fall-back repeats only because that DateTime keeps its ambiguous-DST bit, which no test pins without swapping the process's time zone. An UNTESTED comment at each conversion says so, and what would break it. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A token's u holds only ASCII characters, so that certificates can carry it as an IA5String (CONTRACT.agent-identity.md §2); an internationalized host is written in its xn-- form. EnrollmentToken.Create now refuses a product base URL that is not ASCII, which also refuses a host holding a character such as the fullwidth commercial at. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…file Every certificate in a chain is signed with ecdsa-with-SHA256 by a P-256 key or with ecdsa-with-SHA384 by a P-384 key, never with RSA, and every CA certificate, the root included, carries basicConstraints with cA set and key usage with keyCertSign (CONTRACT.agent-identity.md §4). The issuer signed with SHA-256 and whatever generator the host's IRootSigner gave, so an RSA root, another curve, or a root that is not a CA produced chains the contract forbids. IdentityCertificateIssuer.Issue now picks the hash from the issuer key's curve and throws ArgumentException, with ParamName issuer, unless the issuer certificate has a P-256 or P-384 key, basicConstraints with cA set and key usage with keyCertSign, and its generator's algorithm identifier for that hash is exactly the matching ECDSA one without parameters. IRootSigner's docs and the README name the profile. BREAKING CHANGE: IdentityCertificateIssuer.Issue refuses an issuer outside that profile; a host whose root is RSA, another curve, or not a CA certificate must replace it. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An enrollment whose CSR names no enrollment URL, or not the presented token's u, gets token_url_mismatch with HTTP 403, a permanent outcome for the agent (CONTRACT.agent-identity.md §5.4). It takes the next unused value, and maps to UNAUTHENTICATED like the other token codes. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The agent's CSR requests the enrollment URL extension in an extensionRequest, naming the base URL it sends the request to, and the server reads it there while ignoring every other attribute and requested extension (CONTRACT.agent-identity.md §4). CertificateSigningRequest.EnrollmentUrl holds the value of the one enrollment URL extension that the attributes request, as an IA5String read as text, or null when there is none, there are several, its value is not one IA5String, or the attributes cannot be read; a CSR stays valid either way, since enrollment, not validation, refuses it. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A token works only at the base URL it names: at enrollment, a CSR that names no enrollment URL, or not the presented token's u without trailing slashes compared as text, gets token_url_mismatch, right after invalid_request and before the key lookup (CONTRACT.agent-identity.md §2.1 and §4), so that a server relaying a token from another address is refused. EnrollmentTokenState takes the token's u, which EnrollmentToken now exposes as ProductBaseUrl for the host to store, and EnrollmentRules.Decide takes the request's CSR. A decision to create an identity carries the token's u without trailing slashes as EnrollmentUrl, which every certificate of the identity names. BREAKING CHANGE: EnrollmentTokenState takes the token's product base URL after revoked, and EnrollmentRules.Decide takes the request's CSR after the token state. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…icate Every certificate issued for an identity carries the enrollment URL extension once, non-critical, holding as an IA5String the u of the token that enrolled it, without trailing slashes, so that an agent can refuse a certificate from an authority at another address (CONTRACT.agent-identity.md §4). The enrollment overload of IdentityCertificateIssuer.Issue takes the URL from the decision, and the renewal overload takes the identity's enrollment URL, which the host keeps from that decision; it throws ArgumentException for one that is empty, ends in a slash or is not ASCII. The extension's OID is one named constant. BREAKING CHANGE: the renewal overload of IdentityCertificateIssuer.Issue takes the identity's enrollment URL after the renewing identity. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server ignores every CSR attribute but the enrollment URL, so the parser now skips the values of other attributes unread, and a malformed value of another attribute no longer hides a well-formed enrollment URL. IdentityCertificateIssuer.Issue refuses a generator over another key than the issuer certificate's, whose certificates would not chain, and EnrollmentTokenState refuses a product base URL that is empty once its trailing slashes are removed. Tests pin a critical flag on the requested extension, an attribute that is not a SEQUENCE and an attribute set of indefinite length, and the docs say the attributes are read for the enrollment URL. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The enrollment URL extension's OID sits under the enterprise number that RFC 5612 reserves for documentation, and the contract moves it under Devolutions' own enterprise number before release. Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared vectors now give each CSR's outcome at enroll and at renew in expected_at, and name the enrollment URL a valid CSR carries. The loader reads both members, and a theory runs every CSR vector through validation and then EnrollmentRules.Decide, with a token whose u is the entry's enrollment URL, or RenewalRules.Decide; so a CSR with an empty extensionRequest is refused at enrollment with token_url_mismatch and accepted at renewal. A CSR row also pins that the enrollment URL extension requested in two extensionRequest attributes names no enrollment URL (CONTRACT.agent-identity.md §4). Issue: DGW-479 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0bad2a8 to
ea60050
Compare

Add
Devolutions.AgentIdentity, the .NET library for what every Agent Identity authority must do the same way under the agent-identity contract, so that DVLS and PSU cannot drift apart on the agent-facing protocol.The host keeps storage, hosting, management, permissions and the root key at rest, and supplies the interfaces the library calls: a certificate registry looked up by thumbprint, a nonce store, a root signer, a clock and a random number generator.
The library has no ASP.NET Core dependency and hosts no endpoint or gRPC service.
The library covers:
keyidandnonceencodings;Content-Digest;Hello, an undecodable first message, or noHelloin time;uis ASCII with no@, the SHA-256 of the 32 bytes their secret encodes, accepted only in canonical form, and the enrollment decision of §2.1, which refuses a CSR that does not name the token'suwithtoken_url_mismatch;confirmbody, which must be empty;The public API is grouped as follows:
RequestAuthenticator, which returns anAuthenticationResult;AuthenticatedIdentity;SignedHttpRequest,AgentOperation,RegisteredCertificateandCertificateStatus;ICertificateRegistryandINonceStore.ChannelOpening, withVerifyHelloAsync,HelloTimedOutAsyncandFirstMessageUnreadableAsync;ChannelOpeningResult,ChannelHandshakeandChannelHandshakeResult;ChannelMessages,ReconnectReasonandRenewalRequestReason.EnrollmentTokenandEnrollmentTokenState;EnrollmentAuthorizationand its result;EnrollmentRules,EnrollmentDecision,EnrollmentOutcomeandIssuedKeyMatch;FriendlyNameFormat.CertificateSigningRequestandCsrValidationResult;IdentityCertificateIssuerand the host interfaceIRootSigner;RenewalRules,RenewalDecisionandRenewalOutcome.EnrollRequestBody,RenewRequestBody,ConfirmRequestBodyandCheckInRequestBody, parsed into aRequestBodyParseResult;AgentMetadataandMetadataValidationResult;IdentityConfig;ResponseBodies.AgentIdentityError,AgentIdentityErrorCodeand its extensions;AgentIdentityResult, the base of every result type;AgentIdentityProtocol.Only the signature check and the channel handshake create an
AuthenticatedIdentity, the type that code acting for an identity takes, so an identity is never taken from a value the agent or a caller sent.Likewise, a new identity's certificate is issued only from the enrollment decision that creates it, under the identity ID the library mints, so no public issuance path takes an identity ID.
The RFC 9421 profile is written over the BCL's ECDSA rather than NSign, because NSign's parser keeps the last of repeated parameters, which step 1 forbids, and its request verification is built for ASP.NET Core.
The library is derived from the contract, the shared test vectors and the shared intent alone, as one more independent implementation, and its tests run every shared vector.
Every test names the source of its expected value with an
// oracle:marker.dotnet/AgentIdentity/README.mdtells a host developer how to integrate. It covers:trust-anchor,enroll, the signedrenew,confirmandcheck-inrequests, and the agent channel, including gRPC message size limits;The package lives in
dotnet/AgentIdentity/and targetsnet10.0.It is built and tested in the .NET library CI job, and
publish-libraries.ymlpublishes it next toDevolutions.Agent.Channel, on which it depends.Issue: DGW-479
Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com