Repository navigation
feat(dotnet-agent-identity): add the authority library - #2125
Open
Benoît Cortier (CBenoit) wants to merge 45 commits into
Open
Benoît Cortier (CBenoit) wants to merge 45 commits into
Benoît Cortier (CBenoit) wants to merge 45 commits into
Conversation
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>
Every authority must accept and reject the same metadata, whether it arrives in a request body or in a channel Hello, so the limits of the contract live in one place. Add AgentMetadata, a read-only dictionary that only validation creates, and AgentMetadata.Validate, which enforces the key pattern, the key count, the value length in UTF-8 bytes, the control-character ban and the 16 KiB total, and rejects repeated keys and strings that are not valid Unicode. A failure carries invalid_request and a detail for host logs that never quotes an invalid key. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
An authority takes only the public key from an agent's CSR, after the CSR proves possession of it, and each key must have exactly one SubjectPublicKeyInfo so that its SHA-256 identifies it among every key the authority has issued. Add CertificateSigningRequest.Validate, which accepts a DER PKCS #10 request of version 0 without trailing bytes, whose key is an uncompressed point on the named curve P-256 and whose self-signature is ecdsa-with-SHA256 without parameters over the exact request info. The subject and attributes are read only as DER elements of their type. The validator checks the curve equation itself, since the platform throws a platform-specific exception for a point off the curve, and rejects an r or s that DER makes negative, which the platform verifier accepts as unsigned. The result keeps the exact SubjectPublicKeyInfo and its SHA-256. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The authority must classify a request body, its CSR and its metadata the same way on every endpoint, and must read the same values as any proxy, log or second parser would. Add EnrollRequestBody, RenewRequestBody and CheckInRequestBody, whose Parse methods share one reader. It accepts one JSON object without comments or trailing commas, rejects a member name repeated at any depth, including inside unknown members, and a string that is not Unicode text, then reads csr as canonical base64 into a validated CSR and metadata through AgentMetadata.Validate. A null member counts as absent and unknown members are ignored. Every failure is invalid_request with a detail for host logs. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Every authority must issue the same leaf profile, so that an agent and any relying party read an identity the same way whichever authority issued it. Add IRootSigner, through which the host lends its root certificate and a signature generator without exposing the root key, and IdentityCertificateIssuer.Issue. The leaf holds only the CSR's key, with CN and SAN URI built from the identity ID, clientAuth, a critical digitalSignature key usage, a subject key identifier, an authority key identifier equal to the root's, and no basic constraints. It is valid from the clock truncated to the second for the lifetime, 90 days by default, capped at the root's notAfter, and its serial number is 16 random octets with the top bit cleared, drawn again if zero. A lifetime under one second, a root that expires by notBefore, or a root without a subject key identifier throws. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Every authority must write the same dvaet1 token format, so that one agent enrolls with any of them. Add EnrollmentToken.Create, which writes dvaet1.<bag>.<secret>. The bag holds the product base URL in normalized form and without trailing slashes, since the agent appends the API path to it. The secret encodes 32 bytes from the injected generator, and SecretHash, which the authority stores instead of the token, is the SHA-256 of the secret text. Create rejects a URL that is not an absolute https URL, carries user information, a query or a fragment, or makes the token longer than 4096 bytes. ToString and exception messages never reveal the token. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Every authority must refuse the same enroll credentials, because the agent treats token_invalid as permanent and never retries that token. Add EnrollmentAuthorization.ReadBearerToken, which returns the hash under which the authority finds the token, or token_invalid when the Authorization field is missing, repeated, not Bearer, or holds no dvaet1 token of at most 4096 bytes. As RFC 9110 defines credentials, the scheme is case-insensitive and one or more spaces precede the token. The bag is not decoded, since the authority finds a token by its secret alone, and the hash covers the secret text, so a non-canonical secret matches no stored token. The failure detail for logs is a fixed sentence that never holds any part of the token. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Every authority must answer an enroll request the same way, because the agent retries a lost enrollment response with the same token and CSR, and treats every token_* code as permanent. Add EnrollmentRules.Decide, which applies the contract's precedence to the stored token state and to the issued certificate that holds the CSR key, if any. The current key of a non-revoked identity enrolled with the same token replays that identity without counting a use, even with a deleted, expired or exhausted token. Any other issued key gets the first matching key rule, or token_invalid with a deleted token; the key of a deleted identity gets identity_revoked, since the authority keeps only its SPKI hash and issuance time. A new key gets token_invalid with a deleted token, then token_expired, then token_exhausted, and otherwise creates an identity. The host supplies these facts through EnrollmentTokenState and IssuedKeyMatch, with the CertificateStatus of the matching certificate. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Every authority must name a new identity the same way from its token's
friendly-name format, and reject the same formats when a token is
created.
Add FriendlyNameFormat. TryParse accepts literal text, {{ and }}, and
the placeholders of the known metadata keys and {token_name}, and
explains an unpaired brace or an unknown placeholder to the
administrator. Evaluate substitutes the values, with a missing metadata
value empty, trims Unicode White_Space, truncates to 255 Unicode scalar
values without splitting one, trims again, and falls back to the
identity ID when the name is empty. A lone surrogate becomes U+FFFD, so
the name stays well-formed. Default is {hostname}.
Issue: DGW-479
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A default-initialized CertificateStatus was Pending, so a host that never set a status passed IssuedKeyMatch.ForIdentity a valid one. Number the statuses from 1, like the library's other public enums, so that an uninitialized status is undefined and ForIdentity rejects it. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A friendly-name format is administrator input that may hold a pasted enrollment token, and tokens never appear in logs, not even partially. A parse error repeated an unknown placeholder, and ToString returned the whole format, so either could carry a token into a log. A parse error now says what is wrong and at which character, counted in Unicode scalar values from 1, and lists the valid placeholders, without any part of the format text. ToString returns the type name, and the host stores the text that it parsed. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Certificate statuses start at 1, so 3 is Retired, a defined status. The test for an undefined status checks the default value, which an uninitialized status holds, and 4, which follows the last status. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
FriendlyNameFormat.Evaluate took any string dictionary, so a host could name an identity from metadata that never passed the protocol's limits on keys and values. Evaluate now takes AgentMetadata, which only validation and the request body parsers create. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The FriendlyNameFormat remarks said that no member returns any part of the format text, but Evaluate returns its literal text as part of the friendly name. Limit the guarantee to parse errors and ToString. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The contract now has the server ignore a CSR's attributes, including an extensionRequest with an empty Extensions list, and the valid CSR vector will be regenerated with an empty attribute set. Add tests, built without the vector file, that accept a CSR whose attributes hold an empty extensionRequest and one with an empty attribute set. The tests that alter the valid vector now check that it is accepted and locate its outer length with System.Formats.Asn1, so they hold no octet or length of it. The non-canonical base64 tests use requests built to have pad bits and both + and /, which a regenerated vector may lack. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A signed channel opening never authenticates a stream on its own, so every authority must check the agent's first message against the stream's challenge the same way before it acts for the identity. ChannelOpening.VerifyHello requires a Hello whose correlation_id is the Challenge id and whose proof is a raw 64-byte P-256 signature, by the key of the certificate that opened the stream, over the proof domain, a zero byte, the challenge and the connect nonce. A message failing any of these is signature_invalid, as is HelloTimedOut, which the host calls when no message arrives within ChannelHandshake.HelloTimeout of 10 s; only then does invalid metadata draw invalid_request. Success yields the identity of the opening certificate for the connect operation, the validated metadata, and a Welcome that answers the Hello with the server time. The first call to either method ends the handshake, and any later call throws. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A stream closes no later than the notAfter of the certificate that opened it, and the agent must answer the challenge within 10 s, so the handshake must fail once either deadline has passed, whatever the first message holds, and the host must know when the certificate expires before the stream is authenticated. VerifyHello and HelloTimedOut read the clock once and return certificate_expired after the certificate's notAfter. VerifyHello then returns signature_invalid when it runs more than HelloTimeout after the opening was verified, before it reads the message; exactly 10 s is in time. The opening records that instant from the authority's clock once verification ends, so time spent in the registry and the nonce store does not shorten the agent's window. ChannelOpening.CertificateNotAfter lets the host close a stream still waiting for its Hello at that time. The Welcome carries the same clock reading. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The identity of a stream stays hidden until its proof verifies, so the host cannot close a stream still in its handshake when its identity is revoked or deleted or its certificate retired, and would authenticate it anyway, although revocation must block authentication. ChannelOpening.VerifyHelloAsync replaces VerifyHello. It reads the clock, then the certificate registry once, and checks in the order of the signed-request rules: a certificate no longer registered is identity_unknown and a revoked identity identity_revoked, then come expiry, the Hello deadline, the message and the metadata as before. A certificate that a confirm retired during the handshake still authenticates and is reported as retired, so the host reconnects the stream as for any retired certificate rather than send a healthy agent the terminal identity_unknown. The handshake ends before the registry lookup, and a registry answer for another certificate or identity throws, as in request authentication. HelloTimedOut is unchanged. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The shared vectors now give the private key of the valid CSR and a Content-Type field on some requests. The loader rejects unknown members, so that a change it does not model is noticed, and failed to load the file. Read both as optional members, typed like the fields beside them; the signature profile does not cover Content-Type. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The agent appends a path both to the product base URL of an enrollment token and to the agent channel URL of a config, and the contract states the same rules for both: an absolute https URL without a query or fragment. Move the token's URL check and normalization into an internal BaseUrl helper, unchanged, so that both URLs follow one implementation. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The enroll and check-in responses and the ConfigUpdate message all carry the identity's config, so every authority must write it the same way. IdentityConfig.Create takes the revision, an optional agent channel URL and optional product-specific fields, and fails with ArgumentException for anything the contract rejects. The URL must be an absolute https URL without user information, query or fragment, and is written without trailing slashes, since the agent appends the service path to it. A product field has a snake_case name that no contract member has, and a value other than null. ToUtf8Json writes version 1, the revision as an unsigned 64-bit integer, the URL only when present, and the product fields. The config is written once at creation, so that a value UTF-8 JSON cannot carry fails there rather than in a response. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
IssuedKeyMatch.ForDeletedIdentity said that the key of a deleted identity gets identity_revoked whatever token enrolled it, but a deleted presented token gets token_invalid first, since it authenticates nothing but a replay. Say so. AgentIdentityError.MalformedBody did not say when a host uses it, though the body parsers return their own invalid_request. It is for a body that the host's framework cannot read or hand to the parsers, such as one that is not UTF-8 application/json. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The trust-anchor, enroll, renew and check-in responses are agent-facing, so every authority must write them the same way. ResponseBodies writes each one as a UTF-8 JSON object with exactly the members of the contract. A certificate is its DER in canonical base64, and each published root also carries its thumbprint and its validity as RFC 3339 UTC timestamps. IDs are lowercase hyphenated UUIDs, and the nil UUID is refused, since it stands for no ID. A certificate chain must be leaf first: the issuer of each certificate must be the subject of the next, which RFC 5280 makes byte for byte equal. The config is written as IdentityConfig wrote it, and renewal_requested as a JSON boolean. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
ConfigUpdate, RenewRequested and Reconnect carry agent-facing values on the agent channel, so every authority must build them the same way. ChannelMessages builds each one as a ServerMessage with a fresh lowercase hyphenated UUID id and no correlation_id. ConfigUpdate carries the JSON object that IdentityConfig writes in config_json. RenewRequested carries the reason admin or rotation, and Reconnect the reason certificate_rotated, each from an enum whose values start at 1, so an undefined reason throws. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A renewal certifies a new key for an existing identity, but the only issuance method took a Guid, so a host could issue a renewal for an identity that a request merely named. IdentityCertificateIssuer.Issue now has an overload that takes the AuthenticatedIdentity of a renew request, which only the signature check creates, and throws ArgumentException for an identity authenticated for another operation. The Guid overload is documented as the one for the identity that the authority mints at enrollment. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The contract gives a renew request one outcome per CSR key, and an authority that answers differently could hand out another identity's pending chain or reuse a public key, so every authority must apply the rules the same way. RenewalRules.Decide takes the AuthenticatedIdentity of the renew request and the IssuedKeyMatch of the CSR key, or null for a key never issued. The pending key of that identity returns its pending chain, so a device recovers a lost response. Any other issued key gets invalid_request: the identity's current or retired key, any key of another identity, revoked or not, and any key of a deleted identity. A key never issued gets a new pending certificate, which retires the previous one. An identity authenticated for another operation throws ArgumentException. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The certificate list check of ResponseBodies took a description only to vary two messages, which the parameter name already tells apart. The body tests repeated the reading of the roots and of the chain. Drop the description and give the tests one helper for each array. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A config is immutable, and a response body must write the certificates that it checked, but no test caught a ToUtf8Json that returned the config's own buffer or a writer that read the caller's certificates a second time. Check that changing the returned JSON leaves the config unchanged, and that the trust-anchor, enroll and renew writers read their certificates once. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The handshake now looks the opening certificate up again, so ChannelOpening.VerifyHello became VerifyHelloAsync, and the helper that authenticates a test identity through a channel no longer compiled. Await VerifyHelloAsync in that helper. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
IdentityConfig checked only the names of the product-specific fields, so
a value such as {"maxSessions": 4} reached the enroll and check-in
responses and ConfigUpdate, against the snake_case keys of the
contract's protocol conventions, and a value could repeat a member name,
which two parsers may read differently.
Create now walks each value, through nested objects and arrays, before
writing it. Every member must have a snake_case name that no other
member of its object has, names compared after unescaping, and a value
other than null, since an absent field is omitted. An array element may
still be null. The walk keeps its own stack, so a deep value cannot
exhaust the thread's stack.
Issue: DGW-479
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
DVLS and PSU embed the library to serve the agent-facing protocol, and using it correctly takes facts that no single type documents: what stays with the host, the guarantee each dependency must keep, and the order of the size check, authentication and body parsing on which the contract's error precedence depends. The README covers these, the HTTP and gRPC error responses, why only the signature check and the channel handshake create an AuthenticatedIdentity, and how to build, test and pack the package. It links the contract sections instead of restating them. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
NuGet shows a package's README on its gallery page and in IDEs, and pack warns about a package without one. PackageReadmeFile names the README, and a packed None item places it at the root of the package. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The library now writes the response bodies, decides renewals, issues them for the proved identity, builds the channel push messages and re-reads the registry during the channel handshake, so a host that follows the README must call these rather than reimplement them. Each endpoint now has its own steps: the trust-anchor, enroll, renew and check-in responses come from ResponseBodies, a renewal goes through RenewalRules.Decide and the Issue overload that takes the renewing AuthenticatedIdentity, confirm reconnects the streams of the retired certificate, and the channel uses VerifyHelloAsync, treats a certificate retired during the handshake like any retired one, and pushes RenewRequested and ConfigUpdate. The advice to re-check revocation after the handshake is gone, since VerifyHelloAsync does it. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CONTRACT.md §7.3 steps 5 and 7 send RenewRequested and ConfigUpdate after every Welcome they apply to. §7.4 adds Reconnect and the closure 60 s later for a stream whose certificate was retired during the handshake, but exempts that stream from neither message, and the README skipped both for it. The channel steps now reconcile every authenticated stream, and handle a retired certificate in a separate step that also applies. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
NuGet's README renderer drops repository-relative links, and the contract and intent files are not in the package, so every contract and intent link in the packed README led nowhere. The link definitions now point at those files on the master branch on GitHub, with the same section anchors. The package is published from master, so the paths resolve there. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A revocation, a deletion and the certificate's notAfter close a stream still awaiting its Hello ahead of every failure of the handshake, the timeout included, so a stream whose identity was revoked while it waited must close with identity_revoked rather than signature_invalid. ChannelOpening.HelloTimedOutAsync replaces HelloTimedOut. Like VerifyHelloAsync, it ends the handshake at once, reads the clock and then the certificate registry once, and returns identity_unknown for a certificate no longer registered, identity_revoked for a revoked identity and certificate_expired after notAfter, before it falls back to signature_invalid. A registry answer for another certificate or identity throws. Both methods share the clock and registry reading and these closures. The docs now say that the certificate of a stream may be retired during the handshake by a renew as well as a confirm, and leave its treatment to the contract. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The Hello timeout is ChannelOpening.HelloTimedOutAsync, which re-reads the certificate registry like VerifyHelloAsync, so a deletion, a revocation or the certificate's expiry closes a stream still awaiting its Hello with that error rather than as a timeout. The channel steps call HelloTimedOutAsync, and the README says that both handshake methods read the registry again before any handshake failure. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Member
Author
Acceptance evidence at
|
A project root that holds every source file buries its .csproj among dozens of files and scatters each contract area through an alphabetical list. Each project root now shows its .csproj beside one folder per contract area: Errors, Protocol, Enrollment, Bodies, Certificates, Authentication and Channel, plus Support for the shared test helpers. The files move unchanged and keep their namespace, so the public API stays in the single Devolutions.AgentIdentity namespace and a host still needs one using directive. Issue: DGW-479 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Benoît Cortier (CBenoit)
added this pull request to stack #2126
October 20, 2026 14:01
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add
Devolutions.AgentIdentity, the .NET library that holds 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 and the root key, and supplies a certificate registry, a nonce store, a root signer, a clock and a random number generator.
The library implements:
keyidandnonceencodings;Content-Digest; and a nonce recorded only after the signature verifies;Hellofailures and stream deadlines;Only the signature check and the channel handshake create
AuthenticatedIdentity, the type through which code acts for an identity.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 test vector.
The package lives in
dotnet/AgentIdentity/with a README for host developers, is built and tested in the .NET library CI job, and is published bypublish-libraries.ymlnext toDevolutions.Agent.Channel, on which it depends.Issue: DGW-479
Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com