From 98e192e1ba85fba09ef201cf9d55fbeb2c131b9e Mon Sep 17 00:00:00 2001 From: David Obando Date: Mon, 24 Aug 2026 12:15:41 -0700 Subject: [PATCH] Share signature verification across public-key and host-based auth paths Consolidate signature verification for public-key and host-based authentication requests in the C# and TypeScript servers into a single helper with an explicit contract: when verification does not succeed, the failure is reported and the caller returns immediately without raising the Authenticating event. This aligns both servers with the Go implementation, which already handled the two paths this way. Also correct host-based deserialization in the TypeScript PublicKeyRequestMessage, which read the public-key wire format regardless of method name, and populate PayloadWithoutSignature on the C# host-based read path so the verified transcript is correct if that path is enabled. Adds negative-path test coverage in both languages for authentication requests whose signature does not match the presented public key, across ECDSA P-256/P-384 and RSA SHA-256/SHA-512. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Authentication/PublicKeyRequestMessage.cs | 5 + src/cs/Ssh/Services/AuthenticationService.cs | 79 +++++-- src/ts/ssh/messages/authenticationMessages.ts | 13 ++ src/ts/ssh/services/authenticationService.ts | 80 +++++-- test/cs/Ssh.Test/SessionTests.cs | 206 ++++++++++++++++++ test/ts/ssh-test/sessionTests.ts | 195 +++++++++++++++++ 6 files changed, 536 insertions(+), 42 deletions(-) diff --git a/src/cs/Ssh/Messages/Authentication/PublicKeyRequestMessage.cs b/src/cs/Ssh/Messages/Authentication/PublicKeyRequestMessage.cs index 232b2af..003ab34 100644 --- a/src/cs/Ssh/Messages/Authentication/PublicKeyRequestMessage.cs +++ b/src/cs/Ssh/Messages/Authentication/PublicKeyRequestMessage.cs @@ -92,6 +92,10 @@ protected override void OnRead(ref SshDataReader reader) if (MethodName != AuthenticationMethods.PublicKey) { + // Host-based authentication requests are intentionally rejected here. The + // host-based branch below is therefore currently unreachable, but it is kept + // correct so that relaxing this check cannot silently break the signature + // verification performed by AuthenticationService. throw new ArgumentException($"Method name {MethodName} is not valid."); } @@ -102,6 +106,7 @@ protected override void OnRead(ref SshDataReader reader) ClientHostname = reader.ReadString(Encoding.ASCII); ClientUsername = reader.ReadString(Encoding.UTF8); Signature = reader.ReadBinary(); + PayloadWithoutSignature = RawBytes.Slice(0, reader.Position - Signature.Count - 4); } else { diff --git a/src/cs/Ssh/Services/AuthenticationService.cs b/src/cs/Ssh/Services/AuthenticationService.cs index db15562..e14db01 100644 --- a/src/cs/Ssh/Services/AuthenticationService.cs +++ b/src/cs/Ssh/Services/AuthenticationService.cs @@ -146,6 +146,14 @@ await HandleAuthenticationFailureAsync( SshAuthenticatingEventArgs args; if (message.MethodName == AuthenticationMethods.HostBased) { + // Verify the client host signature before allowing the application to authorize + // the request. (RFC 4252 Section 9) + if (!await VerifySignatureAsync(message, algorithm, publicKey, cancellation) + .ConfigureAwait(false)) + { + return; + } + args = new SshAuthenticatingEventArgs( SshAuthenticationType.ClientHostBased, username: message.Username, @@ -162,27 +170,12 @@ await HandleAuthenticationFailureAsync( } else { - // Verify that the signature matches the public key. - var signature = algorithm.ReadSignatureData(message.Signature); - - var sessionId = Session.SessionId; - if (sessionId == null) + // Verify that the signature matches the public key, proving that the client + // possesses the corresponding private key. + if (!await VerifySignatureAsync(message, algorithm, publicKey, cancellation) + .ConfigureAwait(false)) { - throw new InvalidOperationException("Session ID not initialized."); - } - - var writer = new SshDataWriter(); - writer.WriteBinary(sessionId); - writer.Write(message.PayloadWithoutSignature); - var signedData = writer.ToBuffer(); - - var verifier = algorithm.CreateVerifier(publicKey); - var verified = verifier.Verify(signedData, signature); - if (!verified) - { - await HandleAuthenticationFailureAsync( - "Client authentication failed due to invalid signature.", - cancellation).ConfigureAwait(false); + return; } args = new SshAuthenticatingEventArgs( @@ -196,6 +189,52 @@ await HandleAuthenticationFailureAsync( await HandleAuthenticatingAsync(args, cancellation).ConfigureAwait(false); } + /// + /// Verifies the signature on a public-key or host-based authentication request, proving + /// that the sender possesses the private key corresponding to the presented public key. + /// + /// True if the signature is valid. False if it is missing or invalid, in which + /// case an authentication failure has already been sent and the caller MUST NOT proceed + /// to raise the event. + private async Task VerifySignatureAsync( + PublicKeyRequestMessage message, + PublicKeyAlgorithm algorithm, + IKeyPair publicKey, + CancellationToken cancellation) + { + if (!message.HasSignature) + { + await HandleAuthenticationFailureAsync( + "Client authentication failed due to missing signature.", + cancellation).ConfigureAwait(false); + return false; + } + + var sessionId = Session.SessionId; + if (sessionId == null) + { + throw new InvalidOperationException("Session ID not initialized."); + } + + var signature = algorithm.ReadSignatureData(message.Signature); + + var writer = new SshDataWriter(); + writer.WriteBinary(sessionId); + writer.Write(message.PayloadWithoutSignature); + var signedData = writer.ToBuffer(); + + var verifier = algorithm.CreateVerifier(publicKey); + if (!verifier.Verify(signedData, signature)) + { + await HandleAuthenticationFailureAsync( + "Client authentication failed due to invalid signature.", + cancellation).ConfigureAwait(false); + return false; + } + + return true; + } + private async Task HandleMessageAsync( PasswordRequestMessage message, CancellationToken cancellation) { diff --git a/src/ts/ssh/messages/authenticationMessages.ts b/src/ts/ssh/messages/authenticationMessages.ts index e8acd5a..5679e4f 100644 --- a/src/ts/ssh/messages/authenticationMessages.ts +++ b/src/ts/ssh/messages/authenticationMessages.ts @@ -55,6 +55,19 @@ export class PublicKeyRequestMessage extends AuthenticationRequestMessage { protected onRead(reader: SshDataReader): void { super.onRead(reader); + if (this.methodName === AuthenticationMethod.hostBased) { + this.keyAlgorithmName = reader.readString('ascii'); + this.publicKey = reader.readBinary(); + this.clientHostname = reader.readString('ascii'); + this.clientUsername = reader.readString('ascii'); + this.signature = reader.readBinary(); + this.payloadWithoutSignature = this.rawBytes!.slice( + 0, + this.rawBytes!.length - this.signature.length - 4, + ); + return; + } + const hasSignature = reader.readBoolean(); this.keyAlgorithmName = reader.readString('ascii'); this.publicKey = reader.readBinary(); diff --git a/src/ts/ssh/services/authenticationService.ts b/src/ts/ssh/services/authenticationService.ts index aef50b5..f911a9b 100644 --- a/src/ts/ssh/services/authenticationService.ts +++ b/src/ts/ssh/services/authenticationService.ts @@ -159,6 +159,12 @@ export class AuthenticationService extends SshService { let args: SshAuthenticatingEventArgs; if (message.methodName === AuthenticationMethod.hostBased) { + // Verify the client host signature before allowing the application to authorize + // the request. (RFC 4252 Section 9) + if (!(await this.verifySignature(message, publicKeyAlg, publicKey, cancellation))) { + return; + } + args = new SshAuthenticatingEventArgs(SshAuthenticationType.clientHostBased, { username: message.username ?? '', publicKey: publicKey, @@ -171,28 +177,10 @@ export class AuthenticationService extends SshService { publicKey: publicKey, }); } else { - // Verify that the signature matches the public key. - const signature = publicKeyAlg.readSignatureData(message.signature!); - - const sessionId = this.session.sessionId; - if (sessionId == null) { - throw new Error('Session ID not initialized.'); - } - - const writer = new SshDataWriter( - Buffer.alloc(sessionId.length + message.payloadWithoutSignature!.length + 20), - ); - writer.writeBinary(sessionId); - writer.write(message.payloadWithoutSignature!); - - const signedData = writer.toBuffer(); - const verifier = publicKeyAlg.createVerifier(publicKey); - const verified = await verifier.verify(signedData, signature); - if (!verified) { - await this.handleAuthenticationFailure( - 'Public key authentication failed: invalid signature.', - cancellation, - ); + // Verify that the signature matches the public key, proving that the client + // possesses the corresponding private key. + if (!(await this.verifySignature(message, publicKeyAlg, publicKey, cancellation))) { + return; } args = new SshAuthenticatingEventArgs(SshAuthenticationType.clientPublicKey, { @@ -206,6 +194,54 @@ export class AuthenticationService extends SshService { await this.handleAuthenticating(args, cancellation); } + /** + * Verifies the signature on a public-key or host-based authentication request, proving + * that the sender possesses the private key corresponding to the presented public key. + * + * @returns True if the signature is valid. False if it is missing or invalid, in which case + * an authentication failure has already been sent and the caller MUST NOT proceed to raise + * the `Authenticating` event. + */ + private async verifySignature( + message: PublicKeyRequestMessage, + publicKeyAlg: PublicKeyAlgorithm, + publicKey: KeyPair, + cancellation?: CancellationToken, + ): Promise { + if (!message.hasSignature || !message.payloadWithoutSignature) { + await this.handleAuthenticationFailure( + 'Public key authentication failed: missing signature.', + cancellation, + ); + return false; + } + + const sessionId = this.session.sessionId; + if (sessionId == null) { + throw new Error('Session ID not initialized.'); + } + + const signature = publicKeyAlg.readSignatureData(message.signature!); + + const writer = new SshDataWriter( + Buffer.alloc(sessionId.length + message.payloadWithoutSignature.length + 20), + ); + writer.writeBinary(sessionId); + writer.write(message.payloadWithoutSignature); + + const signedData = writer.toBuffer(); + const verifier = publicKeyAlg.createVerifier(publicKey); + if (!(await verifier.verify(signedData, signature))) { + await this.handleAuthenticationFailure( + 'Public key authentication failed: invalid signature.', + cancellation, + ); + return false; + } + + return true; + } + private async handlePasswordRequestMessage( message: PasswordRequestMessage, cancellation?: CancellationToken, diff --git a/test/cs/Ssh.Test/SessionTests.cs b/test/cs/Ssh.Test/SessionTests.cs index 1288ef8..c8e3366 100644 --- a/test/cs/Ssh.Test/SessionTests.cs +++ b/test/cs/Ssh.Test/SessionTests.cs @@ -3,10 +3,12 @@ using System.Linq; using System.Reflection; using System.Security.Claims; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.DevTunnels.Ssh.Algorithms; using Microsoft.DevTunnels.Ssh.Events; +using Microsoft.DevTunnels.Ssh.IO; using Microsoft.DevTunnels.Ssh.Messages; using Xunit; @@ -374,6 +376,210 @@ public async Task AuthenticateClientWithPublicKeyFail() await this.clientSession.CloseAsync(SshDisconnectReason.NoMoreAuthMethodsAvailable); } + /// + /// Verifies that a public-key authentication request with a signature that does not match + /// the presented public key never reaches the application's Authenticating event handler, + /// and never results in an authenticated session. + /// + [Theory] + [InlineData(ECDsa.ECDsaSha2Nistp256)] + [InlineData(ECDsa.ECDsaSha2Nistp384)] + [InlineData(Rsa.RsaWithSha256, 2048)] + [InlineData(Rsa.RsaWithSha512, 2048)] + public async Task AuthenticateClientWithPublicKeyInvalidSignature( + string pkAlgorithmName, int? keySize = null) + { + var pkAlg = GetAlgorithmByName( + typeof(SshAlgorithms.PublicKey), pkAlgorithmName); + var presentedKey = pkAlg.GenerateKeyPair(keySize); + var signingKey = pkAlg.GenerateKeyPair(keySize); + + var authenticationTypes = new List(); + this.serverSession.Authenticating += (sender, e) => + { + authenticationTypes.Add(e.AuthenticationType); + + // Approve anything that reaches the application callback, so that the assertions + // below fail if the callback is reached for a request with an invalid signature. + e.AuthenticationTask = Task.FromResult(new ClaimsPrincipal()); + }; + + bool serverRaisedClientAuthenticated = false; + this.serverSession.ClientAuthenticated += (sender, e) => + { + serverRaisedClientAuthenticated = true; + }; + + this.clientSession.Authenticating += (sender, e) => + { + e.AuthenticationTask = Task.FromResult(new ClaimsPrincipal()); + }; + + await this.sessionPair.ConnectAsync(authenticate: false).WithTimeout(Timeout); + + // The request presents one public key, but the signature over the transcript that the + // server verifies is produced with a different key. + var request = new PublicKeyRequestMessage( + serviceName: "ssh-connection", + username: TestUsername, + pkAlg, + presentedKey) + { + Signature = CreateMismatchedSignature( + pkAlg, + signingKey, + this.clientSession.SessionId, + TestUsername, + presentedKey.GetPublicKeyBytes(pkAlg.Name)), + }; + + var bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance; + var sendMessageAsync = typeof(SshSession).GetMethod("SendMessageAsync", bindingFlags); + await (Task)sendMessageAsync.Invoke( + this.clientSession, + new object[] { new ServiceRequestMessage { ServiceName = "ssh-userauth" }, default(CancellationToken) }); + await (Task)sendMessageAsync.Invoke( + this.clientSession, new object[] { request, default(CancellationToken) }); + + // Allow ample time for the server to process the request before asserting that it + // did not authenticate the session. + for (int i = 0; i < 100 && this.serverSession.Principal == null; i++) + { + await Task.Delay(20); + } + + Assert.Null(this.serverSession.Principal); + Assert.False(serverRaisedClientAuthenticated); + Assert.DoesNotContain(SshAuthenticationType.ClientPublicKey, authenticationTypes); + Assert.Empty(authenticationTypes); + + await this.clientSession.CloseAsync(SshDisconnectReason.NoMoreAuthMethodsAvailable); + } + + /// + /// Verifies that a host-based authentication request with a signature that does not match + /// the presented host public key never results in an authenticated session. + /// (RFC 4252 Section 9) + /// + /// + /// Host-based authentication is opt-in, and the C# server currently rejects host-based + /// requests during message deserialization + /// (see .OnRead), so today this test passes by way + /// of that earlier rejection rather than by exercising signature verification. It is + /// retained as a fail-closed guard: if host-based deserialization is ever enabled, this + /// test will then exercise the signature check in AuthenticationService and fail if that + /// check is missing. + /// + [Fact] + public async Task AuthenticateClientHostBasedInvalidSignature() + { + var pkAlg = SshAlgorithms.PublicKey.ECDsaSha2Nistp384; + var presentedHostKey = pkAlg.GenerateKeyPair(); + var signingKey = pkAlg.GenerateKeyPair(); + + this.serverSession.Config.AuthenticationMethods.Add(AuthenticationMethods.HostBased); + + var authenticationTypes = new List(); + this.serverSession.Authenticating += (sender, e) => + { + authenticationTypes.Add(e.AuthenticationType); + e.AuthenticationTask = Task.FromResult(new ClaimsPrincipal()); + }; + + bool serverRaisedClientAuthenticated = false; + this.serverSession.ClientAuthenticated += (sender, e) => + { + serverRaisedClientAuthenticated = true; + }; + + this.clientSession.Authenticating += (sender, e) => + { + e.AuthenticationTask = Task.FromResult(new ClaimsPrincipal()); + }; + + await this.sessionPair.ConnectAsync(authenticate: false).WithTimeout(Timeout); + + const string clientHostname = "client-host.example.com"; + var presentedPublicKey = presentedHostKey.GetPublicKeyBytes(pkAlg.Name); + + var writer = new SshDataWriter(); + writer.WriteBinary(this.clientSession.SessionId); + writer.Write(AuthenticationRequestMessage.MessageNumber); + writer.Write(TestUsername, Encoding.UTF8); + writer.Write("ssh-connection", Encoding.ASCII); + writer.Write(AuthenticationMethods.HostBased, Encoding.ASCII); + writer.Write(pkAlg.Name, Encoding.ASCII); + writer.WriteBinary(presentedPublicKey); + writer.Write(clientHostname, Encoding.ASCII); + writer.Write(TestUsername, Encoding.UTF8); + + // Sign the transcript with a key that does not match the presented host public key. + var signer = pkAlg.CreateSigner(signingKey); + var rawSignature = new Buffer(signer.DigestLength); + signer.Sign(writer.ToBuffer(), rawSignature); + + var request = new PublicKeyRequestMessage( + serviceName: "ssh-connection", + username: TestUsername, + pkAlg, + presentedHostKey, + signature: pkAlg.CreateSignatureData(rawSignature), + clientHostname: clientHostname, + clientUsername: TestUsername); + + var bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance; + var sendMessageAsync = typeof(SshSession).GetMethod("SendMessageAsync", bindingFlags); + try + { + await (Task)sendMessageAsync.Invoke( + this.clientSession, + new object[] { new ServiceRequestMessage { ServiceName = "ssh-userauth" }, default(CancellationToken) }); + await (Task)sendMessageAsync.Invoke( + this.clientSession, new object[] { request, default(CancellationToken) }); + } + catch (Exception) + { + // The session may already be torn down; the assertions below still apply. + } + + for (int i = 0; i < 100 && this.serverSession.Principal == null; i++) + { + await Task.Delay(20); + } + + Assert.Null(this.serverSession.Principal); + Assert.False(serverRaisedClientAuthenticated); + Assert.DoesNotContain(SshAuthenticationType.ClientHostBased, authenticationTypes); + } + + /// + /// Signs the public-key authentication transcript + /// (sessionId || type || username || service || method || true || algorithm || publicKey) + /// using a key that does NOT match the public key included in the transcript. + /// + private static Buffer CreateMismatchedSignature( + PublicKeyAlgorithm algorithm, + IKeyPair signingKey, + byte[] sessionId, + string username, + Buffer claimedPublicKey) + { + var writer = new SshDataWriter(); + writer.WriteBinary(sessionId); + writer.Write(AuthenticationRequestMessage.MessageNumber); + writer.Write(username, Encoding.UTF8); + writer.Write("ssh-connection", Encoding.ASCII); + writer.Write(AuthenticationMethods.PublicKey, Encoding.ASCII); + writer.Write(true); + writer.Write(algorithm.Name, Encoding.ASCII); + writer.WriteBinary(claimedPublicKey); + + var signer = algorithm.CreateSigner(signingKey); + var signature = new Buffer(signer.DigestLength); + signer.Sign(writer.ToBuffer(), signature); + return algorithm.CreateSignatureData(signature); + } + [Fact] public async Task AuthenticateCallbackException() { diff --git a/test/ts/ssh-test/sessionTests.ts b/test/ts/ssh-test/sessionTests.ts index fd71727..bd9f524 100644 --- a/test/ts/ssh-test/sessionTests.ts +++ b/test/ts/ssh-test/sessionTests.ts @@ -3,6 +3,7 @@ // import * as assert from 'assert'; +import { Buffer } from 'buffer'; import { suite, test, slow, timeout, pending, params } from '@testdeck/mocha'; import { @@ -28,6 +29,9 @@ import { PublicKeyRequestMessage, ServiceRequestMessage, SshTraceEventIds, + SshDataWriter, + AuthenticationMethod, + PublicKeyAlgorithm, } from '@microsoft/dev-tunnels-ssh'; import { DuplexStream, shutdownWebSocketServer } from './duplexStream'; import { createSessionPair, connectSessionPair } from './sessionPair'; @@ -449,6 +453,197 @@ export class SessionTests { assert.equal(authenticationType, SshAuthenticationType.clientPassword); } + /** + * Verifies that a public-key authentication request with a signature that does not match + * the presented public key never reaches the application's `Authenticating` event handler, + * and never results in an authenticated session. + */ + @test + @params({ pkAlg: 'ecdsa-sha2-nistp256' }) + @params({ pkAlg: 'ecdsa-sha2-nistp384' }) + @params({ pkAlg: 'rsa-sha2-256', keySize: 2048 }) + @params({ pkAlg: 'rsa-sha2-512', keySize: 2048 }) + @params.naming((p) => `authenticateClientWithPublicKeyInvalidSignature(${p.pkAlg})`) + public async authenticateClientWithPublicKeyInvalidSignature({ + pkAlg, + keySize, + }: { + pkAlg: string; + keySize?: number; + }) { + const alg = Object.values(SshAlgorithms.publicKey).find((a) => a?.name === pkAlg)!; + const presentedKey = await alg.generateKeyPair(keySize); + const signingKey = await alg.generateKeyPair(keySize); + + const [clientSession, serverSession] = await this.createSessions(); + + const authenticationTypes: SshAuthenticationType[] = []; + serverSession.onAuthenticating((e) => { + authenticationTypes.push(e.authenticationType); + + // Approve anything that reaches the application callback, so that the assertions + // below fail if the callback is reached for a request with an invalid signature. + e.authenticationPromise = Promise.resolve({}); + }); + + let serverRaisedClientAuthenticated = false; + serverSession.onClientAuthenticated(() => { + serverRaisedClientAuthenticated = true; + }); + + clientSession.onAuthenticating((e) => { + e.authenticationPromise = Promise.resolve({}); + }); + + await connectSessionPair(clientSession, serverSession, undefined, false); + + const presentedPublicKey = (await presentedKey.getPublicKeyBytes(alg.name))!; + + // The request presents one public key, but the signature over the transcript that the + // server verifies is produced with a different key. + const request = new PublicKeyRequestMessage(); + request.serviceName = 'ssh-connection'; + request.username = SessionTests.testUsername; + request.keyAlgorithmName = alg.name; + request.publicKey = presentedPublicKey; + request.signature = await SessionTests.createMismatchedSignature( + alg, + signingKey, + clientSession.sessionId!, + SessionTests.testUsername, + presentedPublicKey, + ); + + const serviceRequestMessage = new ServiceRequestMessage(); + serviceRequestMessage.serviceName = 'ssh-userauth'; + await clientSession.sendMessage(serviceRequestMessage); + await clientSession.sendMessage(request); + + // Allow ample time for the server to process the request before asserting that it + // did not authenticate the session. + for (let i = 0; i < 100 && !serverSession.principal; i++) { + await new Promise((resolve) => setTimeout(resolve, 20)); + } + + assert(!serverSession.principal, 'Server must not assign a principal.'); + assert(!serverRaisedClientAuthenticated, 'Server must not raise ClientAuthenticated.'); + assert( + !authenticationTypes.includes(SshAuthenticationType.clientPublicKey), + 'Server must not raise the clientPublicKey authentication event.', + ); + assert.strictEqual(authenticationTypes.length, 0); + } + + /** + * Verifies that a host-based authentication request with a signature that does not match + * the presented host public key never reaches the application's `Authenticating` event + * handler, and never results in an authenticated session. (RFC 4252 Section 9) + */ + @test + public async authenticateClientHostBasedInvalidSignature() { + const alg = SshAlgorithms.publicKey.ecdsaSha2Nistp384!; + const presentedHostKey = await alg.generateKeyPair(); + const signingKey = await alg.generateKeyPair(); + + const [clientSession, serverSession] = await this.createSessions(); + + // Host-based authentication is opt-in; enable it on the server for this test. + serverSession.config.authenticationMethods.push(AuthenticationMethod.hostBased); + + const authenticationTypes: SshAuthenticationType[] = []; + serverSession.onAuthenticating((e) => { + authenticationTypes.push(e.authenticationType); + e.authenticationPromise = Promise.resolve({}); + }); + + let serverRaisedClientAuthenticated = false; + serverSession.onClientAuthenticated(() => { + serverRaisedClientAuthenticated = true; + }); + + clientSession.onAuthenticating((e) => { + e.authenticationPromise = Promise.resolve({}); + }); + + await connectSessionPair(clientSession, serverSession, undefined, false); + + const presentedPublicKey = (await presentedHostKey.getPublicKeyBytes(alg.name))!; + const clientHostname = 'client-host.example.com'; + const clientUsername = SessionTests.testUsername; + + const sessionId = clientSession.sessionId!; + const writer = new SshDataWriter( + Buffer.alloc(sessionId.length + presentedPublicKey.length + 256), + ); + writer.writeBinary(sessionId); + writer.writeByte(50); // SSH_MSG_USERAUTH_REQUEST + writer.writeString(clientUsername, 'utf8'); + writer.writeString('ssh-connection', 'ascii'); + writer.writeString(AuthenticationMethod.hostBased, 'ascii'); + writer.writeString(alg.name, 'ascii'); + writer.writeBinary(presentedPublicKey); + writer.writeString(clientHostname, 'ascii'); + writer.writeString(clientUsername, 'ascii'); + + // Sign the transcript with a key that does not match the presented host public key. + const signer = alg.createSigner(signingKey); + const rawSignature = await signer.sign(writer.toBuffer()); + + const request = new PublicKeyRequestMessage(); + request.serviceName = 'ssh-connection'; + request.username = clientUsername; + request.methodName = AuthenticationMethod.hostBased; + request.keyAlgorithmName = alg.name; + request.publicKey = presentedPublicKey; + request.clientHostname = clientHostname; + request.clientUsername = clientUsername; + request.signature = alg.createSignatureData(rawSignature); + + const serviceRequestMessage = new ServiceRequestMessage(); + serviceRequestMessage.serviceName = 'ssh-userauth'; + await clientSession.sendMessage(serviceRequestMessage); + await clientSession.sendMessage(request); + + for (let i = 0; i < 100 && !serverSession.principal; i++) { + await new Promise((resolve) => setTimeout(resolve, 20)); + } + + assert(!serverSession.principal, 'Server must not assign a principal.'); + assert(!serverRaisedClientAuthenticated, 'Server must not raise ClientAuthenticated.'); + assert( + !authenticationTypes.includes(SshAuthenticationType.clientHostBased), + 'Server must not raise the clientHostBased authentication event.', + ); + assert.strictEqual(authenticationTypes.length, 0); + } + + /** + * Signs the public-key authentication transcript + * (sessionId || type || username || service || method || true || algorithm || publicKey) + * using a key that does NOT match the public key included in the transcript. + */ + private static async createMismatchedSignature( + alg: PublicKeyAlgorithm, + signingKey: KeyPair, + sessionId: Buffer, + username: string, + claimedPublicKey: Buffer, + ): Promise { + const writer = new SshDataWriter(Buffer.alloc(sessionId.length + claimedPublicKey.length + 128)); + writer.writeBinary(sessionId); + writer.writeByte(50); // SSH_MSG_USERAUTH_REQUEST + writer.writeString(username, 'utf8'); + writer.writeString('ssh-connection', 'ascii'); + writer.writeString(AuthenticationMethod.publicKey, 'ascii'); + writer.writeBoolean(true); + writer.writeString(alg.name, 'ascii'); + writer.writeBinary(claimedPublicKey); + + const signer = alg.createSigner(signingKey); + const signature = await signer.sign(writer.toBuffer()); + return alg.createSignatureData(signature); + } + @test public async authenticateCallbackError() { const [clientSession, serverSession] = await this.createSessions();