Repository navigation
Conversation
The format half of VVK-signed host certificates, which removes the trust-on-first-use prompt entirely because the client already trusts the quorum — no known_hosts to distribute, nothing for an operator to compare. Two checks are required and neither is sufficient alone. The key exchange signature proves the server holds the private key for the key inside the certificate, and the SSH library already does that. The CA signature proves the VVK vouched for that key, and nothing in the library does it — so without this, any self-signed certificate would be accepted. Verification covers the certificate type (a user certificate presented as a host key is refused), the issuing authority, the validity window with validBefore exclusive as OpenSSH treats it, principals, and the signature over the certificate's own signed bytes. Every read is bounds-checked. This parses bytes supplied by whatever is at the other end of the connection, so a truncated field or an overrunning length prefix has to be an error rather than a short read. Verification uses WebCrypto's Ed25519, which is available in the browser and in Node, so no dependency is added. Note the existing Ed25519PublicKeyAlgorithm still throws from createVerifier — wiring certificates into the handshake needs that implemented, and it is not done here.
Completes the client half. Registering a certificate algorithm makes the client advertise ssh-ed25519-cert-v01@openssh.com, which is what causes sshd to send its certificate as the host key; the library then verifies the exchange signature against the key inside it, and the CA signature is checked here. Both are required. The exchange signature alone proves only that the server holds the key its certificate names, which any self-signed certificate would satisfy. Nothing in the SSH library checks who signed the certificate. Implements Ed25519 verification, which previously threw. That gap is why ed25519 host keys were pushed to the end of the algorithm list and effectively avoided; certificates need it for both signatures. Overrides readSignatureData: OpenSSH signs the exchange with the host's own key, so the blob is named ssh-ed25519 rather than after the certificate, and the inherited implementation expects the negotiated name. When an authority is configured the client advertises only the certificate algorithm rather than preferring it, so a server cannot offer a plain host key and pull the connection back to first-use trust. Policy is stricter than OpenSSH in two places, both deliberate: a certificate naming no principals is refused rather than treated as valid for every host, and any critical option is refused rather than ignored.
Issuance is split so nothing here needs a private key: the caller gets the exact bytes the authority must sign, takes them to the quorum, and returns with a signature to assemble. That is what lets the VVK certify a host without any key ever existing outside the enclave. Refuses at issue time what the client would refuse at connection time — a certificate naming no principals, an empty validity window, keys that are not ed25519 sized — so a bad certificate cannot be signed and then fail in the field. Tests issue a certificate, sign it with a real CA key, and run the result through the same parser and verifier a handshake uses, including a serial past 2^53 to prove uint64 stays exact.
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.
UV's step 5, complete, plus the format work. Stacked on #58 — review that first.
Why certificates beat the prompt
Trust-on-first-use (#58) still asks an operator to compare a fingerprint, and people click through prompts. With VVK-signed certificates there is no prompt: the client already trusts the quorum, so there is nothing to distribute and nothing to compare.
Two signatures, both required
verifyCertificateThe first alone is satisfied by any self-signed certificate — and the handshake would succeed and look correct. Nothing in dev-tunnels-ssh checks the second. That is the failure mode this design invites, so it is worth stating in review.
Feasibility, read rather than assumed
getPublicKeyAlgorithms()returns the configured list unfiltered for client sessions, so a custom algorithm is advertised in KEX and sshd sends the certificate.handleDhReplyMessagedoessetPublicKeyBytes(message.hostKey)— the whole certificate blob arrives at our KeyPair.Points UV raised, and what I did
readSignatureDatamust be overridden. Correct, and it was already a bug in waiting: OpenSSH signs the exchange with the host's own key, so the blob is namedssh-ed25519, not the negotiated certificate name. The inherited implementation expects the negotiated name. Overridden, and it now checks both the name and the 64-byte length rather than skipping past them.getBigUint64throughout, with a test that a serial survives exactly.createVerifierpreviously threw ("not implemented by this client"), which is why ed25519 host keys were pushed to the end of the list and avoided. Implemented with WebCrypto, so no dependency is added.Server side
docs/SSH-HOST-CERTIFICATES.mdcovers issuing and installing. The one thing that catches people: placing the file is not enough, sshd does not load host certificates withoutHostCertificateinsshd_config.Testing
364 pass, 20 new. Certificates are built in real wire format and signed with real Ed25519 keys, so the parser meets bytes an sshd would send rather than a fixture shaped to fit it.
The adversarial cases are the substance: signed by a different authority; claiming our CA but signed by an impostor; edited after signing; a user certificate presented as a host key; not-yet-valid, expired, and the exact
validBeforeboundary; wrong host; empty principals; critical options; revoked serial; and a length prefix that overruns the buffer.Not tested: the handshake integration. The algorithm registration,
readSignatureDataand theonAuthenticatingbranch need a live sshd presenting a real certificate. That is the acceptance test — issue one, install it, connect, then confirm a certificate from a different CA is refused.Still open — issuance
Steps 1-3 of UV's plan are not built. The signing pattern exists (
BasicCustomRequestwith an arbitrary draft, astideSsh.tsuses for SSH auth), but which flow certifies a host key is a decision, not a guess: quorum approval implies the explicit multi-admin path rather than the implicitPolicy:1flow used for user auth.Also worth confirming: the CA the client trusts is the realm VVK from
authConfig.jwk, the same key that already signs user SSH auth. One trust anchor in both directions — nice, but I would rather you confirmed it than assumed it.