Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/cs/Ssh/Messages/Authentication/PublicKeyRequestMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}

Expand All @@ -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
{
Expand Down
79 changes: 59 additions & 20 deletions src/cs/Ssh/Services/AuthenticationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -196,6 +189,52 @@ await HandleAuthenticationFailureAsync(
await HandleAuthenticatingAsync(args, cancellation).ConfigureAwait(false);
}

/// <summary>
/// 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.
/// </summary>
/// <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 <see cref="SshSession.Authenticating" /> event.</returns>
private async Task<bool> 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)
{
Expand Down
13 changes: 13 additions & 0 deletions src/ts/ssh/messages/authenticationMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
80 changes: 58 additions & 22 deletions src/ts/ssh/services/authenticationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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, {
Expand All @@ -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<boolean> {
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,
Expand Down
Loading
Loading