diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 364ec9f..8fe5500 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,9 +9,9 @@ jobs: matrix: go-version: [ '1.21.x', '1.22.0-rc.1' ] goos: [linux] - testuser: [ssh3-testuser] - testpasswd: [ssh3-testpasswd] - testuserhome: [/home/ssh3-testuser] + testuser: [soh-testuser] + testpasswd: [soh-testpasswd] + testuserhome: [/home/soh-testuser] archparams: [{goarch: amd64, cc: gcc}] #,{goarch: arm64, cc: aarch64-linux-gnu-gcc}] runs-on: ubuntu-22.04 @@ -37,14 +37,14 @@ jobs: run: sudo useradd -s /bin/bash -m ${{matrix.testuser}} && echo "${{matrix.testuser}}:${{matrix.testpasswd}}" | sudo chpasswd - name: Ensure there are no existing .profile or similar files for testuser run: sudo rm -f ${{matrix.testuserhome}}/.profile ${{matrix.testuserhome}}/.bash_profile ${{matrix.testuserhome}}/.bash_login - - name: Create .ssh3 directory - run: sudo su ${{matrix.testuser}} -c 'mkdir ${{matrix.testuserhome}}/.ssh ${{matrix.testuserhome}}/.ssh3' + - name: Create .soh directory + run: sudo su ${{matrix.testuser}} -c 'mkdir ${{matrix.testuserhome}}/.ssh ${{matrix.testuserhome}}/.soh' - name: add the attacker's key as commented in testuser's authorzed identities - run: echo "#" $(cat attacker_id_rsa.pub) | sudo tee -a ${{matrix.testuserhome}}/.ssh3/authorized_identities + run: echo "#" $(cat attacker_id_rsa.pub) | sudo tee -a ${{matrix.testuserhome}}/.soh/authorized_identities - name: Put test public keys in testuser's authorized_identities - run: cat /testuser_id_rsa.pub /testuser_id_ed25519.pub | sudo tee -a ${{matrix.testuserhome}}/.ssh3/authorized_identities + run: cat /testuser_id_rsa.pub /testuser_id_ed25519.pub | sudo tee -a ${{matrix.testuserhome}}/.soh/authorized_identities - name: log authorized_identities - run: cat ${{matrix.testuserhome}}/.ssh3/authorized_identities + run: cat ${{matrix.testuserhome}}/.soh/authorized_identities - name: Integration tests run: sudo -E PATH=$PATH make -e integration-tests env: @@ -58,7 +58,7 @@ jobs: CGO_ENABLED: "1" GOOS: ${{matrix.goos}} GOARCH: ${{matrix.archparams.goarch}} - SSH3_INTEGRATION_TESTS_WITH_SERVER_ENABLED: "1" + SOH_INTEGRATION_TESTS_WITH_SERVER_ENABLED: "1" build-macos: strategy: matrix: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b6e546..62f4c34 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,9 +9,9 @@ jobs: matrix: go-version: [ '1.21.x', '1.22.0-rc.1' ] goos: [linux] - testuser: [ssh3-testuser] - testpasswd: [ssh3-testpasswd] - testuserhome: [/home/ssh3-testuser] + testuser: [soh-testuser] + testpasswd: [soh-testpasswd] + testuserhome: [/home/soh-testuser] archparams: [{goarch: amd64, cc: gcc}] #,{goarch: arm64, cc: aarch64-linux-gnu-gcc}] runs-on: ubuntu-22.04 diff --git a/.goreleaser-client-only-windows.yml b/.goreleaser-client-only-windows.yml index 461ce0f..3560c51 100644 --- a/.goreleaser-client-only-windows.yml +++ b/.goreleaser-client-only-windows.yml @@ -17,9 +17,9 @@ before: builds: - - id: "ssh3" - main: ./cmd/ssh3/main.go - binary: ssh3 + id: "soh" + main: ./cmd/soh/main.go + binary: soh goos: - windows goarch: diff --git a/.goreleaser-linux-amd64.yml b/.goreleaser-linux-amd64.yml index b39e59c..307fb36 100644 --- a/.goreleaser-linux-amd64.yml +++ b/.goreleaser-linux-amd64.yml @@ -17,9 +17,9 @@ before: builds: - - id: "ssh3" - main: ./cmd/ssh3/main.go - binary: ssh3 + id: "soh" + main: ./cmd/soh/main.go + binary: soh goos: - linux goarch: @@ -30,9 +30,9 @@ builds: - static_build - feature - - id: "ssh3-server" - main: ./cmd/ssh3-server/main.go - binary: ssh3-server + id: "soh-server" + main: ./cmd/soh-server/main.go + binary: soh-server env: - CGO_ENABLED=1 - CC=/usr/local/musl/bin/musl-gcc diff --git a/.goreleaser-linux-arm64.yml b/.goreleaser-linux-arm64.yml index 610a65d..5afc2e4 100644 --- a/.goreleaser-linux-arm64.yml +++ b/.goreleaser-linux-arm64.yml @@ -17,9 +17,9 @@ before: builds: - - id: "ssh3" - main: ./cmd/ssh3/main.go - binary: ssh3 + id: "soh" + main: ./cmd/soh/main.go + binary: soh goos: - linux goarch: @@ -30,9 +30,9 @@ builds: - static_build - feature - - id: "ssh3-server" - main: ./cmd/ssh3-server/main.go - binary: ssh3-server + id: "soh-server" + main: ./cmd/soh-server/main.go + binary: soh-server env: - CC=/tmp/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc - CGO_ENABLED=1 diff --git a/.goreleaser-unix.yml b/.goreleaser-unix.yml index fca92e3..69effb3 100644 --- a/.goreleaser-unix.yml +++ b/.goreleaser-unix.yml @@ -17,9 +17,9 @@ before: builds: - - id: "ssh3" - main: ./cmd/ssh3/main.go - binary: ssh3 + id: "soh" + main: ./cmd/soh/main.go + binary: soh goos: - darwin - freebsd @@ -39,9 +39,9 @@ builds: - netgo - static_build - - id: "ssh3-server" - main: ./cmd/ssh3-server/main.go - binary: ssh3-server + id: "soh-server" + main: ./cmd/soh-server/main.go + binary: soh-server goos: - darwin - freebsd diff --git a/Makefile b/Makefile index 5622653..8a5f8f2 100644 --- a/Makefile +++ b/Makefile @@ -24,17 +24,17 @@ integration-tests: CC=$(CC) \ CGO_ENABLED=1 \ GOOS=$(GOOS) \ - SSH3_INTEGRATION_TESTS_WITH_SERVER_ENABLED=1 \ + SOH_INTEGRATION_TESTS_WITH_SERVER_ENABLED=1 \ go run github.com/onsi/ginkgo/v2/ginkgo ./integration_tests install: - $(GO_OPTS) go install $(BUILDFLAGS) ./cmd/ssh3 - $(GO_OPTS) go install $(BUILDFLAGS) ./cmd/ssh3-server + $(GO_OPTS) go install $(BUILDFLAGS) ./cmd/soh + $(GO_OPTS) go install $(BUILDFLAGS) ./cmd/soh-server build: client server client: - $(GO_OPTS) go build -tags "$(GO_TAGS)" $(BUILD_FLAGS) -o bin/client ./cmd/ssh3/ + $(GO_OPTS) go build -tags "$(GO_TAGS)" $(BUILD_FLAGS) -o bin/client ./cmd/soh/ server: - $(GO_OPTS) go build -tags "$(GO_TAGS)" $(BUILD_FLAGS) -o bin/server ./cmd/ssh3-server/ + $(GO_OPTS) go build -tags "$(GO_TAGS)" $(BUILD_FLAGS) -o bin/server ./cmd/soh-server/ diff --git a/README.md b/README.md index 00e15cc..188cef9 100644 --- a/README.md +++ b/README.md @@ -1,136 +1,136 @@
- +
-# SSH3: faster and rich secure shell using HTTP/3 -SSH3 is a complete revisit of the SSH +# SOH: faster and rich secure shell using HTTP/3 +SOH is a complete revisit of the SSH protocol, mapping its semantics on top of the HTTP mechanisms. -In a nutshell, SSH3 uses [QUIC](https://datatracker.ietf.org/doc/html/rfc9000)+[TLS1.3](https://datatracker.ietf.org/doc/html/rfc8446) for +In a nutshell, SOH uses [QUIC](https://datatracker.ietf.org/doc/html/rfc9000)+[TLS1.3](https://datatracker.ietf.org/doc/html/rfc8446) for secure channel establishment and the [HTTP Authorization](https://www.rfc-editor.org/rfc/rfc9110.html#name-authorization) mechanisms for user authentication. -Among others, SSH3 allows the following improvements: +Among others, SOH allows the following improvements: - Significantly faster session establishment - New HTTP authentication methods such as [OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6749) and [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) in addition to classical SSH authentication -- Robustness to port scanning attacks: your SSH3 server can be made **invisible** to other Internet users +- Robustness to port scanning attacks: your SOH server can be made **invisible** to other Internet users - UDP port forwarding in addition to classical TCP port forwarding - All the features allowed by the modern QUIC protocol: including connection migration (soon) and multipath connections > [!TIP] -> Quickly want to get started ? Checkout how to [install SSH3](#installing-ssh3). You will learn to [setup an SSH3 server](#deploying-an-ssh3-server) and [use the SSH3 client](#using-the-ssh3-client). +> Quickly want to get started ? Checkout how to [install SOH](#installing-soh). You will learn to [setup an SOH server](#deploying-an-soh-server) and [use the SOH client](#using-the-soh-client). -*SSH3* stands for the concatenation of *SSH* and *H3*. +*SOH* stands for *Shell over HTTP*. -## โšก SSH3 is faster -Faster for session establishment, not throughput ! SSH3 offers a significantly faster session establishment than SSHv2. Establishing a new session with SSHv2 can take 5 to 7 network round-trip times, which can easily be noticed by the user. SSH3 only needs 3 round-trip times. The keystroke latency in a running session is unchanged. +## โšก SOH is faster +Faster for session establishment, not throughput ! SOH offers a significantly faster session establishment than SSHv2. Establishing a new session with SSHv2 can take 5 to 7 network round-trip times, which can easily be noticed by the user. SOH only needs 3 round-trip times. The keystroke latency in a running session is unchanged.

- -SSH3 (top) VS SSHv2 (bottom) session establishement with a 100ms ping towards the server. + +SOH (top) VS SSHv2 (bottom) session establishement with a 100ms ping towards the server.

-## ๐Ÿ”’ SSH3 security -While SSHv2 defines its own protocols for user authentication and secure channel establishment, SSH3 relies on the robust and time-tested mechanisms of TLS 1.3, QUIC and HTTP. These protocols are already extensively used to secure security-critical applications on the Internet such as e-commerce and Internet banking. +## ๐Ÿ”’ SOH security +While SSHv2 defines its own protocols for user authentication and secure channel establishment, SOH relies on the robust and time-tested mechanisms of TLS 1.3, QUIC and HTTP. These protocols are already extensively used to secure security-critical applications on the Internet such as e-commerce and Internet banking. -SSH3 already implements the common password-based and public-key (RSA and EdDSA/ed25519) authentication methods. It also supports new authentication methods such as OAuth 2.0 and allows logging in to your servers using your Google/Microsoft/Github accounts. +SOH already implements the common password-based and public-key (RSA and EdDSA/ed25519) authentication methods. It also supports new authentication methods such as OAuth 2.0 and allows logging in to your servers using your Google/Microsoft/Github accounts. -### ๐Ÿงช SSH3 is still experimental -While SSH3 shows promise for faster session establishment, it is still at an early proof-of-concept stage. As with any new complex protocol, **expert cryptographic review over an extended timeframe is required before reasonable security conclusions can be made**. +### ๐Ÿงช SOH is still experimental +While SOH shows promise for faster session establishment, it is still at an early proof-of-concept stage. As with any new complex protocol, **expert cryptographic review over an extended timeframe is required before reasonable security conclusions can be made**. -We are developing SSH3 as an open source project to facilitate community feedback and analysis. However, we **cannot yet endorse its appropriateness for production systems** without further peer review. Please collaborate with us if you have relevant expertise! +We are developing SOH as an open source project to facilitate community feedback and analysis. However, we **cannot yet endorse its appropriateness for production systems** without further peer review. Please collaborate with us if you have relevant expertise! -### ๐Ÿฅท Do not deploy the SSH3 server on your production servers for now -Given the current prototype state, we advise *testing SSH3 in sandboxed environments or private networks*. Be aware that making experimental servers directly Internet-accessible could introduce risk before thorough security vetting. +### ๐Ÿฅท Do not deploy the SOH server on your production servers for now +Given the current prototype state, we advise *testing SOH in sandboxed environments or private networks*. Be aware that making experimental servers directly Internet-accessible could introduce risk before thorough security vetting. -While [hiding](#-your-ssh3-public-server-can-be-hidden) servers behind secret paths has potential benefits, it does not negate the need for rigorous vulnerability analysis before entering production. We are excited by SSH3's future possibilities but encourage additional scrutiny first. +While [hiding](#-your-soh-public-server-can-be-hidden) servers behind secret paths has potential benefits, it does not negate the need for rigorous vulnerability analysis before entering production. We are excited by SOH's future possibilities but encourage additional scrutiny first. -## ๐Ÿฅท Your SSH3 public server can be hidden -Using SSH3, you can avoid the usual stress of scanning and dictionary attacks against your SSH server. Similarly to your secret Google Drive documents, your SSH3 server can be hidden behind a secret link and only answer to authentication attempts that made an HTTP request to this specific link, like the following: +## ๐Ÿฅท Your SOH public server can be hidden +Using SOH, you can avoid the usual stress of scanning and dictionary attacks against your SSH server. Similarly to your secret Google Drive documents, your SOH server can be hidden behind a secret link and only answer to authentication attempts that made an HTTP request to this specific link, like the following: - ssh3-server -bind 192.0.2.0:443 -url-path + soh-server -bind 192.0.2.0:443 -url-path -By replacing `` by, let's say, the random value `M3MzkxYWMxMjYxMjc5YzJkODZiMTAyMjU`, your SSH3 server will only answer to SSH3 connection attempts made to the URL `https://192.0.2.0:443/M3MzkxYWMxMjYxMjc5YzJkODZiMTAyMjU` and it will respond a `404 Not Found` to other requests. Attackers and crawlers on the Internet can therefore not detect the presence of your SSH3 server. They will only see a simple web server answering 404 status codes to every request. +By replacing `` by, let's say, the random value `M3MzkxYWMxMjYxMjc5YzJkODZiMTAyMjU`, your SOH server will only answer to SOH connection attempts made to the URL `https://192.0.2.0:443/M3MzkxYWMxMjYxMjc5YzJkODZiMTAyMjU` and it will respond a `404 Not Found` to other requests. Attackers and crawlers on the Internet can therefore not detect the presence of your SOH server. They will only see a simple web server answering 404 status codes to every request. -## ๐Ÿ’ SSH3 is already feature-rich -SSH3 provides new feature that could not be provided by the SSHv2 protocol. +## ๐Ÿ’ SOH is already feature-rich +SOH provides new feature that could not be provided by the SSHv2 protocol. ### Brand new features -- **UDP port forwarding**: you can now access your QUIC, DNS, RTP or any UDP-based server that are only reachable from your SSH3 host. +- **UDP port forwarding**: you can now access your QUIC, DNS, RTP or any UDP-based server that are only reachable from your SOH host. UDP packets are forwarded using QUIC datagrams. -- **X.509 certificates**: you can now use your classical HTTPS certificates to authenticate your SSH3 server. This mechanism is more secure than the classical SSHv2 host key mechanism. Certificates can be obtained easily using LetsEncrypt for instance. +- **X.509 certificates**: you can now use your classical HTTPS certificates to authenticate your SOH server. This mechanism is more secure than the classical SSHv2 host key mechanism. Certificates can be obtained easily using LetsEncrypt for instance. - **Hiding** your server behind a secret link. -- **Keyless** secure user authentication using **OpenID Connect**. You can connect to your SSH3 server using the SSO of your company or your Google/Github account, and you don't need to copy the public keys of your users anymore. +- **Keyless** secure user authentication using **OpenID Connect**. You can connect to your SOH server using the SSO of your company or your Google/Github account, and you don't need to copy the public keys of your users anymore. ### Famous OpenSSH features implemented -This SSH3 implementation already provides many of the popular features of OpenSSH, so if you are used to OpenSSH, the process of adopting SSH3 will be smooth. Here is a list of some OpenSSH features that SSH3 also implements: +This SOH implementation already provides many of the popular features of OpenSSH, so if you are used to OpenSSH, the process of adopting SOH will be smooth. Here is a list of some OpenSSH features that SOH also implements: - Parses `~/.ssh/authorized_keys` on the server - Certificate-based server authentication - `known_hosts` mechanism when X.509 certificates are not used. - Automatically using the `ssh-agent` for public key authentication - SSH agent forwarding to use your local keys on your remote server - Direct TCP port forwarding (reverse port forwarding will be implemented in the future) -- Proxy jump (see the `-proxy-jump` parameter). If A is an SSH3 client and B and C are both SSH3 servers, you can connect from A to C using B as a gateway/proxy. The proxy uses UDP forwarding to forward the QUIC packets from A to C, so B cannot decrypt the traffic A<->C SSH3 traffic. +- Proxy jump (see the `-proxy-jump` parameter). If A is an SOH client and B and C are both SOH servers, you can connect from A to C using B as a gateway/proxy. The proxy uses UDP forwarding to forward the QUIC packets from A to C, so B cannot decrypt the traffic A<->C SOH traffic. - Parses `~/.ssh/config` on the client and handles the `Hostname`, `User`, `Port` and `IdentityFile` config options (the other options are currently ignored). Also parses a new `UDPProxyJump` that behaves similarly to OpenSSH's `ProxyJump`. ## ๐Ÿ™ Community support -Help us progress SSH3 responsibly! We welcome capable security researchers to review our codebase and provide feedback. Please also connect us with relevant standards bodies to potentially advance SSH3 through the formal IETF/IRTF processes over time. +Help us progress SOH responsibly! We welcome capable security researchers to review our codebase and provide feedback. Please also connect us with relevant standards bodies to potentially advance SOH through the formal IETF/IRTF processes over time. -With collaborative assistance, we hope to iteratively improve SSH3 towards safe production readiness. But we cannot credibly make definitive security claims without evidence of extensive expert cryptographic review and adoption by respected security authorities. Let's work together to realize SSH3's possibilities! +With collaborative assistance, we hope to iteratively improve SOH towards safe production readiness. But we cannot credibly make definitive security claims without evidence of extensive expert cryptographic review and adoption by respected security authorities. Let's work together to realize SOH's possibilities! -## Installing SSH3 -You can either download the last [release binaries](https://github.com/francoismichel/ssh3/releases), -[install it using `go install`](#installing-ssh3-and-ssh3-server-using-go-install) or generate these binaries yourself by compiling the code from source. +## Installing SOH +You can either download the last [release binaries](https://github.com/francoismichel/soh/releases), +[install it using `go install`](#installing-soh-and-soh-server-using-go-install) or generate these binaries yourself by compiling the code from source. > [!TIP] -> SSH3 is still experimental and is the fruit of a research work. If you are afraid of deploying publicly a new SSH3 server, you can use the -> [secret path](#-your-ssh3-public-server-can-be-hidden) feature of SSH3 to hide it behing a secret URL. +> SOH is still experimental and is the fruit of a research work. If you are afraid of deploying publicly a new SOH server, you can use the +> [secret path](#-your-soh-public-server-can-be-hidden) feature of SOH to hide it behing a secret URL. -### Installing ssh3 and ssh3-server using Go install +### Installing soh and soh-server using Go install ```bash -go install github.com/francoismichel/ssh3/cmd/...@v0.1.5-rc5 +go install github.com/francoismichel/soh/cmd/...@v0.1.5-rc5 ``` -### Compiling SSH3 from source +### Compiling SOH from source You need a recent [Golang](https://go.dev/dl/) version to do this. Downloading the source code and compiling the binaries can be done with the following steps: ```bash -git clone https://github.com/francoismichel/ssh3 # clone the repo -cd ssh3 -go build -o ssh3 cmd/ssh3/main.go # build the client -CGO_ENABLED=1 go build -o ssh3-server cmd/ssh3-server/main.go # build the server, requires having gcc installed +git clone https://github.com/francoismichel/soh # clone the repo +cd soh +go build -o soh cmd/soh/main.go # build the client +CGO_ENABLED=1 go build -o soh-server cmd/soh-server/main.go # build the server, requires having gcc installed ``` -If you have root/sudo privileges and you want to make ssh3 accessible to all you users, +If you have root/sudo privileges and you want to make soh accessible to all you users, you can then directly copy the binaries to `/usr/bin`: ```bash -cp ssh3 /usr/bin/ && cp ssh3-server /usr/bin +cp soh /usr/bin/ && cp soh-server /usr/bin ``` Otherwise, you can simply add the executables to your `PATH` environment variable by adding the following line at the end of your `.bashrc` or equivalent: ```bash -export PATH=$PATH:/path/to/the/ssh3/directory +export PATH=$PATH:/path/to/the/soh/directory ``` -### Deploying an SSH3 server -Before connecting to your host, you need to deploy an SSH3 server on it. There is currently -no SSH3 daemon, so right now, you will have to run the `ssh3-server` executable in background +### Deploying an SOH server +Before connecting to your host, you need to deploy an SOH server on it. There is currently +no SOH daemon, so right now, you will have to run the `soh-server` executable in background using `screen` or a similar utility. > [!NOTE] -> As SSH3 runs on top of HTTP/3, a server needs an X.509 certificate and its corresponding private key. If you do not want to generate a certificate signed by a real certificate authority, you can generate a self-signed one using the `generate_openssl_selfsigned_certificate.sh` script. This provides you with similar security guarantees to SSHv2's host keys mechanism, with the same security issue: you may be vulnerable to machine-in-the-middle attacks during your first connection to your server. Using real certificates signed by public certificate authorities such as Let's Encrypt avoids this issue. +> As SOH runs on top of HTTP/3, a server needs an X.509 certificate and its corresponding private key. If you do not want to generate a certificate signed by a real certificate authority, you can generate a self-signed one using the `generate_openssl_selfsigned_certificate.sh` script. This provides you with similar security guarantees to SSHv2's host keys mechanism, with the same security issue: you may be vulnerable to machine-in-the-middle attacks during your first connection to your server. Using real certificates signed by public certificate authorities such as Let's Encrypt avoids this issue. -Here is the usage of the `ssh3-server` executable: +Here is the usage of the `soh-server` executable: ``` -Usage of ./ssh3-server: +Usage of ./soh-server: -bind string the address:port pair to listen to, e.g. 0.0.0.0:443 (default "[::]:443") -cert string @@ -143,31 +143,31 @@ Usage of ./ssh3-server: -key string the filename of the certificate private key (default "./priv.key") -url-path string - the secret URL path on which the ssh3 server listens (default "/ssh3-term") + the secret URL path on which the soh server listens (default "/soh-term") -v verbose mode, if set ``` -The following command starts a public SSH3 server on port 443 and answers to new -sessions requests querying the `/ssh3` URL path: +The following command starts a public SOH server on port 443 and answers to new +sessions requests querying the `/soh` URL path: - ssh3-server -cert /path/to/cert/or/fullchain -key /path/to/cert/private/key -url-path /ssh3 + soh-server -cert /path/to/cert/or/fullchain -key /path/to/cert/private/key -url-path /soh > [!NOTE] > Similarly to OpenSSH, the server must be run with root priviledges to log in as other users. #### Authorized keys and authorized identities -By default, the SSH3 server will look for identities in the `~/.ssh/authorized_keys` and `~/.ssh3/authorized_identities` files for each user. -`~/.ssh3/authorized_identities` allows new identities such as OpenID Connect (`oidc`) discussed [below](#openid-connect-authentication-still-experimental). +By default, the SOH server will look for identities in the `~/.ssh/authorized_keys` and `~/.soh/authorized_identities` files for each user. +`~/.soh/authorized_identities` allows new identities such as OpenID Connect (`oidc`) discussed [below](#openid-connect-authentication-still-experimental). Popular key types such as `rsa`, `ed25519` and keys in the OpenSSH format can be used. -### Using the SSH3 client -Once you have an SSH3 server running, you can connect to it using the SSH3 client similarly to what +### Using the SOH client +Once you have an SOH server running, you can connect to it using the SOH client similarly to what you did with your classical SSHv2 tool. -Here is the usage of the `ssh3` executable: +Here is the usage of the `soh` executable: ``` -Usage of ssh3: +Usage of soh: -pubkey-for-agent string if set, use an agent key whose public key matches the one in the specified path -privkey string @@ -196,13 +196,13 @@ Usage of ssh3: ``` #### Private-key authentication -You can connect to your SSH3 server at my-server.example.org listening on `/my-secret-path` using the private key located in `~/.ssh/id_rsa` with the following command: +You can connect to your SOH server at my-server.example.org listening on `/my-secret-path` using the private key located in `~/.ssh/id_rsa` with the following command: - ssh3 -privkey ~/.ssh/id_rsa username@my-server.example.org/my-secret-path + soh -privkey ~/.ssh/id_rsa username@my-server.example.org/my-secret-path #### Agent-based private key authentication -The SSH3 client works with the OpenSSH agent and uses the classical `SSH_AUTH_SOCK` environment variable to -communicate with this agent. Similarly to OpenSSH, SSH3 will list the keys provided by the SSH agent +The SOH client works with the OpenSSH agent and uses the classical `SSH_AUTH_SOCK` environment variable to +communicate with this agent. Similarly to OpenSSH, SOH will list the keys provided by the SSH agent and connect using the first key listen by the agent by default. If you want to specify a specific key to use with the agent, you can either specify the private key directly with the `-privkey` argument like above, or specify the corresponding public key using the @@ -210,13 +210,13 @@ directly with the `-privkey` argument like above, or specify the corresponding p a direct access to the private key but you only have access to the public key. #### Password-based authentication -While discouraged, you can connect to your server using passwords (if explicitly enabled on the `ssh3-server`) +While discouraged, you can connect to your server using passwords (if explicitly enabled on the `soh-server`) with the following command: - ssh3 -use-password username@my-server.example.org/my-secret-path + soh -use-password username@my-server.example.org/my-secret-path #### Config-based session establishment -`ssh3` parses your OpenSSH config. Currently, it only handles the `Hostname`; `User`, `Port` and `IdentityFile` options. +`soh` parses your OpenSSH config. Currently, it only handles the `Hostname`; `User`, `Port` and `IdentityFile` options. Let's say you have the following lines in your OpenSSH config located in `~/.ssh/config` : ``` Host my-server @@ -225,11 +225,11 @@ Host my-server IdentityFile ~/.ssh/id_rsa ``` -Similarly to what OpenSSH does, the following `ssh3` command will connect you to the SSH3 server running on 192.0.2.0 on UDP port 443 using public key authentication with the private key located in `.ssh/id_rsa` : +Similarly to what OpenSSH does, the following `soh` command will connect you to the SOH server running on 192.0.2.0 on UDP port 443 using public key authentication with the private key located in `.ssh/id_rsa` : - ssh3 my-server/my-secret-path + soh my-server/my-secret-path -If you do not want a config-based utilization of SSH3, you can read the sections below to see how to use the CLI parameters of `ssh3`. +If you do not want a config-based utilization of SOH, you can read the sections below to see how to use the CLI parameters of `soh`. #### OpenID Connect authentication (still experimental) This feature allows you to connect using an external identity provider such as the one @@ -237,12 +237,12 @@ of your company or any other provider that implements the OpenID Connect standar Github or Microsoft Entra. The authentication flow is illustrated in the GIF below.
- + *Secure connection without private key using a Google account.*
-The way it connects to your identity provider is configured in a file named `~/.ssh3/oidc_config.json`. +The way it connects to your identity provider is configured in a file named `~/.soh/oidc_config.json`. Below is an example `config.json` file for use with a Google account. This configuration file is an array and can contain several identity providers configurations. ```json @@ -255,7 +255,7 @@ and can contain several identity providers configurations. ] ``` This might change in the future, but currently, to make this feature work with your Google account, you will need to setup a new experimental application in your Google Cloud console and add your email as authorized users. -This will provide you with a `client_id` and a `client_secret` that you can then set in your `~/.ssh3/oidc_config.json`. On the server side, you just have to add the following line in your `~/.ssh3/authorized_identities`: +This will provide you with a `client_id` and a `client_secret` that you can then set in your `~/.soh/oidc_config.json`. On the server side, you just have to add the following line in your `~/.soh/authorized_identities`: ``` oidc https://accounts.google.com @@ -263,6 +263,6 @@ oidc https://accounts.google.com We currently consider removing the need of setting the client_id in the `authorized_identities` file in the future. #### Proxy jump -It is often the case that some SSH hosts can only be accessed through a gateway. SSH3 allows you to perform a Proxy Jump similarly to what is proposed by OpenSSH. -You can connect from A to C using B as a gateway/proxy. B and C must both be running a valid SSH3 server. This works by establishing UDP port forwarding on B to forward QUIC packets from A to C. -The connection from A to C is therefore fully end-to-end and B cannot decrypt or alter the SSH3 traffic between A and C. +It is often the case that some SSH hosts can only be accessed through a gateway. SOH allows you to perform a Proxy Jump similarly to what is proposed by OpenSSH. +You can connect from A to C using B as a gateway/proxy. B and C must both be running a valid SOH server. This works by establishing UDP port forwarding on B to forward QUIC packets from A to C. +The connection from A to C is therefore fully end-to-end and B cannot decrypt or alter the SOH traffic between A and C. diff --git a/SECURITY.md b/SECURITY.md index 39db0c6..ca18e92 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,6 +2,6 @@ ## Reporting a Vulnerability -We take the security of SSH3 very seriously. SSH3 is still experimental and under development. If you believe you have found a security vulnerability the current version of SSH3, please report it to us *privately*, -[using the following method](https://github.com/francoismichel/ssh3/security/advisories/new). We will first fix the vulnerability before making it public if it is serious. +We take the security of SOH very seriously. SOH is still experimental and under development. If you believe you have found a security vulnerability the current version of SOH, please report it to us *privately*, +[using the following method](https://github.com/francoismichel/soh/security/advisories/new). We will first fix the vulnerability before making it public if it is serious. diff --git a/channel.go b/channel.go index 259300a..a9ef811 100644 --- a/channel.go +++ b/channel.go @@ -1,4 +1,4 @@ -package ssh3 +package soh import ( "context" @@ -7,8 +7,8 @@ import ( "io" "net" - ssh3 "github.com/francoismichel/ssh3/message" - "github.com/francoismichel/ssh3/util" + soh "github.com/francoismichel/soh/message" + "github.com/francoismichel/soh/util" "github.com/quic-go/quic-go" ) @@ -23,7 +23,7 @@ func (e ChannelOpenFailure) Error() string { } type MessageOnNonConfirmedChannel struct { - message ssh3.Message + message soh.Message } func (e MessageOnNonConfirmedChannel) Error() string { @@ -46,17 +46,17 @@ func (e SentDatagramOnNonDatagramChannel) Error() string { return fmt.Sprintf("a datagram has been sent on non-datagram channel %d", e.channelID) } -type PtyReqHandler func(channel Channel, request ssh3.PtyRequest, wantReply bool) -type X11ReqHandler func(channel Channel, request ssh3.X11Request, wantReply bool) -type ShellReqHandler func(channel Channel, request ssh3.ShellRequest, wantReply bool) -type ExecReqHandler func(channel Channel, request ssh3.ExecRequest, wantReply bool) -type SubsystemReqHandler func(channel Channel, request ssh3.SubsystemRequest, wantReply bool) -type WindowChangeReqHandler func(channel Channel, request ssh3.WindowChangeRequest, wantReply bool) -type SignalReqHandler func(channel Channel, request ssh3.SignalRequest, wantReply bool) -type ExitStatusReqHandler func(channel Channel, request ssh3.ExitStatusRequest, wantReply bool) -type ExitSignalReqHandler func(channel Channel, request ssh3.ExitSignalRequest, wantReply bool) +type PtyReqHandler func(channel Channel, request soh.PtyRequest, wantReply bool) +type X11ReqHandler func(channel Channel, request soh.X11Request, wantReply bool) +type ShellReqHandler func(channel Channel, request soh.ShellRequest, wantReply bool) +type ExecReqHandler func(channel Channel, request soh.ExecRequest, wantReply bool) +type SubsystemReqHandler func(channel Channel, request soh.SubsystemRequest, wantReply bool) +type WindowChangeReqHandler func(channel Channel, request soh.WindowChangeRequest, wantReply bool) +type SignalReqHandler func(channel Channel, request soh.SignalRequest, wantReply bool) +type ExitStatusReqHandler func(channel Channel, request soh.ExitStatusRequest, wantReply bool) +type ExitSignalReqHandler func(channel Channel, request soh.ExitSignalRequest, wantReply bool) -type ChannelDataHandler func(channel Channel, dataType ssh3.SSHDataType, data string) +type ChannelDataHandler func(channel Channel, dataType soh.SSHDataType, data string) type channelCloseListener interface { onChannelClose(channel Channel) @@ -74,14 +74,14 @@ type Channel interface { ChannelID() util.ChannelID ConversationID() ConversationID ConversationStreamID() uint64 - NextMessage() (ssh3.Message, error) + NextMessage() (soh.Message, error) ReceiveDatagram(ctx context.Context) ([]byte, error) SendDatagram(datagram []byte) error - SendRequest(r *ssh3.ChannelRequestMessage) error + SendRequest(r *soh.ChannelRequestMessage) error CancelRead() Close() MaxPacketSize() uint64 - WriteData(dataBuf []byte, dataType ssh3.SSHDataType) (int, error) + WriteData(dataBuf []byte, dataType soh.SSHDataType) (int, error) ChannelType() string confirmChannel(maxPacketSize uint64) error setDatagramSender(func(datagram []byte) error) @@ -97,7 +97,7 @@ type channelImpl struct { confirmReceived bool header []byte - datagramSender util.SSH3DatagramSenderFunc + datagramSender util.SOHDatagramSenderFunc channelCloseListener @@ -229,7 +229,7 @@ func parseTCPForwardingHeader(channelID uint64, buf util.Reader) (*net.TCPAddr, } func NewChannel(conversationStreamID uint64, conversationID ConversationID, channelID uint64, channelType string, maxPacketSize uint64, recv quic.ReceiveStream, - send io.WriteCloser, datagramSender util.SSH3DatagramSenderFunc, channelCloseListener channelCloseListener, sendHeader bool, confirmSent bool, + send io.WriteCloser, datagramSender util.SOHDatagramSenderFunc, channelCloseListener channelCloseListener, sendHeader bool, confirmSent bool, confirmReceived bool, datagramsQueueSize uint64, additonalHeaderBytes []byte) Channel { var header []byte = nil if sendHeader { @@ -269,24 +269,24 @@ func (c *channelImpl) ConversationID() ConversationID { // / The error is EOF only if no bytes were read. If an EOF happens // / after reading some but not all the bytes, nextMessage returns // / ErrUnexpectedEOF. -func (c *channelImpl) nextMessage() (ssh3.Message, error) { - return ssh3.ParseMessage(util.NewReader(c.recv)) +func (c *channelImpl) nextMessage() (soh.Message, error) { + return soh.ParseMessage(util.NewReader(c.recv)) } // The returned message will neither be ChannelOpenConfirmationMessage nor ChannelOpenFailureMessage // as this function handles it internally -func (c *channelImpl) NextMessage() (ssh3.Message, error) { +func (c *channelImpl) NextMessage() (soh.Message, error) { genericMessage, err := c.nextMessage() if err != nil { return nil, err } switch message := genericMessage.(type) { - case *ssh3.ChannelOpenConfirmationMessage: + case *soh.ChannelOpenConfirmationMessage: c.confirmReceived = true // let's read the next message return c.NextMessage() - case *ssh3.ChannelOpenFailureMessage: + case *soh.ChannelOpenFailureMessage: return nil, ChannelOpenFailure{ReasonCode: message.ReasonCode, ErrorMsg: message.ErrorMessageUTF8} } @@ -308,14 +308,14 @@ func (c *channelImpl) maybeSendHeader() error { return nil } -func (c *channelImpl) WriteData(dataBuf []byte, dataType ssh3.SSHDataType) (int, error) { +func (c *channelImpl) WriteData(dataBuf []byte, dataType soh.SSHDataType) (int, error) { err := c.maybeSendHeader() if err != nil { return 0, err } written := 0 for len(dataBuf) > 0 { - dataMsg := &ssh3.DataOrExtendedDataMessage{ + dataMsg := &soh.DataOrExtendedDataMessage{ DataType: dataType, Data: "", } @@ -340,14 +340,14 @@ func (c *channelImpl) WriteData(dataBuf []byte, dataType ssh3.SSHDataType) (int, } func (c *channelImpl) confirmChannel(maxPacketSize uint64) error { - err := c.sendMessage(&ssh3.ChannelOpenConfirmationMessage{MaxPacketSize: maxPacketSize}) + err := c.sendMessage(&soh.ChannelOpenConfirmationMessage{MaxPacketSize: maxPacketSize}) if err == nil { c.confirmSent = true } return err } -func (c *channelImpl) sendMessage(m ssh3.Message) error { +func (c *channelImpl) sendMessage(m soh.Message) error { err := c.maybeSendHeader() if err != nil { return err @@ -383,7 +383,7 @@ func (c *channelImpl) SendDatagram(datagram []byte) error { return c.datagramSender(datagram) } -func (c *channelImpl) SendRequest(r *ssh3.ChannelRequestMessage) error { +func (c *channelImpl) SendRequest(r *soh.ChannelRequestMessage) error { //TODO: make it thread safe return c.sendMessage(r) } diff --git a/client/client.go b/client/client.go index 6c1faee..001028d 100644 --- a/client/client.go +++ b/client/client.go @@ -22,11 +22,11 @@ import ( "golang.org/x/crypto/ssh/agent" "golang.org/x/term" - "github.com/francoismichel/ssh3" - "github.com/francoismichel/ssh3/auth" - "github.com/francoismichel/ssh3/client/winsize" - ssh3Messages "github.com/francoismichel/ssh3/message" - "github.com/francoismichel/ssh3/util" + "github.com/francoismichel/soh" + "github.com/francoismichel/soh/auth" + "github.com/francoismichel/soh/client/winsize" + sohMessages "github.com/francoismichel/soh/message" + "github.com/francoismichel/soh/util" ) type ExitStatus struct { @@ -52,7 +52,7 @@ func (e NoSuitableIdentity) Error() string { return "no suitable identity found" } -func forwardAgent(parent context.Context, channel ssh3.Channel) error { +func forwardAgent(parent context.Context, channel soh.Channel) error { sockPath := os.Getenv("SSH_AUTH_SOCK") if sockPath == "" { return fmt.Errorf("no auth socket in SSH_AUTH_SOCK env var") @@ -65,7 +65,7 @@ func forwardAgent(parent context.Context, channel ssh3.Channel) error { ctx, cancel := context.WithCancelCause(parent) go func() { var err error = nil - var genericMessage ssh3Messages.Message + var genericMessage sohMessages.Message for { select { case <-ctx.Done(): @@ -85,7 +85,7 @@ func forwardAgent(parent context.Context, channel ssh3.Channel) error { return } switch message := genericMessage.(type) { - case *ssh3Messages.DataOrExtendedDataMessage: + case *sohMessages.DataOrExtendedDataMessage: _, err = c.Write([]byte(message.Data)) if err != nil { err = fmt.Errorf("error when writing on unix socker for agent forwarding channel %d: %s", channel.ChannelID(), err.Error()) @@ -120,7 +120,7 @@ func forwardAgent(parent context.Context, channel ssh3.Channel) error { log.Error().Msgf("could not read on unix socket: %s", err.Error()) return err } - _, err = channel.WriteData(buf[:n], ssh3Messages.SSH_EXTENDED_DATA_NONE) + _, err = channel.WriteData(buf[:n], sohMessages.SSH_EXTENDED_DATA_NONE) if err != nil { cancel(err) log.Error().Msgf("could not write on ssh channel: %s", err.Error()) @@ -130,7 +130,7 @@ func forwardAgent(parent context.Context, channel ssh3.Channel) error { } } -func forwardTCPInBackground(ctx context.Context, channel ssh3.Channel, conn *net.TCPConn) { +func forwardTCPInBackground(ctx context.Context, channel soh.Channel, conn *net.TCPConn) { go func() { defer conn.CloseWrite() for { @@ -153,8 +153,8 @@ func forwardTCPInBackground(ctx context.Context, channel ssh3.Channel, conn *net } switch message := genericMessage.(type) { - case *ssh3Messages.DataOrExtendedDataMessage: - if message.DataType == ssh3Messages.SSH_EXTENDED_DATA_NONE { + case *sohMessages.DataOrExtendedDataMessage: + if message.DataType == sohMessages.SSH_EXTENDED_DATA_NONE { _, err := conn.Write([]byte(message.Data)) if err != nil { log.Error().Msgf("could not write data on TCP socket: %s", err) @@ -186,7 +186,7 @@ func forwardTCPInBackground(ctx context.Context, channel ssh3.Channel, conn *net log.Error().Msgf("could read data on TCP socket: %s", err) return } - _, errWrite := channel.WriteData(buf[:n], ssh3Messages.SSH_EXTENDED_DATA_NONE) + _, errWrite := channel.WriteData(buf[:n], sohMessages.SSH_EXTENDED_DATA_NONE) if errWrite != nil { switch quicErr := errWrite.(type) { case *quic.StreamError: @@ -209,7 +209,7 @@ func forwardTCPInBackground(ctx context.Context, channel ssh3.Channel, conn *net type Client struct { qconn quic.EarlyConnection - *ssh3.Conversation + *soh.Conversation } func Dial(ctx context.Context, options *Options, qconn quic.EarlyConnection, @@ -257,7 +257,7 @@ func Dial(ctx context.Context, options *Options, qconn quic.EarlyConnection, log.Debug().Msgf("QUIC handshake complete") // Now, we're 1-RTT, we can get the TLS exporter and create the conversation tls := qconn.ConnectionState().TLS - conv, err := ssh3.NewClientConversation(30000, 10, &tls) + conv, err := soh.NewClientConversation(30000, 10, &tls) if err != nil { return nil, err } @@ -267,13 +267,13 @@ func Dial(ctx context.Context, options *Options, qconn quic.EarlyConnection, if err != nil { log.Fatal().Msgf("%s", err) } - req.Proto = "ssh3" - req.Header.Set("User-Agent", ssh3.GetCurrentVersionString()) + req.Proto = "soh" + req.Header.Set("User-Agent", soh.GetCurrentVersionString()) - var identity ssh3.Identity + var identity soh.Identity for _, method := range options.authMethods { switch m := method.(type) { - case *ssh3.PasswordAuthMethod: + case *soh.PasswordAuthMethod: log.Debug().Msgf("try password-based auth") fmt.Printf("password for %s:", hostUrl.String()) password, err := term.ReadPassword(int(syscall.Stdin)) @@ -283,7 +283,7 @@ func Dial(ctx context.Context, options *Options, qconn quic.EarlyConnection, return nil, err } identity = m.IntoIdentity(string(password)) - case *ssh3.PrivkeyFileAuthMethod: + case *soh.PrivkeyFileAuthMethod: log.Debug().Msgf("try file-based pubkey auth using file %s", m.Filename()) identity, err = m.IntoIdentityWithoutPassphrase() // could not identify without passphrase, try agent authentication by using the key's public key @@ -307,7 +307,7 @@ func Dial(ctx context.Context, options *Options, qconn quic.EarlyConnection, for _, agentKey := range agentKeys { if bytes.Equal(agentKey.Marshal(), pubkey.Marshal()) { log.Debug().Msgf("found key in agent: %s", agentKey) - identity = ssh3.NewAgentAuthMethod(pubkey).IntoIdentity(sshAgent) + identity = soh.NewAgentAuthMethod(pubkey).IntoIdentity(sshAgent) foundAgentKey = true break } @@ -334,10 +334,10 @@ func Dial(ctx context.Context, options *Options, qconn quic.EarlyConnection, } else if err != nil { log.Warn().Msgf("Could not load private key: %s", err) } - case *ssh3.AgentAuthMethod: + case *soh.AgentAuthMethod: log.Debug().Msgf("try ssh-agent-based auth") identity = m.IntoIdentity(sshAgent) - case *ssh3.OidcAuthMethod: + case *soh.OidcAuthMethod: log.Debug().Msgf("try OIDC auth to issuer %s", m.OIDCConfig().IssuerUrl) token, err := auth.Connect(context.Background(), m.OIDCConfig(), m.OIDCConfig().IssuerUrl, m.DoPKCE()) if err != nil { @@ -386,7 +386,7 @@ func (c *Client) ForwardUDP(ctx context.Context, localUDPAddr *net.UDPAddr, remo log.Error().Msgf("could listen on UDP socket: %s", err) return nil, err } - forwardings := make(map[string]ssh3.Channel) + forwardings := make(map[string]soh.Channel) go func() { buf := make([]byte, 1500) for { @@ -467,7 +467,7 @@ func (c *Client) RunSession(tty *os.File, forwardSSHAgent bool, command ...strin log.Debug().Msgf("opened new session channel") if forwardSSHAgent { - _, err := channel.WriteData([]byte("forward-agent"), ssh3Messages.SSH_EXTENDED_DATA_NONE) + _, err := channel.WriteData([]byte("forward-agent"), sohMessages.SSH_EXTENDED_DATA_NONE) if err != nil { log.Error().Msgf("could not forward agent: %s", err.Error()) return err @@ -508,9 +508,9 @@ func (c *Client) RunSession(tty *os.File, forwardSSHAgent bool, command ...strin hasWinSize := err == nil if isATTY && hasWinSize { err = channel.SendRequest( - &ssh3Messages.ChannelRequestMessage{ + &sohMessages.ChannelRequestMessage{ WantReply: true, - ChannelRequest: &ssh3Messages.PtyRequest{ + ChannelRequest: &sohMessages.PtyRequest{ Term: os.Getenv("TERM"), CharWidth: uint64(windowSize.NCols), CharHeight: uint64(windowSize.NRows), @@ -528,9 +528,9 @@ func (c *Client) RunSession(tty *os.File, forwardSSHAgent bool, command ...strin } err = channel.SendRequest( - &ssh3Messages.ChannelRequestMessage{ + &sohMessages.ChannelRequestMessage{ WantReply: true, - ChannelRequest: &ssh3Messages.ShellRequest{}, + ChannelRequest: &sohMessages.ShellRequest{}, }, ) if err != nil { @@ -551,9 +551,9 @@ func (c *Client) RunSession(tty *os.File, forwardSSHAgent bool, command ...strin } } else { channel.SendRequest( - &ssh3Messages.ChannelRequestMessage{ + &sohMessages.ChannelRequestMessage{ WantReply: true, - ChannelRequest: &ssh3Messages.ExecRequest{ + ChannelRequest: &sohMessages.ExecRequest{ Command: strings.Join(command, " "), }, }, @@ -571,7 +571,7 @@ func (c *Client) RunSession(tty *os.File, forwardSSHAgent bool, command ...strin for { n, err := os.Stdin.Read(buf) if n > 0 { - _, err2 := channel.WriteData(buf[:n], ssh3Messages.SSH_EXTENDED_DATA_NONE) + _, err2 := channel.WriteData(buf[:n], sohMessages.SSH_EXTENDED_DATA_NONE) if err2 != nil { fmt.Fprintf(os.Stderr, "could not write data on channel: %+v", err2) return @@ -593,40 +593,40 @@ func (c *Client) RunSession(tty *os.File, forwardSSHAgent bool, command ...strin os.Exit(-1) } switch message := genericMessage.(type) { - case *ssh3Messages.ChannelRequestMessage: + case *sohMessages.ChannelRequestMessage: switch requestMessage := message.ChannelRequest.(type) { - case *ssh3Messages.PtyRequest: + case *sohMessages.PtyRequest: fmt.Fprintf(os.Stderr, "receiving a pty request on the client is not implemented\n") - case *ssh3Messages.X11Request: + case *sohMessages.X11Request: fmt.Fprintf(os.Stderr, "receiving a x11 request on the client is not implemented\n") - case *ssh3Messages.ShellRequest: + case *sohMessages.ShellRequest: fmt.Fprintf(os.Stderr, "receiving a shell request on the client is not implemented\n") - case *ssh3Messages.ExecRequest: + case *sohMessages.ExecRequest: fmt.Fprintf(os.Stderr, "receiving a exec request on the client is not implemented\n") - case *ssh3Messages.SubsystemRequest: + case *sohMessages.SubsystemRequest: fmt.Fprintf(os.Stderr, "receiving a subsystem request on the client is not implemented\n") - case *ssh3Messages.WindowChangeRequest: + case *sohMessages.WindowChangeRequest: fmt.Fprintf(os.Stderr, "receiving a windowchange request on the client is not implemented\n") - case *ssh3Messages.SignalRequest: + case *sohMessages.SignalRequest: fmt.Fprintf(os.Stderr, "receiving a signal request on the client is not implemented\n") - case *ssh3Messages.ExitStatusRequest: - log.Info().Msgf("ssh3: process exited with status: %d\n", requestMessage.ExitStatus) + case *sohMessages.ExitStatusRequest: + log.Info().Msgf("soh: process exited with status: %d\n", requestMessage.ExitStatus) // forward the process' status code to the user return ExitStatus{StatusCode: int(requestMessage.ExitStatus)} - case *ssh3Messages.ExitSignalRequest: - log.Info().Msgf("ssh3: process exited with signal: %s: %s\n", requestMessage.SignalNameWithoutSig, requestMessage.ErrorMessageUTF8) + case *sohMessages.ExitSignalRequest: + log.Info().Msgf("soh: process exited with signal: %s: %s\n", requestMessage.SignalNameWithoutSig, requestMessage.ErrorMessageUTF8) return ExitSignal{Signal: requestMessage.SignalNameWithoutSig, ErrorMessageUTF8: requestMessage.ErrorMessageUTF8} } - case *ssh3Messages.DataOrExtendedDataMessage: + case *sohMessages.DataOrExtendedDataMessage: switch message.DataType { - case ssh3Messages.SSH_EXTENDED_DATA_NONE: + case sohMessages.SSH_EXTENDED_DATA_NONE: _, err = os.Stdout.Write([]byte(message.Data)) if err != nil { log.Fatal().Msgf("%s", err) } log.Trace().Msgf("received data %s", message.Data) - case ssh3Messages.SSH_EXTENDED_DATA_STDERR: + case sohMessages.SSH_EXTENDED_DATA_STDERR: _, err = os.Stderr.Write([]byte(message.Data)) if err != nil { log.Fatal().Msgf("%s", err) diff --git a/client/options.go b/client/options.go index b0d4a4e..291c4c7 100644 --- a/client/options.go +++ b/client/options.go @@ -56,7 +56,7 @@ func (o *Options) UrlPath() string { return o.urlPath } -// Returns the canonical host representation used by SSH3. +// Returns the canonical host representation used by SOH. // The format is // is the host:port pair in the format returned by // URLHostnamePort() diff --git a/client_auth.go b/client_auth.go index 22cc56a..f06984e 100644 --- a/client_auth.go +++ b/client_auth.go @@ -1,4 +1,4 @@ -package ssh3 +package soh import ( "crypto" @@ -11,8 +11,8 @@ import ( "strings" "time" - "github.com/francoismichel/ssh3/auth" - "github.com/francoismichel/ssh3/util" + "github.com/francoismichel/soh/auth" + "github.com/francoismichel/soh/util" "github.com/golang-jwt/jwt/v5" "github.com/kevinburke/ssh_config" @@ -71,7 +71,7 @@ func (m *PrivkeyFileAuthMethod) Filename() string { return m.filename } -// IntoIdentityWithoutPassphrase returns an SSH3 identity stored on the provided path. +// IntoIdentityWithoutPassphrase returns an SOH identity stored on the provided path. // It supports the same keys as ssh.ParsePrivateKey // If the private key is encrypted, it returns an ssh.PassphraseMissingError. func (m *PrivkeyFileAuthMethod) IntoIdentityWithoutPassphrase() (Identity, error) { @@ -136,7 +136,7 @@ func (m *AgentAuthMethod) IntoIdentity(agent agent.ExtendedAgent) Identity { } } -// a generic way to generate SSH3 identities to populate the HTTP Authorization header +// a generic way to generate SOH identities to populate the HTTP Authorization header type Identity interface { SetAuthorizationHeader(req *http.Request, username string, conversation *Conversation) error // provides an authentication name that can be used as a hint for the server in the url query params @@ -302,9 +302,9 @@ func buildJWTBearerToken(signingMethod jwt.SigningMethod, key interface{}, usern "iss": username, "iat": jwt.NewNumericDate(time.Now()), "exp": jwt.NewNumericDate(time.Now().Add(10 * time.Second)), - "sub": "ssh3", + "sub": "soh", "aud": "unused", - "client_id": fmt.Sprintf("ssh3-%s", username), + "client_id": fmt.Sprintf("soh-%s", username), "jti": b64ConvID, }) diff --git a/cmd/ssh3-server/main.go b/cmd/ssh3-server/main.go index cadbb82..d524f06 100644 --- a/cmd/ssh3-server/main.go +++ b/cmd/ssh3-server/main.go @@ -26,11 +26,11 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" - ssh3 "github.com/francoismichel/ssh3" - ssh3Messages "github.com/francoismichel/ssh3/message" - "github.com/francoismichel/ssh3/unix_server" - util "github.com/francoismichel/ssh3/util" - "github.com/francoismichel/ssh3/util/unix_util" + soh "github.com/francoismichel/soh" + sohMessages "github.com/francoismichel/soh/message" + "github.com/francoismichel/soh/unix_server" + util "github.com/francoismichel/soh/util" + "github.com/francoismichel/soh/util/unix_util" ) var signals = map[string]os.Signal{ @@ -99,8 +99,8 @@ type runningSession struct { authAgentSocketPath string } -// var runningSessions = make(map[ssh3.Channel]*runningSession) -var runningSessions = util.NewSyncMap[ssh3.Channel, *runningSession]() +// var runningSessions = make(map[soh.Channel]*runningSession) +var runningSessions = util.NewSyncMap[soh.Channel, *runningSession]() func setWinsize(f *os.File, charWidth, charHeight, pixWidth, pixHeight uint64) { syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), @@ -124,7 +124,7 @@ func setupEnv(user *unix_util.User, runningCommand *runningCommand, authAgentSoc } } -func forwardUDPInBackground(ctx context.Context, channel ssh3.Channel, conn *net.UDPConn) { +func forwardUDPInBackground(ctx context.Context, channel soh.Channel, conn *net.UDPConn) { go func() { defer conn.Close() for { @@ -170,7 +170,7 @@ func forwardUDPInBackground(ctx context.Context, channel ssh3.Channel, conn *net }() } -func forwardTCPInBackground(ctx context.Context, channel ssh3.Channel, conn *net.TCPConn) { +func forwardTCPInBackground(ctx context.Context, channel soh.Channel, conn *net.TCPConn) { go func() { defer conn.CloseWrite() for { @@ -193,8 +193,8 @@ func forwardTCPInBackground(ctx context.Context, channel ssh3.Channel, conn *net } switch message := genericMessage.(type) { - case *ssh3Messages.DataOrExtendedDataMessage: - if message.DataType == ssh3Messages.SSH_EXTENDED_DATA_NONE { + case *sohMessages.DataOrExtendedDataMessage: + if message.DataType == sohMessages.SSH_EXTENDED_DATA_NONE { _, err := conn.Write([]byte(message.Data)) if err != nil { log.Error().Msgf("could not write data on TCP socket: %s", err) @@ -226,7 +226,7 @@ func forwardTCPInBackground(ctx context.Context, channel ssh3.Channel, conn *net log.Error().Msgf("could read data on TCP socket: %s", err) return } - _, errWrite := channel.WriteData(buf[:n], ssh3Messages.SSH_EXTENDED_DATA_NONE) + _, errWrite := channel.WriteData(buf[:n], sohMessages.SSH_EXTENDED_DATA_NONE) if errWrite != nil { switch quicErr := errWrite.(type) { case *quic.StreamError: @@ -247,7 +247,7 @@ func forwardTCPInBackground(ctx context.Context, channel ssh3.Channel, conn *net }() } -func execCmdInBackground(channel ssh3.Channel, openPty *openPty, user *unix_util.User, runningCommand *runningCommand, authAgentSocketPath string) error { +func execCmdInBackground(channel soh.Channel, openPty *openPty, user *unix_util.User, runningCommand *runningCommand, authAgentSocketPath string) error { setupEnv(user, runningCommand, authAgentSocketPath) if openPty != nil { err := unix_util.StartWithSizeAndPty(&runningCommand.Cmd, openPty.winSize, openPty.pty, openPty.tty) @@ -320,7 +320,7 @@ func execCmdInBackground(channel ssh3.Channel, openPty *openPty, user *unix_util } else { buf, err := stdoutResult.data, stdoutResult.err // an error could be returned but still with relevant data, so first send the data - _, err2 := channel.WriteData(buf, ssh3Messages.SSH_EXTENDED_DATA_NONE) + _, err2 := channel.WriteData(buf, sohMessages.SSH_EXTENDED_DATA_NONE) if err2 != nil { log.Error().Msgf("could not write the pty's output in an SSH message: %+v\n", err) return @@ -336,7 +336,7 @@ func execCmdInBackground(channel ssh3.Channel, openPty *openPty, user *unix_util stderrChan = nil } else { buf, err := stderrResult.data, stderrResult.err - _, err2 := channel.WriteData(buf, ssh3Messages.SSH_EXTENDED_DATA_STDERR) + _, err2 := channel.WriteData(buf, sohMessages.SSH_EXTENDED_DATA_STDERR) if err2 != nil { log.Error().Msgf("could not write the pty's output in an SSH message: %+v\n", err) return @@ -360,9 +360,9 @@ func execCmdInBackground(channel ssh3.Channel, openPty *openPty, user *unix_util } } if stdoutChan == nil && stderrChan == nil && execResultChan == nil { - err := channel.SendRequest(&ssh3Messages.ChannelRequestMessage{ + err := channel.SendRequest(&sohMessages.ChannelRequestMessage{ WantReply: false, - ChannelRequest: &ssh3Messages.ExitStatusRequest{ExitStatus: execExitStatus}, + ChannelRequest: &sohMessages.ExitStatusRequest{ExitStatus: execExitStatus}, }) if err != nil { log.Error().Msgf("Could not send exit status message to the peer: %s", err) @@ -375,7 +375,7 @@ func execCmdInBackground(channel ssh3.Channel, openPty *openPty, user *unix_util return nil } -func newPtyReq(user *unix_util.User, channel ssh3.Channel, request ssh3Messages.PtyRequest, wantReply bool) error { +func newPtyReq(user *unix_util.User, channel soh.Channel, request sohMessages.PtyRequest, wantReply bool) error { var session *runningSession session, ok := runningSessions.Get(channel) if !ok { @@ -407,11 +407,11 @@ func newPtyReq(user *unix_util.User, channel ssh3.Channel, request ssh3Messages. return nil } -func newX11Req(user *unix_util.User, channel ssh3.Channel, request ssh3Messages.X11Request, wantReply bool) error { +func newX11Req(user *unix_util.User, channel soh.Channel, request sohMessages.X11Request, wantReply bool) error { return fmt.Errorf("%T not implemented", request) } -func newCommand(user *unix_util.User, channel ssh3.Channel, loginShell bool, command string, args ...string) error { +func newCommand(user *unix_util.User, channel soh.Channel, loginShell bool, command string, args ...string) error { var session *runningSession session, ok := runningSessions.Get(channel) if !ok { @@ -475,24 +475,24 @@ func newCommand(user *unix_util.User, channel ssh3.Channel, loginShell bool, com return execCmdInBackground(channel, session.pty, user, session.runningCmd, session.authAgentSocketPath) } -func newShellReq(user *unix_util.User, channel ssh3.Channel, wantReply bool) error { +func newShellReq(user *unix_util.User, channel soh.Channel, wantReply bool) error { return newCommand(user, channel, true, user.Shell) } // similar behaviour to OpenSSH; exec requests are just pasted in the user's shell -func newCommandInShellReq(user *unix_util.User, channel ssh3.Channel, wantReply bool, command string) error { +func newCommandInShellReq(user *unix_util.User, channel soh.Channel, wantReply bool, command string) error { return newCommand(user, channel, false, user.Shell, "-c", command) } -func newSubsystemReq(user *unix_util.User, channel ssh3.Channel, request ssh3Messages.SubsystemRequest, wantReply bool) error { +func newSubsystemReq(user *unix_util.User, channel soh.Channel, request sohMessages.SubsystemRequest, wantReply bool) error { return fmt.Errorf("%T not implemented", request) } -func newWindowChangeReq(user *unix_util.User, channel ssh3.Channel, request ssh3Messages.WindowChangeRequest, wantReply bool) error { +func newWindowChangeReq(user *unix_util.User, channel soh.Channel, request sohMessages.WindowChangeRequest, wantReply bool) error { return fmt.Errorf("%T not implemented", request) } -func newSignalReq(user *unix_util.User, channel ssh3.Channel, request ssh3Messages.SignalRequest, wantReply bool) error { +func newSignalReq(user *unix_util.User, channel soh.Channel, request sohMessages.SignalRequest, wantReply bool) error { runningSession, ok := runningSessions.Get(channel) if !ok { return fmt.Errorf("could not find running session for channel %d (conv %d)", channel.ChannelID(), channel.ConversationID()) @@ -518,15 +518,15 @@ func newSignalReq(user *unix_util.User, channel ssh3.Channel, request ssh3Messag return nil } -func newExitStatusReq(user *unix_util.User, channel ssh3.Channel, request ssh3Messages.ExitStatusRequest, wantReply bool) error { +func newExitStatusReq(user *unix_util.User, channel soh.Channel, request sohMessages.ExitStatusRequest, wantReply bool) error { return fmt.Errorf("%T not implemented", request) } -func newExitSignalReq(user *unix_util.User, channel ssh3.Channel, request ssh3Messages.ExitSignalRequest, wantReply bool) error { +func newExitSignalReq(user *unix_util.User, channel soh.Channel, request sohMessages.ExitSignalRequest, wantReply bool) error { return fmt.Errorf("%T not implemented", request) } -func handleUDPForwardingChannel(ctx context.Context, user *unix_util.User, conv *ssh3.Conversation, channel *ssh3.UDPForwardingChannelImpl) error { +func handleUDPForwardingChannel(ctx context.Context, user *unix_util.User, conv *soh.Conversation, channel *soh.UDPForwardingChannelImpl) error { // TODO: currently, the rights for socket creation are not checked. The socket is opened with the process's uid and gid // Not sure how to handled that in go since we cannot temporarily change the uid/gid without potentially impacting every // other goroutine @@ -538,7 +538,7 @@ func handleUDPForwardingChannel(ctx context.Context, user *unix_util.User, conv return nil } -func handleTCPForwardingChannel(ctx context.Context, user *unix_util.User, conv *ssh3.Conversation, channel *ssh3.TCPForwardingChannelImpl) error { +func handleTCPForwardingChannel(ctx context.Context, user *unix_util.User, conv *soh.Conversation, channel *soh.TCPForwardingChannelImpl) error { // TODO: currently, the rights for socket creation are not checked. The socket is opened with the process's uid and gid // Not sure how to handled that in go since we cannot temporarily change the uid/gid without potentially impacting every // other goroutine @@ -550,7 +550,7 @@ func handleTCPForwardingChannel(ctx context.Context, user *unix_util.User, conv return nil } -func newDataReq(user *unix_util.User, channel ssh3.Channel, request ssh3Messages.DataOrExtendedDataMessage) error { +func newDataReq(user *unix_util.User, channel soh.Channel, request sohMessages.DataOrExtendedDataMessage) error { runningSession, ok := runningSessions.Get(channel) if !ok { return fmt.Errorf("could not find running session for channel %d (conv %d)", channel.ChannelID(), channel.ConversationID()) @@ -566,7 +566,7 @@ func newDataReq(user *unix_util.User, channel ssh3.Channel, request ssh3Messages return fmt.Errorf("there is no running command on Channel %d (conv %d) to feed the received data", channel.ChannelID(), channel.ConversationID()) } switch request.DataType { - case ssh3Messages.SSH_EXTENDED_DATA_NONE: + case sohMessages.SSH_EXTENDED_DATA_NONE: runningSession.runningCmd.stdinW.Write([]byte(request.Data)) default: return fmt.Errorf("extended data type forbidden server PTY") @@ -577,7 +577,7 @@ func newDataReq(user *unix_util.User, channel ssh3.Channel, request ssh3Messages return nil } -func handleAuthAgentSocketConn(conn net.Conn, conversation *ssh3.Conversation) { +func handleAuthAgentSocketConn(conn net.Conn, conversation *soh.Conversation) { channel, err := conversation.OpenChannel("agent-connection", 30000, 10) if err != nil { log.Error().Msgf("could not open channel from server: %s", err.Error()) @@ -592,7 +592,7 @@ func handleAuthAgentSocketConn(conn net.Conn, conversation *ssh3.Conversation) { log.Info().Msgf("could not read data socket %d: %s", channel.ChannelID(), err.Error()) return } - _, err = channel.WriteData(buf[:n], ssh3Messages.SSH_EXTENDED_DATA_NONE) + _, err = channel.WriteData(buf[:n], sohMessages.SSH_EXTENDED_DATA_NONE) if err != nil { log.Info().Msgf("could not write data on agent channel %d: %s", channel.ChannelID(), err.Error()) return @@ -606,7 +606,7 @@ func handleAuthAgentSocketConn(conn net.Conn, conversation *ssh3.Conversation) { return } switch message := genericMessage.(type) { - case *ssh3Messages.DataOrExtendedDataMessage: + case *sohMessages.DataOrExtendedDataMessage: _, err := conn.Write([]byte(message.Data)) if err != nil { log.Error().Msgf("could not write data to channel %d: %s", channel.ChannelID(), err.Error()) @@ -619,7 +619,7 @@ func handleAuthAgentSocketConn(conn net.Conn, conversation *ssh3.Conversation) { } } -func listenAndAcceptAuthSockets(cancel context.CancelCauseFunc, conversation *ssh3.Conversation, listener net.Listener, maxSSHPacketSize uint64) { +func listenAndAcceptAuthSockets(cancel context.CancelCauseFunc, conversation *soh.Conversation, listener net.Listener, maxSSHPacketSize uint64) { defer cancel(nil) defer listener.Close() for { @@ -635,7 +635,7 @@ func listenAndAcceptAuthSockets(cancel context.CancelCauseFunc, conversation *ss } } -func openAgentSocketAndForwardAgent(parent context.Context, conv *ssh3.Conversation, user *unix_util.User) (string, error) { +func openAgentSocketAndForwardAgent(parent context.Context, conv *soh.Conversation, user *unix_util.User) (string, error) { ctx, cancel := context.WithCancelCause(parent) sockPath, err := unix_util.NewUnixSocketPath() if err != nil { @@ -674,7 +674,7 @@ func main() { bindAddr := flag.String("bind", "[::]:443", "the address:port pair to listen to, e.g. 0.0.0.0:443") verbose := flag.Bool("v", false, "verbose mode, if set") displayVersion := flag.Bool("version", false, "if set, displays the software version on standard output and exit") - urlPath := flag.String("url-path", "/ssh3-term", "the secret URL path on which the ssh3 server listens") + urlPath := flag.String("url-path", "/soh-term", "the secret URL path on which the soh server listens") generateSelfSignedCert := flag.Bool("generate-selfsigned-cert", false, "if set, generates a self-self-signed cerificate and key "+ "that will be stored at the paths indicated by the -cert and -key args (they must not already exist)") certPath := flag.String("cert", "./cert.pem", "the filename of the server certificate (or fullchain)") @@ -686,7 +686,7 @@ func main() { flag.Parse() if *displayVersion { - fmt.Fprintln(os.Stdout, filepath.Base(os.Args[0]), "version", ssh3.GetCurrentSoftwareVersion()) + fmt.Fprintln(os.Stdout, filepath.Base(os.Args[0]), "version", soh.GetCurrentSoftwareVersion()) os.Exit(0) } @@ -707,7 +707,7 @@ func main() { if !certPathExists || !keyPathExists { fmt.Fprintln(os.Stderr, "If you have no certificate and want a security comparable to traditional SSH host keys, "+ "you can generate a self-signed certificate using the -generate-selfsigned-cert arg or using the following script:") - fmt.Fprintln(os.Stderr, "https://github.com/francoismichel/ssh3/blob/main/generate_openssl_selfsigned_certificate.sh") + fmt.Fprintln(os.Stderr, "https://github.com/francoismichel/soh/blob/main/generate_openssl_selfsigned_certificate.sh") os.Exit(-1) } } else { @@ -743,11 +743,11 @@ func main() { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) util.ConfigureLogger("debug") } else { - util.ConfigureLogger(os.Getenv("SSH3_LOG_LEVEL")) + util.ConfigureLogger(os.Getenv("SOH_LOG_LEVEL")) - logFileName := os.Getenv("SSH3_LOG_FILE") + logFileName := os.Getenv("SOH_LOG_FILE") if logFileName == "" { - logFileName = "/var/log/ssh3.log" + logFileName = "/var/log/soh.log" } logFile, err := os.OpenFile(logFileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { @@ -757,7 +757,7 @@ func main() { log.Logger = log.Output(logFile) } - log.Debug().Msgf("version %s", ssh3.GetCurrentSoftwareVersion()) + log.Debug().Msgf("version %s", soh.GetCurrentSoftwareVersion()) quicConf := &quic.Config{ Allow0RTT: true, @@ -776,7 +776,7 @@ func main() { } mux := http.NewServeMux() - ssh3Server := ssh3.NewServer(30000, 10, &server, func(authenticatedUsername string, conv *ssh3.Conversation) error { + sohServer := soh.NewServer(30000, 10, &server, func(authenticatedUsername string, conv *soh.Conversation) error { authenticatedUser, err := unix_util.GetUser(authenticatedUsername) if err != nil { return err @@ -788,9 +788,9 @@ func main() { } switch c := channel.(type) { - case *ssh3.UDPForwardingChannelImpl: + case *soh.UDPForwardingChannelImpl: handleUDPForwardingChannel(conv.Context(), authenticatedUser, conv, c) - case *ssh3.TCPForwardingChannelImpl: + case *soh.TCPForwardingChannelImpl: handleTCPForwardingChannel(conv.Context(), authenticatedUser, conv, c) default: runningSessions.Insert(channel, &runningSession{ @@ -815,28 +815,28 @@ func main() { return } switch message := genericMessage.(type) { - case *ssh3Messages.ChannelRequestMessage: + case *sohMessages.ChannelRequestMessage: switch requestMessage := message.ChannelRequest.(type) { - case *ssh3Messages.PtyRequest: + case *sohMessages.PtyRequest: err = newPtyReq(authenticatedUser, channel, *requestMessage, message.WantReply) - case *ssh3Messages.X11Request: + case *sohMessages.X11Request: err = newX11Req(authenticatedUser, channel, *requestMessage, message.WantReply) - case *ssh3Messages.ShellRequest: + case *sohMessages.ShellRequest: err = newShellReq(authenticatedUser, channel, message.WantReply) - case *ssh3Messages.ExecRequest: + case *sohMessages.ExecRequest: err = newCommandInShellReq(authenticatedUser, channel, message.WantReply, requestMessage.Command) - case *ssh3Messages.SubsystemRequest: + case *sohMessages.SubsystemRequest: err = newSubsystemReq(authenticatedUser, channel, *requestMessage, message.WantReply) - case *ssh3Messages.WindowChangeRequest: + case *sohMessages.WindowChangeRequest: err = newWindowChangeReq(authenticatedUser, channel, *requestMessage, message.WantReply) - case *ssh3Messages.SignalRequest: + case *sohMessages.SignalRequest: err = newSignalReq(authenticatedUser, channel, *requestMessage, message.WantReply) - case *ssh3Messages.ExitStatusRequest: + case *sohMessages.ExitStatusRequest: err = newExitStatusReq(authenticatedUser, channel, *requestMessage, message.WantReply) - case *ssh3Messages.ExitSignalRequest: + case *sohMessages.ExitSignalRequest: err = newExitSignalReq(authenticatedUser, channel, *requestMessage, message.WantReply) } - case *ssh3Messages.DataOrExtendedDataMessage: + case *sohMessages.DataOrExtendedDataMessage: runningSession, ok := runningSessions.Get(channel) if ok && runningSession.channelState == LARVAL { if message.Data == string("forward-agent") { @@ -859,8 +859,8 @@ func main() { } }) - ssh3Handler := ssh3Server.GetHTTPHandlerFunc(context.Background()) - handler, err := unix_server.HandleAuths(context.Background(), enablePasswordLogin, 30000, ssh3Handler) + sohHandler := sohServer.GetHTTPHandlerFunc(context.Background()) + handler, err := unix_server.HandleAuths(context.Background(), enablePasswordLogin, 30000, sohHandler) if err != nil { log.Error().Msgf("Could not get authentication handlers: %s", err) return diff --git a/cmd/ssh3/main.go b/cmd/ssh3/main.go index 1a923f7..434b2ff 100644 --- a/cmd/ssh3/main.go +++ b/cmd/ssh3/main.go @@ -20,10 +20,10 @@ import ( "strings" "time" - "github.com/francoismichel/ssh3" - "github.com/francoismichel/ssh3/auth" - "github.com/francoismichel/ssh3/client" - "github.com/francoismichel/ssh3/util" + "github.com/francoismichel/soh" + "github.com/francoismichel/soh/auth" + "github.com/francoismichel/soh/client" + "github.com/francoismichel/soh/util" "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/http3" "golang.org/x/crypto/ssh" @@ -43,10 +43,10 @@ func homedir() string { } } -// Prepares the QUIC connection that will be used by SSH3 +// Prepares the QUIC connection that will be used by SOH // If non-nil, use udpConn as transport (can be used for proxy jump) // Otherwise, create a UDPConn from udp://host:port -func setupQUICConnection(ctx context.Context, skipHostVerification bool, keylog io.Writer, ssh3Dir string, certPool *x509.CertPool, knownHostsPath string, knownHosts ssh3.KnownHosts, +func setupQUICConnection(ctx context.Context, skipHostVerification bool, keylog io.Writer, sohDir string, certPool *x509.CertPool, knownHostsPath string, knownHosts soh.KnownHosts, oidcConfig []*auth.OIDCConfig, options *client.Options, proxyRemoteAddr *net.UDPAddr, tty *os.File) (quic.EarlyConnection, int) { var err error @@ -81,21 +81,21 @@ func setupQUICConnection(ctx context.Context, skipHostVerification bool, keylog qconf.KeepAlivePeriod = 1 * time.Second if certs, ok := knownHosts[options.CanonicalHostFormat()]; ok { - foundSelfsignedSSH3 := false + foundSelfsignedSOH := false for _, cert := range certs { certPool.AddCert(cert) - if cert.VerifyHostname("selfsigned.ssh3") == nil { - foundSelfsignedSSH3 = true + if cert.VerifyHostname("selfsigned.soh") == nil { + foundSelfsignedSOH = true } } - // If no IP SAN was in the cert, then assume the self-signed cert at least matches the .ssh3 TLD - if foundSelfsignedSSH3 { - // Put "ssh3" as ServerName so that the TLS verification can succeed + // If no IP SAN was in the cert, then assume the self-signed cert at least matches the .soh TLD + if foundSelfsignedSOH { + // Put "soh" as ServerName so that the TLS verification can succeed // Otherwise, TLS refuses to validate a certificate without IP SANs // if the hostname is an IP address. - tlsConf.ServerName = "selfsigned.ssh3" + tlsConf.ServerName = "selfsigned.soh" } } @@ -150,7 +150,7 @@ func setupQUICConnection(ctx context.Context, skipHostVerification bool, keylog "This session is vulnerable a machine-in-the-middle attack.\n\r" + "Certificate fingerprint: " + "SHA256 " + util.Sha256Fingerprint(peerCertificate.Raw) + "\n\r" + - "Do you want to add this certificate to ~/.ssh3/known_hosts (yes/no)? ") + "Do you want to add this certificate to ~/.soh/known_hosts (yes/no)? ") if err != nil { log.Error().Msgf("cound not write on /dev/tty: %s", err) return nil, -1 @@ -171,7 +171,7 @@ func setupQUICConnection(ctx context.Context, skipHostVerification bool, keylog log.Info().Msg("Connection aborted") return nil, 0 } - if err := ssh3.AppendKnownHost(knownHostsPath, options.CanonicalHostFormat(), peerCertificate); err != nil { + if err := soh.AppendKnownHost(knownHostsPath, options.CanonicalHostFormat(), peerCertificate); err != nil { log.Error().Msgf("could not append known host to %s: %s", knownHostsPath, err) return nil, -1 } @@ -211,7 +211,7 @@ func parseAddrPort(addrPort string) (localPort int, remoteIP net.IP, remotePort func getConfigOptions(hostUrl *url.URL, sshConfig *ssh_config.Config) (*client.Options, error) { urlHostname, urlPort := hostUrl.Hostname(), hostUrl.Port() - configHostname, configPort, configUser, configAuthMethods, err := ssh3.GetConfigForHost(urlHostname, sshConfig) + configHostname, configPort, configUser, configAuthMethods, err := soh.GetConfigForHost(urlHostname, sshConfig) if err != nil { log.Error().Msgf("could not get config for %s: %s", urlHostname, err) return nil, err @@ -307,20 +307,20 @@ func mainWithStatusCode() int { args := flag.Args() if *displayVersion { - fmt.Fprintln(os.Stdout, filepath.Base(os.Args[0]), "version", ssh3.GetCurrentSoftwareVersion()) + fmt.Fprintln(os.Stdout, filepath.Base(os.Args[0]), "version", soh.GetCurrentSoftwareVersion()) return 0 } useOIDC := *issuerUrl != "" - ssh3Dir := path.Join(homedir(), ".ssh3") - os.MkdirAll(ssh3Dir, 0700) + sohDir := path.Join(homedir(), ".soh") + os.MkdirAll(sohDir, 0700) log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) - if *verbose && os.Getenv("SSH3_LOG_LEVEL") != "trace" { + if *verbose && os.Getenv("SOH_LOG_LEVEL") != "trace" { util.ConfigureLogger("debug") } else { - util.ConfigureLogger(os.Getenv("SSH3_LOG_LEVEL")) + util.ConfigureLogger(os.Getenv("SOH_LOG_LEVEL")) } if len(args) == 0 { @@ -329,10 +329,10 @@ func mainWithStatusCode() int { os.Exit(-1) } - log.Debug().Msgf("version %s", ssh3.GetCurrentSoftwareVersion()) + log.Debug().Msgf("version %s", soh.GetCurrentSoftwareVersion()) - knownHostsPath := path.Join(ssh3Dir, "known_hosts") - knownHosts, skippedLines, err := ssh3.ParseKnownHosts(knownHostsPath) + knownHostsPath := path.Join(sohDir, "known_hosts") + knownHosts, skippedLines, err := soh.ParseKnownHosts(knownHostsPath) if len(skippedLines) != 0 { stringSkippedLines := []string{} for _, lineNumber := range skippedLines { @@ -427,7 +427,7 @@ func mainWithStatusCode() int { var oidcConfig auth.OIDCIssuerConfig = nil var oidcConfigFile *os.File = nil if *oidcConfigFileName == "" { - defaultFileName := path.Join(ssh3Dir, "oidc_config.json") + defaultFileName := path.Join(sohDir, "oidc_config.json") log.Debug().Msgf("no OIDC config file specified, use default file: %s", defaultFileName) oidcConfigFile, err = os.Open(defaultFileName) if os.IsNotExist(err) { @@ -471,7 +471,7 @@ func mainWithStatusCode() int { // Only do privkey and agent auth if OIDC is not asked explicitly if !useOIDC { if *privKeyFile != "" { - cliAuthMethods = append(cliAuthMethods, ssh3.NewPrivkeyFileAuthMethod(*privKeyFile)) + cliAuthMethods = append(cliAuthMethods, soh.NewPrivkeyFileAuthMethod(*privKeyFile)) } if *pubkeyForAgent != "" { @@ -490,13 +490,13 @@ func mainWithStatusCode() int { log.Error().Msgf("could not parse public key: %s", err) return -1 } - cliAuthMethods = append(cliAuthMethods, ssh3.NewAgentAuthMethod(pubkey)) + cliAuthMethods = append(cliAuthMethods, soh.NewAgentAuthMethod(pubkey)) } } } if *passwordAuthentication { - cliAuthMethods = append(cliAuthMethods, ssh3.NewPasswordAuthMethod()) + cliAuthMethods = append(cliAuthMethods, soh.NewPasswordAuthMethod()) } } else { @@ -506,7 +506,7 @@ func mainWithStatusCode() int { for _, issuerConfig := range oidcConfig { if *issuerUrl == issuerConfig.IssuerUrl { log.Debug().Msgf("found issuer %s matching the issuer specified in the command-line", issuerConfig.IssuerUrl) - cliAuthMethods = append(cliAuthMethods, ssh3.NewOidcAuthMethod(*doPKCE, issuerConfig)) + cliAuthMethods = append(cliAuthMethods, soh.NewOidcAuthMethod(*doPKCE, issuerConfig)) } else { log.Debug().Msgf("issuer %s does not match issuer URL %s specified in the command-line", issuerConfig.IssuerUrl, *issuerUrl) } @@ -559,7 +559,7 @@ func mainWithStatusCode() int { log.Error().Msgf("Could not get connection material for proxy %s: %s", proxyParsedUrl, err) return -1 } - qconn, status := setupQUICConnection(ctx, *insecure, keyLog, ssh3Dir, pool, knownHostsPath, knownHosts, oidcConfig, proxyOptions, nil, tty) + qconn, status := setupQUICConnection(ctx, *insecure, keyLog, sohDir, pool, knownHostsPath, knownHosts, oidcConfig, proxyOptions, nil, tty) if qconn == nil { if status != 0 { @@ -574,7 +574,7 @@ func mainWithStatusCode() int { proxyClient, err := client.Dial(ctx, proxyOptions, qconn, roundTripper, proxyAgentClient) if err != nil { - log.Error().Msgf("could not establish SSH3 proxy conversation: %s", err) + log.Error().Msgf("could not establish SOH proxy conversation: %s", err) return -1 } @@ -597,7 +597,7 @@ func mainWithStatusCode() int { log.Debug().Msgf("started proxy jump at %s", proxyAddress) } - qconn, status := setupQUICConnection(ctx, *insecure, keyLog, ssh3Dir, pool, knownHostsPath, knownHosts, oidcConfig, options, proxyAddress, tty) + qconn, status := setupQUICConnection(ctx, *insecure, keyLog, sohDir, pool, knownHostsPath, knownHosts, oidcConfig, options, proxyAddress, tty) if qconn == nil { if status != 0 { diff --git a/conversation.go b/conversation.go index 252564b..2ffc30e 100644 --- a/conversation.go +++ b/conversation.go @@ -1,4 +1,4 @@ -package ssh3 +package soh import ( "bytes" @@ -10,7 +10,7 @@ import ( "net" "net/http" - "github.com/francoismichel/ssh3/util" + "github.com/francoismichel/soh/util" "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/http3" @@ -40,7 +40,7 @@ type Conversation struct { } func GenerateConversationID(tls *tls.ConnectionState) (convID ConversationID, err error) { - ret, err := tls.ExportKeyingMaterial("EXPORTER-SSH3", nil, 32) + ret, err := tls.ExportKeyingMaterial("EXPORTER-SOH", nil, 32) if err != nil { return convID, err } diff --git a/generate_openssl_selfsigned_certificate.sh b/generate_openssl_selfsigned_certificate.sh index 4fba441..a30cd77 100644 --- a/generate_openssl_selfsigned_certificate.sh +++ b/generate_openssl_selfsigned_certificate.sh @@ -4,4 +4,4 @@ # however, if you want a comparable security level to OpenSSH's host keys, # you can use this script to generate a self-signed certificate for every host # and every IP address to install on your server -openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout priv.key -days 3660 -out cert.pem -subj "/C=XX/O=Default Company/OU=XX/CN=selfsigned.ssh3" -addext "subjectAltName = DNS:selfsigned.ssh3,DNS:*" \ No newline at end of file +openssl req -x509 -sha256 -nodes -newkey rsa:4096 -keyout priv.key -days 3660 -out cert.pem -subj "/C=XX/O=Default Company/OU=XX/CN=selfsigned.soh" -addext "subjectAltName = DNS:selfsigned.soh,DNS:*" \ No newline at end of file diff --git a/go.mod b/go.mod index 60dfc0a..7e6f40b 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/francoismichel/ssh3 +module github.com/francoismichel/soh require ( github.com/coreos/go-oidc/v3 v3.7.0 diff --git a/integration_tests/ssh3_test.go b/integration_tests/soh_test.go similarity index 90% rename from integration_tests/ssh3_test.go rename to integration_tests/soh_test.go index 048f73a..17b61a5 100644 --- a/integration_tests/ssh3_test.go +++ b/integration_tests/soh_test.go @@ -15,11 +15,11 @@ import ( . "github.com/onsi/gomega/gexec" ) -var ssh3Path string -var ssh3ServerPath string +var sohPath string +var sohServerPath string -const DEFAULT_URL_PATH = "/ssh3-tests" -const DEFAULT_PROXY_URL_PATH = "/ssh3-tests-proxy" +const DEFAULT_URL_PATH = "/soh-tests" +const DEFAULT_PROXY_URL_PATH = "/soh-tests-proxy" var serverCommand *exec.Cmd var serverSession *Session @@ -52,34 +52,34 @@ func fileExists(path string) bool { var _ = BeforeSuite(func() { var err error - ssh3Path, err = Build("../cmd/ssh3/main.go") + sohPath, err = Build("../cmd/soh/main.go") Expect(err).ToNot(HaveOccurred()) - if os.Getenv("SSH3_INTEGRATION_TESTS_WITH_SERVER_ENABLED") == "1" { + if os.Getenv("SOH_INTEGRATION_TESTS_WITH_SERVER_ENABLED") == "1" { // Tests implying a server will only work on Linux // (the server currently only builds on Linux) // and the server needs root priviledges, so we only // run them is they are enabled explicitly. - ssh3ServerPath, err = BuildWithEnvironment("../cmd/ssh3-server/main.go", []string{fmt.Sprintf("CGO_ENABLED=%s", os.Getenv("CGO_ENABLED"))}) + sohServerPath, err = BuildWithEnvironment("../cmd/soh-server/main.go", []string{fmt.Sprintf("CGO_ENABLED=%s", os.Getenv("CGO_ENABLED"))}) Expect(err).ToNot(HaveOccurred()) - serverCommand = exec.Command(ssh3ServerPath, + serverCommand = exec.Command(sohServerPath, "-bind", serverBind, "-v", "-enable-password-login", "-url-path", DEFAULT_URL_PATH, "-cert", os.Getenv("CERT_PEM"), "-key", os.Getenv("CERT_PRIV_KEY")) - serverCommand.Env = append(serverCommand.Env, "SSH3_LOG_LEVEL=debug") + serverCommand.Env = append(serverCommand.Env, "SOH_LOG_LEVEL=debug") serverSession, err = Start(serverCommand, GinkgoWriter, GinkgoWriter) Expect(err).ToNot(HaveOccurred()) - proxyServerCommand = exec.Command(ssh3ServerPath, + proxyServerCommand = exec.Command(sohServerPath, "-bind", proxyServerBind, "-v", "-enable-password-login", "-url-path", DEFAULT_PROXY_URL_PATH, "-cert", os.Getenv("CERT_PEM"), "-key", os.Getenv("CERT_PRIV_KEY")) - proxyServerCommand.Env = append(proxyServerCommand.Env, "SSH3_LOG_LEVEL=debug") + proxyServerCommand.Env = append(proxyServerCommand.Env, "SOH_LOG_LEVEL=debug") proxyServerSession, err = Start(proxyServerCommand, GinkgoWriter, GinkgoWriter) Expect(err).ToNot(HaveOccurred()) @@ -101,11 +101,11 @@ var _ = AfterSuite(func() { } }) -var _ = Describe("Testing the ssh3 cli", func() { +var _ = Describe("Testing the soh cli", func() { Context("Usage", func() { It("Displays the help", func() { - command := exec.Command(ssh3Path, "-h") + command := exec.Command(sohPath, "-h") session, err := Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ToNot(HaveOccurred()) Eventually(session).Should(Exit(0)) @@ -115,7 +115,7 @@ var _ = Describe("Testing the ssh3 cli", func() { Context("With running server", func() { BeforeEach(func() { - if os.Getenv("SSH3_INTEGRATION_TESTS_WITH_SERVER_ENABLED") != "1" { + if os.Getenv("SOH_INTEGRATION_TESTS_WITH_SERVER_ENABLED") != "1" { Skip("skipping integration tests") } Consistently(serverSession, "200ms").ShouldNot(Exit()) @@ -137,7 +137,7 @@ var _ = Describe("Testing the ssh3 cli", func() { Context("Client behaviour", func() { It("Should connect using an RSA privkey", func() { clientArgs = append(getClientArgs(rsaPrivKeyPath), "echo", "Hello, World!") - command := exec.Command(ssh3Path, clientArgs...) + command := exec.Command(sohPath, clientArgs...) session, err := Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ToNot(HaveOccurred()) Eventually(session).Should(Exit(0)) @@ -146,7 +146,7 @@ var _ = Describe("Testing the ssh3 cli", func() { It("Should connect using an RSA privkey through proxy jump", func() { clientArgs = append(getClientArgs(rsaPrivKeyPath, "-proxy-jump", fmt.Sprintf("%s@%s%s", username, proxyServerBind, DEFAULT_PROXY_URL_PATH)), "echo", "Hello, World!") - command := exec.Command(ssh3Path, clientArgs...) + command := exec.Command(sohPath, clientArgs...) session, err := Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ToNot(HaveOccurred()) Eventually(session).Should(Exit(0)) @@ -155,7 +155,7 @@ var _ = Describe("Testing the ssh3 cli", func() { It("Should connect using an ed25519 privkey", func() { clientArgs = append(getClientArgs(ed25519PrivKeyPath), "echo", "Hello, World!") - command := exec.Command(ssh3Path, clientArgs...) + command := exec.Command(sohPath, clientArgs...) session, err := Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ToNot(HaveOccurred()) Eventually(session).Should(Exit(0)) @@ -168,22 +168,22 @@ var _ = Describe("Testing the ssh3 cli", func() { clientArgs255 := append(getClientArgs(rsaPrivKeyPath), "exit", "255") clientArgsMinus1 := append(getClientArgs(rsaPrivKeyPath), "exit", "-1") - command0 := exec.Command(ssh3Path, clientArgs0...) + command0 := exec.Command(sohPath, clientArgs0...) session, err := Start(command0, GinkgoWriter, GinkgoWriter) Expect(err).ToNot(HaveOccurred()) Eventually(session).Should(Exit(0)) - command1 := exec.Command(ssh3Path, clientArgs1...) + command1 := exec.Command(sohPath, clientArgs1...) session, err = Start(command1, GinkgoWriter, GinkgoWriter) Expect(err).ToNot(HaveOccurred()) Eventually(session).Should(Exit(1)) - command255 := exec.Command(ssh3Path, clientArgs255...) + command255 := exec.Command(sohPath, clientArgs255...) session, err = Start(command255, GinkgoWriter, GinkgoWriter) Expect(err).ToNot(HaveOccurred()) Eventually(session).Should(Exit(255)) - commandMinus1 := exec.Command(ssh3Path, clientArgsMinus1...) + commandMinus1 := exec.Command(sohPath, clientArgsMinus1...) session, err = Start(commandMinus1, GinkgoWriter, GinkgoWriter) Expect(err).ToNot(HaveOccurred()) Eventually(session).Should(Exit(255)) @@ -191,7 +191,7 @@ var _ = Describe("Testing the ssh3 cli", func() { It("Should run the interactive shell in login mode and read .profile", func() { clientArgs = getClientArgs(rsaPrivKeyPath) - command := exec.Command(ssh3Path, clientArgs...) + command := exec.Command(sohPath, clientArgs...) stdin, err := command.StdinPipe() Expect(err).ToNot(HaveOccurred()) session, err := Start(command, GinkgoWriter, GinkgoWriter) @@ -205,7 +205,7 @@ var _ = Describe("Testing the ssh3 cli", func() { // It checks that upon executing the client with the -forward-tcp, // a TCP socket is indeed well open on the client and is indeed forwarded - // through the SSH3 connection towards the specified remote IP and port. + // through the SOH connection towards the specified remote IP and port. Context("TCP port forwarding", func() { testTCPPortForwarding := func(localPort uint16, proxyJump bool, remoteAddr *net.TCPAddr, messageFromClient string, messageFromServer string) { localIP := "[::1]" @@ -253,7 +253,7 @@ var _ = Describe("Testing the ssh3 cli", func() { } additionalArgs = append(additionalArgs, "-forward-tcp", fmt.Sprintf("%d/%s@%d", localPort, remoteAddr.IP, remoteAddr.Port)) clientArgs := getClientArgs(rsaPrivKeyPath, additionalArgs...) - command := exec.Command(ssh3Path, clientArgs...) + command := exec.Command(sohPath, clientArgs...) session, err := Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ToNot(HaveOccurred()) defer session.Terminate() @@ -329,7 +329,7 @@ var _ = Describe("Testing the ssh3 cli", func() { // It checks that upon executing the client with the -forward-udp, // a UDP socket is indeed well open on the client and is indeed forwarded - // through the SSH3 connection towards the specified remote IP and port. + // through the SOH connection towards the specified remote IP and port. Context("UDP port forwarding", func() { testUDPPortForwarding := func(localPort uint16, proxyJump bool, remoteAddr *net.UDPAddr, messageFromClient, messageFromServer string) { localIP := "[::1]" @@ -369,7 +369,7 @@ var _ = Describe("Testing the ssh3 cli", func() { } additionalArgs = append(additionalArgs, "-forward-udp", fmt.Sprintf("%d/%s@%d", localPort, remoteAddr.IP, remoteAddr.Port)) clientArgs := getClientArgs(rsaPrivKeyPath, additionalArgs...) - command := exec.Command(ssh3Path, clientArgs...) + command := exec.Command(sohPath, clientArgs...) session, err := Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ToNot(HaveOccurred()) defer session.Terminate() @@ -377,7 +377,7 @@ var _ = Describe("Testing the ssh3 cli", func() { // Wait for some time to ensure that the client has established the forwarding time.Sleep(2 * time.Second) - // if the remote addr is IPv4 (resp. IPv6), ssh3 listens on the IPv4 (resp. IPv6) loopback + // if the remote addr is IPv4 (resp. IPv6), soh listens on the IPv4 (resp. IPv6) loopback // Try to connect to the local forwarded port localAddr := fmt.Sprintf("%s:%d", localIP, localPort) @@ -442,7 +442,7 @@ var _ = Describe("Testing the ssh3 cli", func() { It("Should not grand access to non-authorized identity", func() { clientArgs = append(getClientArgs(attackerPrivKeyPath), "echo", "Hello, World!") - command := exec.Command(ssh3Path, clientArgs...) + command := exec.Command(sohPath, clientArgs...) session, err := Start(command, GinkgoWriter, GinkgoWriter) Expect(err).ToNot(HaveOccurred()) Eventually(session).Should(Exit()) diff --git a/known_hosts.go b/known_hosts.go index d8ffb14..aafb8b4 100644 --- a/known_hosts.go +++ b/known_hosts.go @@ -1,4 +1,4 @@ -package ssh3 +package soh import ( "bufio" diff --git a/message/channel_request.go b/message/channel_request.go index 2b9fb2d..f279dab 100644 --- a/message/channel_request.go +++ b/message/channel_request.go @@ -8,7 +8,7 @@ import ( "io" "net" - util "github.com/francoismichel/ssh3/util" + util "github.com/francoismichel/soh/util" ) var ChannelRequestParseFuncs = map[string]func(util.Reader) (ChannelRequest, error){ diff --git a/message/message.go b/message/message.go index ce97a94..88559da 100644 --- a/message/message.go +++ b/message/message.go @@ -4,7 +4,7 @@ import ( "errors" "io" - "github.com/francoismichel/ssh3/util" + "github.com/francoismichel/soh/util" ) // ssh messages type diff --git a/message/message_test.go b/message/message_test.go index 5fd697d..8f9b253 100644 --- a/message/message_test.go +++ b/message/message_test.go @@ -5,7 +5,7 @@ import ( "crypto/rand" mathrand "math/rand" - "github.com/francoismichel/ssh3/util" + "github.com/francoismichel/soh/util" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" diff --git a/resources/figures/ssh3.png b/resources/figures/soh.png similarity index 100% rename from resources/figures/ssh3.png rename to resources/figures/soh.png diff --git a/resources/figures/ssh3_100ms_rtt.gif b/resources/figures/soh_100ms_rtt.gif similarity index 100% rename from resources/figures/ssh3_100ms_rtt.gif rename to resources/figures/soh_100ms_rtt.gif diff --git a/resources/figures/ssh3_oidc.gif b/resources/figures/soh_oidc.gif similarity index 100% rename from resources/figures/ssh3_oidc.gif rename to resources/figures/soh_oidc.gif diff --git a/resources_manager.go b/resources_manager.go index 6e78d98..7620e22 100644 --- a/resources_manager.go +++ b/resources_manager.go @@ -1,9 +1,9 @@ -package ssh3 +package soh import ( "sync" - "github.com/francoismichel/ssh3/util" + "github.com/francoismichel/soh/util" "github.com/quic-go/quic-go/http3" ) diff --git a/server.go b/server.go index 679ee3e..dd6e38b 100644 --- a/server.go +++ b/server.go @@ -1,4 +1,4 @@ -package ssh3 +package soh import ( "bytes" @@ -13,7 +13,7 @@ import ( "github.com/quic-go/quic-go/http3" "github.com/rs/zerolog/log" - "github.com/francoismichel/ssh3/util" + "github.com/francoismichel/soh/util" ) type ServerConversationHandler func(authenticatedUsername string, conversation *Conversation) error @@ -30,7 +30,7 @@ type Server struct { // Creates a new server handling http requests for SSH conversations func NewServer(maxPacketSize uint64, defaultDatagramQueueSize uint64, h3Server *http3.Server, conversationHandler ServerConversationHandler) *Server { - ssh3Server := &Server{ + sohServer := &Server{ maxPacketSize: maxPacketSize, h3Server: h3Server, conversations: make(map[http3.StreamCreator]*conversationsManager), @@ -46,9 +46,9 @@ func NewServer(maxPacketSize uint64, defaultDatagramQueueSize uint64, h3Server * return false, nil } - conversationsManager, ok := ssh3Server.getConversationsManager(qconn) + conversationsManager, ok := sohServer.getConversationsManager(qconn) if !ok { - err := fmt.Errorf("could not find SSH3 conversation for new channel %d on conn %+v", stream.StreamID(), qconn) + err := fmt.Errorf("could not find SOH conversation for new channel %d on conn %+v", stream.StreamID(), qconn) log.Error().Msgf("%s", err) return false, err } @@ -60,7 +60,7 @@ func NewServer(maxPacketSize uint64, defaultDatagramQueueSize uint64, h3Server * conversation, ok := conversationsManager.getConversation(conversationControlStreamID) if !ok { - err := fmt.Errorf("could not find SSH3 conversation with control stream id %d for new channel %d", conversationControlStreamID, + err := fmt.Errorf("could not find SOH conversation with control stream id %d for new channel %d", conversationControlStreamID, uint64(stream.StreamID())) log.Error().Msgf("%s", err) return false, err @@ -95,7 +95,7 @@ func NewServer(maxPacketSize uint64, defaultDatagramQueueSize uint64, h3Server * conversation.channelsAcceptQueue.Add(newChannel) return true, nil } - return ssh3Server + return sohServer } func (s *Server) getConversationsManager(streamCreator http3.StreamCreator) (*conversationsManager, bool) { @@ -130,7 +130,7 @@ func (s *Server) GetHTTPHandlerFunc(ctx context.Context) AuthenticatedHandlerFun return func(authenticatedUsername string, newConv *Conversation, w http.ResponseWriter, r *http.Request) { log.Info().Msgf("got request: method: %s, URL: %s", r.Method, r.URL.String()) - if r.Method == http.MethodConnect && r.Proto == "ssh3" { + if r.Method == http.MethodConnect && r.Proto == "soh" { hijacker, ok := w.(http3.Hijacker) if !ok { // should never happen, unless quic-go change their API log.Error().Msg("failed to hijack HTTP conversation: is it an HTTP/3 conversation ?") diff --git a/ssh3_suite_test.go b/soh_suite_test.go similarity index 90% rename from ssh3_suite_test.go rename to soh_suite_test.go index e40243a..e8f9400 100644 --- a/ssh3_suite_test.go +++ b/soh_suite_test.go @@ -1,4 +1,4 @@ -package ssh3_test +package soh_test import ( "testing" diff --git a/unix_server/auth.go b/unix_server/auth.go index df25c98..1220417 100644 --- a/unix_server/auth.go +++ b/unix_server/auth.go @@ -8,26 +8,26 @@ import ( "runtime" "strings" - "github.com/francoismichel/ssh3" - "github.com/francoismichel/ssh3/util/unix_util" + "github.com/francoismichel/soh" + "github.com/francoismichel/soh/util/unix_util" "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/http3" "github.com/rs/zerolog/log" ) -func HandleAuths(ctx context.Context, enablePasswordLogin bool, defaultMaxPacketSize uint64, handlerFunc ssh3.AuthenticatedHandlerFunc) (http.HandlerFunc, error) { +func HandleAuths(ctx context.Context, enablePasswordLogin bool, defaultMaxPacketSize uint64, handlerFunc soh.AuthenticatedHandlerFunc) (http.HandlerFunc, error) { if runtime.GOOS != "linux" && enablePasswordLogin { return nil, fmt.Errorf("password login not supported on %s/%s systems", runtime.GOOS, runtime.GOARCH) } return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Server", ssh3.GetCurrentVersionString()) - major, minor, patch, err := ssh3.ParseVersionString(r.UserAgent()) + w.Header().Set("Server", soh.GetCurrentVersionString()) + major, minor, patch, err := soh.ParseVersionString(r.UserAgent()) log.Debug().Msgf("received request from User-Agent %s (major %d, minor %d, patch %d)", r.UserAgent(), major, minor, patch) // currently apply strict version rules - if err != nil || major != ssh3.MAJOR || minor != ssh3.MINOR { + if err != nil || major != soh.MAJOR || minor != soh.MINOR { if err == nil { - http.Error(w, fmt.Sprintf("Unsupported version: %d.%d.%d not supported by server with version %s", major, minor, patch, ssh3.GetCurrentVersionString()), http.StatusForbidden) + http.Error(w, fmt.Sprintf("Unsupported version: %d.%d.%d not supported by server with version %s", major, minor, patch, soh.GetCurrentVersionString()), http.StatusForbidden) } else { http.Error(w, "Unsupported user-agent", http.StatusForbidden) } @@ -52,7 +52,7 @@ func HandleAuths(ctx context.Context, enablePasswordLogin bool, defaultMaxPacket return } str := r.Body.(http3.HTTPStreamer).HTTPStream() - conv, err := ssh3.NewServerConversation(ctx, str, qconn, qconn, defaultMaxPacketSize) + conv, err := soh.NewServerConversation(ctx, str, qconn, qconn, defaultMaxPacketSize) if err != nil { log.Error().Msgf("could not create new server conversation") w.WriteHeader(http.StatusInternalServerError) @@ -75,7 +75,7 @@ func HandleAuths(ctx context.Context, enablePasswordLogin bool, defaultMaxPacket }, nil } -func HandleBasicAuth(handlerFunc ssh3.AuthenticatedHandlerFunc, conv *ssh3.Conversation) http.HandlerFunc { +func HandleBasicAuth(handlerFunc soh.AuthenticatedHandlerFunc, conv *soh.Conversation) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() if !ok { diff --git a/unix_server/authorized_identities.go b/unix_server/authorized_identities.go index 1041c6c..03d89fe 100644 --- a/unix_server/authorized_identities.go +++ b/unix_server/authorized_identities.go @@ -9,9 +9,9 @@ import ( "path" "strings" - "github.com/francoismichel/ssh3/auth" - "github.com/francoismichel/ssh3/util" - "github.com/francoismichel/ssh3/util/unix_util" + "github.com/francoismichel/soh/auth" + "github.com/francoismichel/soh/util" + "github.com/francoismichel/soh/util/unix_util" "github.com/rs/zerolog/log" @@ -21,7 +21,7 @@ import ( ) /* - * In ssh3, authorized_keys are replaced by authorized_identities where a use can specify classical + * In soh, authorized_keys are replaced by authorized_identities where a use can specify classical * public keys as well as other authentication and authorization methods such as OAUTH2 and SAML 2.0 * */ @@ -38,7 +38,7 @@ type PubKeyIdentity struct { } func DefaultIdentitiesFileNames(user *unix_util.User) []string { - return []string{path.Join(user.Dir, ".ssh3", "authorized_identities"), path.Join(user.Dir, ".ssh", "authorized_keys")} + return []string{path.Join(user.Dir, ".soh", "authorized_identities"), path.Join(user.Dir, ".ssh", "authorized_keys")} } func (i *PubKeyIdentity) Verify(genericCandidate interface{}, base64ConversationID string) bool { @@ -54,7 +54,7 @@ func (i *PubKeyIdentity) Verify(genericCandidate interface{}, base64Conversation return nil, fmt.Errorf("unsupported signature algorithm '%s' for %T", unvalidatedToken.Method.Alg(), i) }, jwt.WithIssuer(i.username), - jwt.WithSubject("ssh3"), + jwt.WithSubject("soh"), jwt.WithIssuedAt(), jwt.WithAudience("unused"), jwt.WithValidMethods([]string{"RS256", "EdDSA"})) @@ -66,7 +66,7 @@ func (i *PubKeyIdentity) Verify(genericCandidate interface{}, base64Conversation if _, ok = claims["exp"]; !ok { return false } - if clientId, ok := claims["client_id"]; !ok || clientId != fmt.Sprintf("ssh3-%s", i.username) { + if clientId, ok := claims["client_id"]; !ok || clientId != fmt.Sprintf("soh-%s", i.username) { return false } if jti, ok := claims["jti"]; !ok || jti != base64ConversationID { diff --git a/unix_server/handlers.go b/unix_server/handlers.go index e49f553..b356e6e 100644 --- a/unix_server/handlers.go +++ b/unix_server/handlers.go @@ -4,9 +4,9 @@ import ( "net/http" "os" - "github.com/francoismichel/ssh3" - "github.com/francoismichel/ssh3/util" - "github.com/francoismichel/ssh3/util/unix_util" + "github.com/francoismichel/soh" + "github.com/francoismichel/soh/util" + "github.com/francoismichel/soh/util/unix_util" "github.com/rs/zerolog/log" ) @@ -34,7 +34,7 @@ func parseBearerAuth(auth string) (bearer string, ok bool) { return string(auth[len(prefix):]), true } -func HandleBearerAuth(username string, base64ConversationID string, handlerFunc ssh3.UnauthenticatedBearerFunc) http.HandlerFunc { +func HandleBearerAuth(username string, base64ConversationID string, handlerFunc soh.UnauthenticatedBearerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { bearerString, ok := BearerAuth(r) if !ok { @@ -46,7 +46,7 @@ func HandleBearerAuth(username string, base64ConversationID string, handlerFunc } // currently only supports RS256 and EdDSA signing algorithms -func HandleJWTAuth(username string, newConv *ssh3.Conversation, handlerFunc ssh3.AuthenticatedHandlerFunc) ssh3.UnauthenticatedBearerFunc { +func HandleJWTAuth(username string, newConv *soh.Conversation, handlerFunc soh.AuthenticatedHandlerFunc) soh.UnauthenticatedBearerFunc { return func(unauthenticatedBearerString string, base64ConversationID string, w http.ResponseWriter, r *http.Request) { user, err := unix_util.GetUser(username) if err != nil { diff --git a/util/types.go b/util/types.go index e3eeba9..8245ca3 100644 --- a/util/types.go +++ b/util/types.go @@ -75,8 +75,8 @@ type BytesReadCloser struct { func (b *BytesReadCloser) Close() error { return nil } -// sends an ssh3 datagram. The function must know the ID of the channel -type SSH3DatagramSenderFunc func(p []byte) error +// sends an soh datagram. The function must know the ID of the channel +type SOHDatagramSenderFunc func(p []byte) error type DatagramSender interface { SendDatagram(p []byte) error diff --git a/util/unix_util/linux_user.go b/util/unix_util/linux_user.go index 2fcdc77..70c5336 100644 --- a/util/unix_util/linux_user.go +++ b/util/unix_util/linux_user.go @@ -22,7 +22,7 @@ import ( "syscall" "unsafe" - "github.com/francoismichel/ssh3/util" + "github.com/francoismichel/soh/util" ) type ShadowEntry struct { diff --git a/util/util.go b/util/util.go index de32c59..4258162 100644 --- a/util/util.go +++ b/util/util.go @@ -266,7 +266,7 @@ func GenerateCert(priv crypto.PrivateKey) (*x509.Certificate, error) { cert := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ - Organization: []string{"SSH3Organization"}, + Organization: []string{"SOHOrganization"}, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour * 24 * 365 * 10), @@ -274,7 +274,7 @@ func GenerateCert(priv crypto.PrivateKey) (*x509.Certificate, error) { KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, - DNSNames: []string{"*", "selfsigned.ssh3"}, + DNSNames: []string{"*", "selfsigned.soh"}, IsCA: true, } diff --git a/version.go b/version.go index f775995..f0781b6 100644 --- a/version.go +++ b/version.go @@ -1,4 +1,4 @@ -package ssh3 +package soh import ( "fmt" @@ -32,7 +32,7 @@ func (e UnsupportedSSHVersion) Error() string { // GetCurrentVersionString() returns the version string to be exchanged between two // endpoints for version negotiation func GetCurrentVersionString() string { - return fmt.Sprintf("SSH 3.0 francoismichel/ssh3 %d.%d.%d", MAJOR, MINOR, PATCH) + return fmt.Sprintf("SSH 3.0 francoismichel/soh %d.%d.%d", MAJOR, MINOR, PATCH) } func ParseVersionString(version string) (major int, minor int, patch int, err error) {