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