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();