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
64 changes: 15 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,55 +25,14 @@ that runs in reaction to state changes.

## Algorithm Support

### Authentication
- `keyboard-interactive`
- `password`
- `publickey`

### Host Keys
- `ssh-ed25519` ([RFC 8709](https://tools.ietf.org/html/rfc8709))
- `ssh-ed448` ([RFC 8709](https://tools.ietf.org/html/rfc8709))
- `ecdsa-sha2-nistp256` ([RFC 5656](https://tools.ietf.org/html/rfc5656#section-3))
- `ecdsa-sha2-nistp384` ([RFC 5656](https://tools.ietf.org/html/rfc5656#section-3))
- `ecdsa-sha2-nistp521` ([RFC 5656](https://tools.ietf.org/html/rfc5656#section-3))
- `rsa-sha2-512` ([RFC 8332](https://tools.ietf.org/html/rfc8332#section-3))
- `rsa-sha2-256` ([RFC 8332](https://tools.ietf.org/html/rfc8332#section-3))
- `ssh-rsa` ([RFC 4253](https://tools.ietf.org/html/rfc4253#section-8.1))

### Key Exchange
- `mlkem768x25519-sha256` ([draft-ietf-sshm-mlkem-hybrid-kex](https://datatracker.ietf.org/doc/draft-ietf-sshm-mlkem-hybrid-kex/) (depends on JEP-496 support)
- `curve25519-sha256` ([RFC 8731](https://tools.ietf.org/html/rfc8731))
- `ecdh-sha2-nistp521` ([RFC 5656](https://tools.ietf.org/html/rfc5656#section-4))
- `ecdh-sha2-nistp384` ([RFC 5656](https://tools.ietf.org/html/rfc5656#section-4))
- `ecdh-sha2-nistp256` ([RFC 5656](https://tools.ietf.org/html/rfc5656#section-4))
- `diffie-hellman-group18-sha512` ([RFC 8268](https://tools.ietf.org/html/rfc8268))
- `diffie-hellman-group16-sha512` ([RFC 8268](https://tools.ietf.org/html/rfc8268))
- `diffie-hellman-group14-sha256` ([RFC 8268](https://tools.ietf.org/html/rfc8268))
- `diffie-hellman-group-exchange-sha256` ([RFC 4419](https://tools.ietf.org/html/rfc4419))
- `diffie-hellman-group14-sha1` ([RFC 4253](https://tools.ietf.org/html/rfc4253#section-8.1))
- `diffie-hellman-group-exchange-sha1` ([RFC 4419](https://tools.ietf.org/html/rfc4419))
- `diffie-hellman-group1-sha1` ([RFC 4253](https://tools.ietf.org/html/rfc4253#section-8.1))

### Encryption
- `chacha20-poly1305@openssh.com` ([draft-ietf-sshm-chacha20-poly1305](https://datatracker.ietf.org/doc/html/draft-ietf-sshm-chacha20-poly1305))
- `aes256-gcm@openssh.com` ([draft-miller-sshm-aes-gcm](https://datatracker.ietf.org/doc/html/draft-miller-sshm-aes-gcm))
- `aes128-gcm@openssh.com` ([draft-miller-sshm-aes-gcm](https://datatracker.ietf.org/doc/html/draft-miller-sshm-aes-gcm))
- `aes256-ctr` ([RFC 4344](https://tools.ietf.org/html/rfc4344#section-4))
- `aes128-ctr` ([RFC 4344](https://tools.ietf.org/html/rfc4344#section-4))
- `aes256-cbc` ([RFC 4253](https://tools.ietf.org/html/rfc4253#section-6.3))
- `aes128-cbc` ([RFC 4253](https://tools.ietf.org/html/rfc4253#section-6.3))
- `3des-cbc` ([RFC 4253](https://datatracker.ietf.org/doc/html/rfc4253#section-6.3))

### MACs
- `hmac-sha2-512-etm@openssh.com` ([OpenSSH PROTOCOL](
https://github.com/openssh/openssh-portable/blob/60b909fb110f77c1ffd15cceb5d09b8e3f79b27e/PROTOCOL#L50))
- `hmac-sha2-256-etm@openssh.com` ([OpenSSH PROTOCOL](
https://github.com/openssh/openssh-portable/blob/60b909fb110f77c1ffd15cceb5d09b8e3f79b27e/PROTOCOL#L50))
- `hmac-sha1-etm@openssh.com` ([OpenSSH PROTOCOL](
https://github.com/openssh/openssh-portable/blob/60b909fb110f77c1ffd15cceb5d09b8e3f79b27e/PROTOCOL#L50))
- `hmac-sha2-512` ([RFC 4868](https://tools.ietf.org/html/rfc4868))
- `hmac-sha2-256` ([RFC 4868](https://tools.ietf.org/html/rfc4868))
- `hmac-sha1` ([RFC 4253](https://tools.ietf.org/html/rfc4253))
The library supports a wide range of modern SSH algorithms, including:
- **Authentication**: `publickey` (including FIDO2/SK), `password`, `keyboard-interactive`
- **Host Keys**: Ed25519, Ed448, ECDSA, RSA (SHA-2)
- **Key Exchange**: ML-KEM hybrid, Curve25519, ECDH, DH group-exchange
- **Encryption**: ChaCha20-Poly1305, AES-GCM, AES-CTR
- **MACs**: HMAC-SHA2 (including ETM variants)

For a complete list of supported algorithms and their respective RFCs, see [docs/ALGORITHMS.md](docs/ALGORITHMS.md).

## Quick Start

Expand Down Expand Up @@ -146,6 +105,13 @@ try {
}
```

### FIDO2 / Security Key Authentication

The library supports authentication with `sk-ssh-ed25519@openssh.com` and
`sk-ecdsa-sha2-nistp256@openssh.com` keys. Callers provide their own FIDO2 stack and surface the resulting assertion through the library's helpers.

See [docs/SK_AUTH.md](docs/SK_AUTH.md) for detailed implementation details and examples.

### SSH Agent Forwarding

Enable SSH agent forwarding to allow remote servers to use your keys:
Expand Down
50 changes: 50 additions & 0 deletions docs/ALGORITHMS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Supported SSH Algorithms

This document lists the cryptographic algorithms supported by the ConnectBot SSH library.

## Authentication
- `keyboard-interactive`
- `password`
- `publickey` (including FIDO2 / Security Key algorithms `sk-ssh-ed25519@openssh.com` and `sk-ecdsa-sha2-nistp256@openssh.com`)

## Host Keys
- `ssh-ed25519` ([RFC 8709](https://tools.ietf.org/html/rfc8709))
- `ssh-ed448` ([RFC 8709](https://tools.ietf.org/html/rfc8709))
- `ecdsa-sha2-nistp256` ([RFC 5656](https://tools.ietf.org/html/rfc5656#section-3))
- `ecdsa-sha2-nistp384` ([RFC 5656](https://tools.ietf.org/html/rfc5656#section-3))
- `ecdsa-sha2-nistp521` ([RFC 5656](https://tools.ietf.org/html/rfc5656#section-3))
- `rsa-sha2-512` ([RFC 8332](https://tools.ietf.org/html/rfc8332#section-3))
- `rsa-sha2-256` ([RFC 8332](https://tools.ietf.org/html/rfc8332#section-3))
- `ssh-rsa` ([RFC 4253](https://tools.ietf.org/html/rfc4253#section-8.1))

## Key Exchange
- `mlkem768x25519-sha256` ([draft-ietf-sshm-mlkem-hybrid-kex](https://datatracker.ietf.org/doc/draft-ietf-sshm-mlkem-hybrid-kex/)) (depends on JEP-496 support)
- `curve25519-sha256` ([RFC 8731](https://tools.ietf.org/html/rfc8731))
- `ecdh-sha2-nistp521` ([RFC 5656](https://tools.ietf.org/html/rfc5656#section-4))
- `ecdh-sha2-nistp384` ([RFC 5656](https://tools.ietf.org/html/rfc5656#section-4))
- `ecdh-sha2-nistp256` ([RFC 5656](https://tools.ietf.org/html/rfc5656#section-4))
- `diffie-hellman-group18-sha512` ([RFC 8268](https://tools.ietf.org/html/rfc8268))
- `diffie-hellman-group16-sha512` ([RFC 8268](https://tools.ietf.org/html/rfc8268))
- `diffie-hellman-group14-sha256` ([RFC 8268](https://tools.ietf.org/html/rfc8268))
- `diffie-hellman-group-exchange-sha256` ([RFC 4419](https://tools.ietf.org/html/rfc4419))
- `diffie-hellman-group14-sha1` ([RFC 4253](https://tools.ietf.org/html/rfc4253#section-8.1))
- `diffie-hellman-group-exchange-sha1` ([RFC 4419](https://tools.ietf.org/html/rfc4419))
- `diffie-hellman-group1-sha1` ([RFC 4253](https://tools.ietf.org/html/rfc4253#section-8.1))

## Encryption
- `chacha20-poly1305@openssh.com` ([draft-ietf-sshm-chacha20-poly1305](https://datatracker.ietf.org/doc/html/draft-ietf-sshm-chacha20-poly1305))
- `aes256-gcm@openssh.com` ([draft-miller-sshm-aes-gcm](https://datatracker.ietf.org/doc/html/draft-miller-sshm-aes-gcm))
- `aes128-gcm@openssh.com` ([draft-miller-sshm-aes-gcm](https://datatracker.ietf.org/doc/html/draft-miller-sshm-aes-gcm))
- `aes256-ctr` ([RFC 4344](https://tools.ietf.org/html/rfc4344#section-4))
- `aes128-ctr` ([RFC 4344](https://tools.ietf.org/html/rfc4344#section-4))
- `aes256-cbc` ([RFC 4253](https://tools.ietf.org/html/rfc4253#section-6.3))
- `aes128-cbc` ([RFC 4253](https://tools.ietf.org/html/rfc4253#section-6.3))
- `3des-cbc` ([RFC 4253](https://datatracker.ietf.org/doc/html/rfc4253#section-6.3))

## MACs
- `hmac-sha2-512-etm@openssh.com` ([OpenSSH PROTOCOL](https://github.com/openssh/openssh-portable/blob/60b909fb110f77c1ffd15cceb5d09b8e3f79b27e/PROTOCOL#L50))
- `hmac-sha2-256-etm@openssh.com` ([OpenSSH PROTOCOL](https://github.com/openssh/openssh-portable/blob/60b909fb110f77c1ffd15cceb5d09b8e3f79b27e/PROTOCOL#L50))
- `hmac-sha1-etm@openssh.com` ([OpenSSH PROTOCOL](https://github.com/openssh/openssh-portable/blob/60b909fb110f77c1ffd15cceb5d09b8e3f79b27e/PROTOCOL#L50))
- `hmac-sha2-512` ([RFC 4868](https://tools.ietf.org/html/rfc4868))
- `hmac-sha2-256` ([RFC 4868](https://tools.ietf.org/html/rfc4868))
- `hmac-sha1` ([RFC 4253](https://tools.ietf.org/html/rfc4253))
69 changes: 69 additions & 0 deletions docs/SK_AUTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# FIDO2 / Security Key Authentication

The library supports authentication with `sk-ssh-ed25519@openssh.com` and
`sk-ecdsa-sha2-nistp256@openssh.com` keys.

## Overview

This library does **not** include a CTAP2 transport (USB-HID, NFC, BLE). Callers are responsible for providing their own FIDO2 stack and surfacing the resulting assertion through [`AuthHandler.onSignatureRequest`](../sshlib/src/main/kotlin/org/connectbot/sshlib/AuthHandler.kt).

The helpers in `org.connectbot.sshlib.sk` cover the SSH wire-format bits so
callers don't have to implement OpenSSH's `PROTOCOL.u2f` themselves.

## Implementation Example

```kotlin
import org.connectbot.sshlib.AuthHandler
import org.connectbot.sshlib.AuthPublicKey
import org.connectbot.sshlib.sk.SkAlgorithm
import org.connectbot.sshlib.sk.SkAuthHelpers
import org.connectbot.sshlib.sk.SkSignatureBlob

class SkAuthHandler(
private val rawPublicKey: ByteArray, // 32-byte Ed25519 pubkey from your stored SK
private val application: String, // RP id, e.g. "ssh:"
private val credentialId: ByteArray, // CTAP2 credential id
private val ctap2: MyCtap2Stack, // your CTAP2 transport
) : AuthHandler {
override suspend fun onPublicKeysNeeded() = listOf(
SkAuthHelpers.buildAuthPublicKey(SkAlgorithm.ED25519, rawPublicKey, application),
)

override suspend fun onSignatureRequest(key: AuthPublicKey, dataToSign: ByteArray): ByteArray {
// The CTAP2 device hashes (clientDataHash) for us — just pass dataToSign through SHA-256.
val assertion = ctap2.getAssertion(
rpId = application,
credentialId = credentialId,
clientDataHash = sha256(dataToSign),
)
return SkSignatureBlob.pack(
algorithm = SkAlgorithm.ED25519,
rawSignature = assertion.signature, // 64-byte raw Ed25519, or DER for ECDSA-P256
flags = assertion.flags, // 0x01 = UP, |0x04 if UV was tested
counter = assertion.counter,
)
}

override suspend fun onPasswordNeeded() = null
override suspend fun onKeyboardInteractivePrompt(...) = null
}

val client = SshClient("server.example.com")
client.connect()
val result = client.authenticate("user", SkAuthHandler(...))
```

## ECDSA P-256 Keys

For ECDSA-P256 keys, pass `SkAlgorithm.ECDSA_P256` and supply the DER-encoded `SEQUENCE { INTEGER r, INTEGER s }` signature that CTAP2 returns. `SkSignatureBlob.pack()` converts it to OpenSSH's `mpint r || mpint s` form internally.

## Public Key Decoding

Use `SkPublicKeyDecoder` to parse public-key blobs that arrive in OpenSSH-format SK files.

## Out of Scope

The following are **not** handled by this library:
- CTAP2 transport (USB-HID, NFC, BLE).
- Credential creation (`ssh-keygen -t *-sk` equivalent).
- Parsing `ssh-keygen`'s OpenSSH-format SK private-key files. (The library decodes the SK *public-key blob*; the surrounding OpenSSH private-key envelope is the caller's responsibility.)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
meta:
id: sk_ecdsa_p256_public_key_blob
endian: be
imports:
- byte_string
doc: >
OpenSSH Security Key ECDSA P-256 public key blob.
Defined in OpenSSH PROTOCOL.u2f section 3.1.
seq:
- id: curve_name_len
type: u4
valid: 8
- id: curve_name
contents: "nistp256"
- id: public_key
type: byte_string
doc: Uncompressed SEC1 point (0x04 || X || Y)
- id: application
type: byte_string
doc: Relying-party identifier (e.g. "ssh:")
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
meta:
id: sk_ecdsa_p256_signature_blob
endian: be
imports:
- byte_string
- ecdsa_signature_blob
doc: >
OpenSSH Security Key ECDSA P-256 signature blob.
Defined in OpenSSH PROTOCOL.u2f section 3.2.
seq:
- id: signature
type: ecdsa_signature_blob
doc: ECDSA signature material (mpint r || mpint s)
- id: flags
type: u1
doc: FIDO2 device flags (e.g. 0x01 for UP)
- id: counter
type: u4
doc: FIDO2 device signature counter
15 changes: 15 additions & 0 deletions protocol/src/main/resources/kaitai/sk_ed25519_public_key_blob.ksy
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
meta:
id: sk_ed25519_public_key_blob
endian: be
imports:
- byte_string
doc: >
OpenSSH Security Key Ed25519 public key blob.
Defined in OpenSSH PROTOCOL.u2f section 3.1.
seq:
- id: public_key
type: byte_string
doc: Ed25519 public key (32 bytes)
- id: application
type: byte_string
doc: Relying-party identifier (e.g. "ssh:")
18 changes: 18 additions & 0 deletions protocol/src/main/resources/kaitai/sk_ed25519_signature_blob.ksy
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
meta:
id: sk_ed25519_signature_blob
endian: be
imports:
- byte_string
doc: >
OpenSSH Security Key Ed25519 signature blob.
Defined in OpenSSH PROTOCOL.u2f section 3.2.
seq:
- id: signature
type: byte_string
doc: Raw Ed25519 signature (64 bytes)
- id: flags
type: u1
doc: FIDO2 device flags (e.g. 0x01 for UP)
- id: counter
type: u4
doc: FIDO2 device signature counter
4 changes: 4 additions & 0 deletions protocol/src/main/resources/kaitai/ssh_public_key.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ meta:
- ssh_ed25519_public_key_blob
- ssh_ed448_public_key_blob
- ssh_rsa_public_key_blob
- sk_ed25519_public_key_blob
- sk_ecdsa_p256_public_key_blob
doc-ref: RFC 4253 section 6.6
doc: >
Generic SSH public key structure. The key blob format is defined
Expand All @@ -30,4 +32,6 @@ seq:
'"ecdsa-sha2-nistp521"': ecdsa_public_key_blob
'"ssh-ed25519"': ssh_ed25519_public_key_blob
'"ssh-ed448"': ssh_ed448_public_key_blob
'"sk-ssh-ed25519@openssh.com"': sk_ed25519_public_key_blob
'"sk-ecdsa-sha2-nistp256@openssh.com"': sk_ecdsa_p256_public_key_blob
_: byte_string
4 changes: 4 additions & 0 deletions protocol/src/main/resources/kaitai/ssh_signature.ksy
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ meta:
- ssh_ed25519_signature_blob
- ssh_ed448_signature_blob
- ssh_rsa_signature_blob
- sk_ed25519_signature_blob
- sk_ecdsa_p256_signature_blob
doc-ref: RFC 4253 section 6.6
doc: >
Generic SSH signature structure. The signature blob format is defined
Expand All @@ -32,4 +34,6 @@ seq:
'"ecdsa-sha2-nistp521"': ecdsa_signature_blob
'"ssh-ed25519"': ssh_ed25519_signature_blob
'"ssh-ed448"': ssh_ed448_signature_blob
'"sk-ssh-ed25519@openssh.com"': sk_ed25519_signature_blob
'"sk-ecdsa-sha2-nistp256@openssh.com"': sk_ecdsa_p256_signature_blob
_: byte_string
58 changes: 58 additions & 0 deletions sshlib/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,64 @@ package org.connectbot.sshlib.client {

}

package org.connectbot.sshlib.sk {

public enum SkAlgorithm {
method @InaccessibleFromKotlin public java.lang.String getSshName();
property public String sshName;
enum_constant public static final org.connectbot.sshlib.sk.SkAlgorithm ECDSA_P256;
enum_constant public static final org.connectbot.sshlib.sk.SkAlgorithm ED25519;
field public static final org.connectbot.sshlib.sk.SkAlgorithm.Companion Companion;
}

public static final class SkAlgorithm.Companion {
method public org.connectbot.sshlib.sk.SkAlgorithm? fromSshName(java.lang.String name);
method public boolean isSkAlgorithm(java.lang.String name);
}

public final class SkAuthHelpers {
method public org.connectbot.sshlib.AuthPublicKey buildAuthPublicKey(org.connectbot.sshlib.sk.SkAlgorithm algorithm, byte[] rawKey, java.lang.String application);
field public static final org.connectbot.sshlib.sk.SkAuthHelpers INSTANCE;
}

public final class SkPublicKey {
ctor public SkPublicKey(org.connectbot.sshlib.sk.SkAlgorithm algorithm, byte[] rawKey, java.lang.String application);
method public org.connectbot.sshlib.sk.SkAlgorithm component1();
method public byte[] component2();
method public java.lang.String component3();
method public org.connectbot.sshlib.sk.SkPublicKey copy(optional org.connectbot.sshlib.sk.SkAlgorithm algorithm, optional byte[] rawKey, optional java.lang.String application);
method public boolean equals(java.lang.Object? other);
method @InaccessibleFromKotlin public org.connectbot.sshlib.sk.SkAlgorithm getAlgorithm();
method @InaccessibleFromKotlin public java.lang.String getApplication();
method @InaccessibleFromKotlin public byte[] getRawKey();
method public int hashCode();
method public java.lang.String toString();
property public org.connectbot.sshlib.sk.SkAlgorithm algorithm;
property public String application;
property public byte[] rawKey;
}

public final class SkPublicKeyDecoder {
method public org.connectbot.sshlib.sk.SkPublicKey decode(byte[] blob);
field public static final org.connectbot.sshlib.sk.SkPublicKeyDecoder INSTANCE;
}

public final class SkPublicKeyEncoder {
method public byte[] encode(org.connectbot.sshlib.sk.SkAlgorithm algorithm, byte[] rawKey, java.lang.String application);
field public static final org.connectbot.sshlib.sk.SkPublicKeyEncoder INSTANCE;
}

public final class SkSignatureBlob {
method @KotlinOnly public byte[] pack(org.connectbot.sshlib.sk.SkAlgorithm algorithm, byte[] rawSignature, byte flags, kotlin.UInt counter);
property public static byte FLAG_USER_PRESENCE;
property public static byte FLAG_USER_VERIFICATION;
field public static final byte FLAG_USER_PRESENCE = 1; // 0x1
field public static final byte FLAG_USER_VERIFICATION = 4; // 0x4
field public static final org.connectbot.sshlib.sk.SkSignatureBlob INSTANCE;
}

}

package org.connectbot.sshlib.transport {

public interface AddressResolver {
Expand Down
16 changes: 14 additions & 2 deletions sshlib/src/main/kotlin/org/connectbot/sshlib/AuthHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,20 @@ interface AuthHandler {
* Called only for keys the server accepted (PK_OK). Return the signature
* over [dataToSign], or null to skip this key.
*
* Use [SshSigning.sign] for local private keys, or return an SSH agent
* response directly.
* The returned bytes are written verbatim into the publickey
* `SSH_MSG_USERAUTH_REQUEST` signature field — the library does not
* decode or repackage them. This makes the callback a clean extension
* point for externally-signed keys.
*
* Use cases:
* - **Local private key**: call [SshSigning.sign].
* - **SSH agent**: forward [dataToSign] to your agent and return its response.
* - **FIDO2 / Security Key** (`sk-ssh-ed25519@openssh.com`,
* `sk-ecdsa-sha2-nistp256@openssh.com`): drive your CTAP2 stack with
* `clientDataHash = SHA-256(dataToSign)`, then return
* `org.connectbot.sshlib.sk.SkSignatureBlob.pack(algorithm, rawSignature, flags, counter)`.
* See `org.connectbot.sshlib.sk.SkAuthHelpers` for the matching
* public-key blob constructor.
*/
suspend fun onSignatureRequest(key: AuthPublicKey, dataToSign: ByteArray): ByteArray?

Expand Down
Loading
Loading