Skip to content

feat(agent): device identity enrollment and channel - #2000

Draft
Benoît Cortier (CBenoit) wants to merge 215 commits into
masterfrom
cbenoit-agent-identity-v1
Draft

Benoît Cortier (CBenoit) wants to merge 215 commits into
masterfrom
cbenoit-agent-identity-v1

Conversation

@CBenoit

@CBenoit Benoît Cortier (CBenoit) commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Devolutions Agent can obtain its own device identity from a management server such as Devolutions Server: a server-issued device ID and an X.509 certificate whose private key never leaves the machine.
One reusable enrollment token, deployable through msiexec or Microsoft Intune, enrolls any number of machines.
After enrolling, the agent renews its certificate on its own and keeps an authenticated, bidirectional gRPC channel to the server, so the server can reach the agent (for example, to ask it to renew).

This draft contains the product-neutral contract and its conformance suite; the agent implementation follows in the same PR:

  • docs/agent-identity/CONTRACT.md: the agent-facing HTTP API, the RFC 9421 request-signature profile, the gRPC channel and its proof-of-possession handshake, and the agent's local files and configuration.
  • docs/agent-identity/test-vectors.json: shared signature, channel-proof and CSR vectors for every implementation.
  • agent-identity-channel-proto: the channel schema for Rust, also published as the Devolutions.AgentIdentity.Channel NuGet package for server implementers.
  • agent-identity-mock and agent-identity-conformance: a mock server and a conformance tester that checks a server (the mock, or a real DVLS) and the agent against the contract; the testsuite runs both.

@CBenoit

Copy link
Copy Markdown
Member Author

Implementation notes (Phase 1: contract and conformance suite)

Contents

  • docs/agent-identity/CONTRACT.md v0.3 (approved), including the §10.1 clarification that extra_trusted_root is a PEM bundle.
  • docs/agent-identity/test-vectors.json, produced by a throwaway generator that builds the signature bases by hand and signs deterministically (RFC 6979).
    It was cross-checked with Python cryptography and includes RFC 9421 Appendix B.2.4.
  • agent-identity-channel-proto: tonic/prost code generated with vendored protoc, passed through prost_build::Config::protoc_executable rather than set_var.
    The same .proto builds the Devolutions.AgentIdentity.Channel NuGet package (net10.0, Google.Protobuf + Grpc.Core.Api).
    CI builds the package and publish-libraries.yml publishes it; the new package ID may need nuget.org trusted-publishing setup.
  • agent-identity-mock: the conformance oracle.
    It verifies the RFC 9421 profile by hand with p256, without an RFC 9421 library, applying the §6 checks in contract order and committing each nonce only after its signature verifies.
    Authenticated handlers take an AuthenticatedDevice that only the verifier can construct.
    A unit test runs every entry of test-vectors.json through the server's own verifier.
  • agent-identity-conformance: a custom harness that exits non-zero on failure.
    • Protocol tests act as a raw client, with their own signer written independently of the mock.
    • Agent tests spawn devolutions-agent run with DAGENT_CONFIG_PATH and stand in for the MSI through the pending file (Unix 0600; Windows DPAPI machine scope with a protected DACL).
    • Observation order: admin API first, agent files second, logs last.
    • Mock-only tests are skipped with --target dvls.
    • Rotation tests against DVLS need --disposable-dvls-target.
  • The testsuite starts two mocks under /mock and runs the tester against them.

Current results (Windows, local)

  • 47/47 protocol tests pass against the mock (about 15 s).
  • Expected to fail until the agent implementation lands: the 22 a_* tests below, which need devolutions-agent identity support.
    a_file_backend_permissions is Unix-only and skips on Windows.
    Until then, the testsuite test agent_identity::conformance is red in CI; fmt, clippy, the existing tests and the protocol tests are green.
    • a_pending_file_enroll_success, a_pending_file_deleted_on_permanent_error, a_pending_file_kept_on_transient_error, a_token_never_logged
    • a_same_token_no_enrollment, a_same_token_rejected_identity_no_enrollment, a_different_token_replaces_identity, a_cli_identity_enroll_writes_pending_file
    • a_renewal_happy_path, a_renewal_lost_response_retried, a_channel_connected_and_metadata, a_make_before_break_on_renewal
    • a_request_renewal_connected, a_request_renewal_while_offline, a_reconnect_make_before_break, a_revocation_stops_agent
    • a_device_unknown_recorded, a_no_channel_when_absent, a_multi_authority, a_key_non_exportable
    • a_rotation_migrates_connected_agent, a_rotation_migrates_on_schedule

Dependencies

  • The workspace resolves p256 0.14.0-rc.14 and ecdsa 0.17.0-rc.22, because ironrdp-connector 0.10 → sspi 0.21 → picky =7.0.0-rc.25 pins those release candidates.
    The mock and tester therefore pin p256 =0.14.0-rc.14.
  • The agent's signing layer, in the next phase, holds httpsig at =0.0.24 for the same reason.
    httpsig is used only to build and sign the agent's own requests, never to parse incoming headers, because 0.0.24 can panic on malformed Signature-Input.
  • Follow-up: once IronRDP releases on sspi 0.22 (picky rc.26), bump the picky pins to rc.26 and httpsig to 0.0.26, and drop the duplicate p256/ecdsa generation.

Suite decisions

  • Enroll idempotence is checked before token expiry and exhaustion, so a lost response on a maxUses = 1 token can be retried.
  • Chain checks:
    • The last chain element must equal a published root byte for byte.
    • Links are checked by name, and their signatures are verified for P-256 issuers.
    • Validity is checked at evaluation time.
    • There's no full RFC 5280 path validation (§4, invariant 10).
  • Windows test cleanup deletes a machine key only when the key didn't exist before the test, its name matches the contract pattern, and its authority is the server-issued authority_id.

Review
Each suite task went through a coder ↔ reviewer loop using the repository code-review skill.
The tester task ended after 3 rounds (19 → 14 → 8 findings); a follow-up fix task addressed the remaining 8 and was approved in round 2.

Note

LLM-assisted content (no human feedback).

Devolutions Agent gains an identity per authority (e.g.: DVLS, PSU):
the device enrolls once from a reusable token, the authority mints an
identity and issues an X.509 certificate, and the agent authenticates
later requests and a bidirectional gRPC channel with that identity.

- `docs/agent-identity/CONTRACT.md` defines what the agent and an
  authority agree on, with shared test vectors and an independent
  vector verifier; the design rationale lives in the shared intent
  `INTENT.agent-identity.md`, symlinked into each component.
- `device-key` keeps device keys in the Windows software provider or in
  files, under an access rule granting only the caller and SYSTEM.
- `agent-identity` and `agent-identity-httpsig` hold pending enrollment,
  stored identities, the REST client and RFC 9421 request signing;
  `agent-channel-proto` defines the channel for Rust and .NET.
- `devolutions-agent identity enroll` writes a pending enrollment, and
  the service enrolls from it.
- `agent-identity-mock` and `agent-identity-conformance` form an
  independent conformance suite that the testsuite runs against the
  agent.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Lets the Windows and Linux test jobs both finish while the agent identity
conformance tests are expected to fail. To be reverted before the pull
request leaves draft status.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…outcome

From experiment 1 (RFC 9421, DPoP, and DPoP with authority-issued nonces,
built as probes against a shared proxy harness and judged blind), the
maintainer kept RFC 9421 and recorded the two decisions the evidence
settled, each with the alternative it excludes: request authentication
never covers the host or path, and the agent supplies each request's
freshness.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…d hit

Found by the blind judge of experiment 1 and approved by the maintainer:

- a verifier detects repeated parameters, members and labels on the raw
  field, since RFC 8941 parsers keep only the last occurrence;
- byte sequences in `Signature` and `Content-Digest` use canonical
  padded base64;
- a refused channel opening carries a `server-time` trailer, so the
  agent's single clock-skew retry works on the channel too.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The rule-to-test mapping, the review-only claims and the known gaps
describe the conformance suite, not something the agent and an authority
agree on, and they drift as tests are renamed. The contract keeps what
conformance means; the mapping moves to the suite's COVERAGE.md.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The contract's rules define conformance; the suite is independent
evidence derived from them, so defining conformance as passing the suite
inverted that. §6 still requires the shared vectors, and the suite's role
is stated in its own intent.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The vectors are evidence for the §6 rules, not a rule both parties agree
to, so their role, the signer's pass condition and the independent
verifier move to the shared intent's Conformance section. The vector
file no longer claims a signer reproduces signature bytes, which
contradicted that condition.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The contract no longer defines friendly names, so §5.2 drops its §3
reference, and the DVLS intent points at the shared intent's Enrollment
section, where the format is defined.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…adapter

The device-key file backend signs through RustCrypto's Signer and the httpsig adapter's tests implement it; Signer::sign panics on failure, so both crates keep calling try_sign.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A hard link keeps the regular-file type, owner and ACL of the key it aliases, so the reparse-point and symlink checks let it through.
The file backend now refuses to open a key file whose link count is not one.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The software provider persists a key only at finalization, so two creators of one name could both succeed and the later one replaced the earlier key.
Creation now takes a global named mutex for the key name, checks that the key does not exist, and holds the lock through finalization.
The wait is bounded at five seconds, like the file backend's lock.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The DVLS intent keeps decisions and leaves exact routes, fields and codes
to DVLS's OpenAPI document. It adds the root's creation, custody,
expiry and failure rules, the channel offer as a hosting property,
renewal load shedding, the grace identity status, four management
permissions, and a suggested 90-day token expiry with an expiry warning.

The suite reaches each authority's management API through a module per
authority, and N/A covers tests that module declares unsupported. The
mock replicates DVLS's management API as it is. The channel client backs
off up to 1 h when the path cannot carry gRPC. Only the caller's account
and SYSTEM can block a Windows key's creation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
An independent verifier found points the profile left open. Step 1 now
names one member in each of Signature-Input and Signature, parameter
types, no parameters on the Signature member, a covered component or the
Content-Digest member, the keyid and nonce formats, and canonical
base64. A malformed keyid fails there, so an agent bug never draws a
terminal identity_unknown. Unexpired follows RFC 5280 (now <= notAfter),
and uncovered headers are ignored.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Each byte string has one canonical base64url encoding; a padded,
standard-alphabet or non-zero-pad-bit keyid or nonce fails step 1 like
non-canonical base64 does.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A future notBefore can only come from the authority's own clock, so
rejecting it would lock devices out over an authority-side fault.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Request bodies reject repeated JSON member names at any depth, every
base64 and base64url value is canonical, only the exact token string
authenticates, and a CSR key must be an uncompressed P-256 point so each
key has one SPKI SHA-256. A friendly name is trimmed again after
truncation, so it never ends in white space.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
During pending-key recovery the expired pending key sits in `previous`
and the fresh key in `pending`, so a restart resumes recovery. A runner
checks in when it starts, including after a new enrollment, and the
channel's 1 h timer restarts when the URL changes. A rejected identity
keeps its keys until replaced or uninstalled, an unusable pending key is
defined, and the agent never judges certificate validity itself: the
server's clock decides.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The contract gains a transient `unavailable` code for 5xx answers, which
overload shedding and an unusable root key need. DVLS's management API
becomes part of its public API and follows that API's contract, with
settings readable under View and changeable under Manage.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
When the authority's clock is ahead, notBefore lies in the agent's
future and a schedule anchored on it renews late by the skew. Anchoring
on the earlier of notBefore and the moment the agent stored or loaded
the certificate keeps renewal at two thirds of the authority's lifetime
without storing anything new.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The bag is unsigned routing data and the authority keeps only the
secret's hash, so an altered bag authenticates like the original and
no authority needs to store the full token.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The route spelling now follows DVLS's public API contract, so the intent
keeps only the naming decision: records are identities, and "device"
stays free for a later machine view.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
An empty extensionRequest attribute violates RFC 5280's SIZE (1..MAX),
so a strict PKCS#10 parser at an authority rejects the request. The
server still ignores any attribute it receives.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
RFC 2986 requires the attributes field, so "no attributes" means an
empty set encoded A0 00, not an omitted element.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A DVLS run skipped three checks for want of a differently configured
authority, flaked on agent tests whose 20-second leaves expired during
a slow step, and deleted the data directory of each failed agent test.

Each --authority adds an authority with its own profile (leaf
lifetime, channel, disposability), its secret read from a named
environment variable. Each check runs on the first authority that meets
its needs and reports SKIP with the missing capability only when none
does. On DVLS, the stale-Hello check switches the agent channel off and
on, the only config change the management API makes.

a_renewal_happy_path, a_request_renewal_connected and a_multi_authority
wait one more step for a renewal when the agent's 2/3-lifetime schedule
can renew during a wait, and accept a reconnect after an expiry that
closed the stream, or a renewal from the persisted key once it is due.

A failed or timed-out agent test keeps its data directory and the
agent's logs and prints KEPT with its path; the testsuite keeps its
directory when the runner fails.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The short-lifetime relaxations measured time from the token's creation,
which bounds a certificate's issuance only from below, so they could
drop the continuity and persisted-key checks while the relevant
certificate was still unexpired or not yet due.

a_request_renewal_connected and a_multi_authority now require an
unbroken channel until a certificate of the identity has expired by the
authority's clock, which the server_time of a refused unsigned confirm
reports. a_renewal_happy_path accepts a later certificate only when the
authority issued it once the persisted certificate was due, by that
certificate's own validity. The token-based waits remain a budget only.

The settings-lifetime capability now compares each disposable DVLS
target's declared leaf lifetime with its certificate lifetime setting,
read at the start, instead of testing for whole days. The README states
that usage errors never echo values, which the usage-error tests cite.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The continuity checks of a_request_renewal_connected and
a_multi_authority relaxed once any certificate in the identity's history
had expired, by a clock read after the record, so an expired retired
leaf could excuse a break on a newer one.

They now accept a closed channel only when the certificate that was
current when the channel was last seen connected had expired by the
authority's clock, read right before the record that found it closed.

a_renewal_happy_path no longer assumes that notBefore is the issuance
time: it accepts a later certificate only when the runner saw it no
earlier than the persisted certificate was due, anchored at the earlier
of its notBefore and the moment the restarted agent loaded it.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
a_renewal_happy_path, a_request_renewal_connected and a_multi_authority
excused a closed channel or a later certificate by inferring that a
certificate had expired or come due. The management record does not say
which certificate a live stream uses, and a clock sample taken before a
read cannot rule out an expiry before it, so those inferences could
excuse a real break.

The three checks again require make-before-break, no gap on a requested
renewal, and a reconnect with the persisted certificate. Instead, each
needs leaves of at least 3/2 of a test's 90-second timeout, so that the
agent's schedule, which renews two thirds into a leaf's lifetime, renews
none of their certificates and none expires before the test ends. An
authority with shorter leaves reports SKIP naming that need; the mock's
default 90-day leaves meet it. Their renewal-aware waits remain a budget
only.

Markers for the harness rules that the suite's intent now states cite
its Shape section instead of the README.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…iptions

An --authority description names the environment variable holding its
secret instead of carrying it, so it never holds a credential.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The file-name-limit test measured names it built by copying the backend's naming scheme, so its length checks ran no backend code.
Every fault its name parsing caught is also caught by the leftover-removal test.

The leftover-removal test's final `generate` succeeded only because the `list` before it had already removed the leftover, and no fault failed it alone.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Issue: DGW-478
The vector key test now checks that the key file opens to the vector's public key and that its signature verifies under that key.
The shared intent's conformance rule asks no more of a signer, and the device-key intent lets a backend return different signatures for the same message, so the exact vector signature pinned a deterministic nonce that nothing requires.

The Unix round trip checks only that no group or other permission bit is set, which is what the access rule decides.

The invalid-name test adds a name that would be a valid file name, so `generate` must refuse it through the name check rather than fail later on a path that does not exist.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Issue: DGW-478
No document says that an operation refusing an invalid name leaves the key directory untouched, and since the test now refuses a name that would be a valid file name, the name check fails before this assertion for every fault it caught.
The test keeps its refusal checks under a name that says what they check.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Issue: DGW-478
…ched

Each test fails on a fault that every existing test let through.

- Listing finds a key whose part contains `_`, because the prefix ends at the first `_`.
- A key directory that does not exist holds no keys: opening reports the key missing, deleting succeeds and listing is empty.
- A temporary written while its creator holds the directory lock survives another operation, which waits for the lock before it removes leftovers.
- An operation contending for a lock file that an earlier operation left behind waits and fails once the limit runs out, as it does when the holder created the file.
- On Windows, a key file that another process holds exclusively is not reported missing.
- On Windows, a key file whose DACL also lets Users read it cannot be opened.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Issue: DGW-478
Most tests rest on the device-key intent: key-name rules, operations, the access rule, signing and backends.
The Windows unit tests also cite the specifications that define the values they check: FileFsAttributeInformation for the ACL volume flag, the SDDL syntax for the key descriptor, and the HRESULT values for provider errors.
Assertions that only catch faults no source decides, such as a key's reported name and the wording of a lock time-out, are marked as such.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Issue: DGW-478
Removing the file-name-limit test also removed the only check that cleanup recognizes the temporary and tombstone of the longest key name the intent allows.
A parser that refused long names would leave those private-key leftovers in place, and no other test failed on it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Issue: DGW-478
…spelling

The descriptor test compared SDDL text, but the SDDL syntax spells one descriptor in several ways, and the access rule lets either the caller or SYSTEM own a key.
It now parses the descriptor built for the process user, another user and SYSTEM, and checks the owner, the protected DACL and that a DACL with an extra principal is refused.
The machine-key round trip likewise accepts the caller or SYSTEM as owner.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Issue: DGW-478
… lock tests

No document sets a minimum wait before a lock time-out, only that the wait ends and the operation fails.
Every fault these checks caught also fails the test that writes a temporary under the lock, so the lock tests now assert only that the contended operation fails.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Issue: DGW-478
Tests and assertions that only catch faults no document decides carry `mutant` markers naming those faults: the creation-lock minimum wait, key-directory creation and its mode, operations under an execute-only ancestor, symbolic links in listings, and the namespace retry.
The former-global-lock test rests on the intent's rule that only the caller's account and SYSTEM can block creation, because any account can create a mutex under that global name.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Issue: DGW-478
…p bounds

Owner-only key directories on Unix, traverse-only ancestors, no links in
listings, no removal of files the crate did not create, and no panic on
unexpected entries.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…nnel messages

The channel client now sends serialized frames as they are, so the suite can send an agent message that does not decode or that exceeds the 64 KiB envelope limit.
Each defect departs from a valid frame of exactly 65,536 bytes that the authority accepts, so a refusal comes from the defect alone.
Before Welcome, such a message closes the stream as signature_invalid without changing the identity; after Welcome, it closes the stream as invalid_request.
The client's decoder also refuses any server message over 65,536 bytes, so every channel test checks the authority's side of the limit.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The crate cannot tell its own files from look-alikes, so it removes only
files whose names follow its scheme for keys and their temporary and
deleted copies, whatever the prefix, and nothing else in the key store.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mark the symlink-listing, owner-only key directory and traverse-only
ancestor tests with the device-key intent statements that decide them,
and withhold every ancestor permission but traverse so the last test
checks exactly that statement.

Add tests that cleanup leaves entries that only resemble its
temporaries, and that stray names a length-based parse would overrun
cause no panic.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A version 4 UUID may contain no letter, so its uppercase form can equal
the name the crate writes and cleanup would rightly remove it.
Put a letter in the first digit, which encodes neither version nor
variant, before uppercasing.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Cleanup and delete may remove only names in the crate's scheme for keys
and their temporary and deleted copies, so check that notes, near-miss
key names and temporaries without a canonical UUID survive both list
and delete beside a real key and a leftover.
A directory under a temporary's name is left unchecked, because a name
in the scheme counts as the crate's own.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ries

Listing skips an unexpected entry; other operations skip it or fail
promptly, because a key store can be shared with other software.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The backoff unit test that called the internal reset directly restated
that method's own doc comment; the requirement it stood for, that an
authenticated stream restarts the backoff from 1 s, is checked at the
channel by the reopening test, which alone still catches a reset that
does nothing.

The shared channel-proof vector test also signed the vector's input
with the test key and verified it under the vector's public key, which
runs no code of this crate. The Hello proof the crate signs is verified
by the challenge test.

Issue: DGW-478
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Several checks passed on faults that the governing documents reject:

- a dropped channel that kept its side open until the 5 s close timeout
  reset the stream, which the queued-acknowledgement test accepted;
  it now requires the clean end of stream the intent's "ends the
  agent's side" means;
- an agent that gave up on a message arriving before the Challenge
  passed, since only messages after it were sent; the handshake test
  now sends the same unexpected messages before the Challenge too;
- an acknowledgement reusing the acknowledged message's ID passed; a
  UUID is unique (RFC 9562 section 1);
- reusing the authority's TLS settings, which offer HTTP/2, for an
  HTTPS proxy passed; the test proxy now offers HTTP/2 as well and
  refuses a client that selects it (RFC 7301 section 3.2).

The clock-skew retry's 5 s allowance becomes the whole seconds that
pass during the test, and the keepalive test's upper bound becomes the
pings that fit the measured window, since hyper sends each ping one
interval after the last frame it read.

Issue: DGW-478
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Each test closes a fault that every existing test let pass:

- the backoff exponent without its bound overflows after 64 closures;
  the backoff now stays at its cap for 100 closures (intent,
  Connection);
- TerminalCode::as_str returning another code; it now names each code
  as the contract's error table does;
- a challenge longer than 32 bytes was answered; the intent's
  Handshake rule covers every other length;
- a proxy URL with a user and no password sent no credentials, where
  the proxy parser of reqwest sends "user:" as Basic credentials;
- socks4 left the name to the proxy, where reqwest resolves it
  locally; the test SOCKS proxy now also speaks SOCKS4 and 4A;
- a dropped channel finished its wait to reopen, and waited forever
  for an authority that kept its side open; both now must release the
  channel's target, which the intent's Closing rule requires;
- an application/grpc-web answer was taken for gRPC, and an upper-case
  gRPC content type was refused (gRPC-Web protocol; RFC 9110 section
  8.3.1).

The close timeout's UNTESTED comment now names only the early release
when the authority ends its side, since the bound itself is tested.

Issue: DGW-478
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Every test, case and mixed assertion now carries the oracle marker of
the source that decides it: the crate's intent for the backoff,
handshake and closing rules, the contract for the channel sequence,
errors and termination, the shared vectors' pass rule, and the RFCs
and reference implementations the proxy and gRPC checks follow.

The prose citations these markers replace are reduced to the
explanation they gave, and the timing checks name why their bounds
hold.

Issue: DGW-478
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The contract's Service section lets each side send envelopes of up to
64 KiB of serialized protobuf. The client sets no decode limit, so
nothing failed when one was added below that size.

The test authority can now send a message the test encodes itself, and
a RenewRequested padded to exactly 65,536 bytes with a field the agent
does not know must still reach the caller. A decode limit of 65,535
bytes fails the test, and one of 65,536 bytes passes it.

Issue: DGW-478
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
With a 300 ms opening timeout, a loaded machine timed the opening out
before the client sent its Hello, so the test authority read a reset
stream and the test failed. Two seconds leaves the client time to reach
the wait for Welcome while the closure still comes well within the
10 s the test waits for it.

Issue: DGW-478
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The intent's Connection rule has the channel connect through the
agent's configured proxy, which an ftp proxy can't carry, so the
channel fails without connecting, neither through the proxy nor
around it, and the contract makes such an unnamed failure transient.
Of the crate's tests, only this one fails when the channel bypasses a
proxy it can't use.

Issue: DGW-478
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A configured proxy the identity requests cannot use is a transient error,
never a reason to connect directly.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A server message that cannot be decoded or whose id is not a UUID closes
the stream as a transient failure, so the agent never echoes an unchecked
id past the channel's size limit.

Issue: DGW-478

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant