pax_global_header00006660000000000000000000000064143345637420014525gustar00rootroot0000000000000052 comment=531a72e0066f52c4c12785fb4fa646b0ea79a4e8 sshutils-0.0.15/000077500000000000000000000000001433456374200134665ustar00rootroot00000000000000sshutils-0.0.15/.github/000077500000000000000000000000001433456374200150265ustar00rootroot00000000000000sshutils-0.0.15/.github/workflows/000077500000000000000000000000001433456374200170635ustar00rootroot00000000000000sshutils-0.0.15/.github/workflows/bump-version.yml000066400000000000000000000005721433456374200222400ustar00rootroot00000000000000name: Bump version on: push: branches: - main jobs: bump-version: name: Bump version runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - uses: anothrNick/github-tag-action@1.39.0 env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} WITH_V: true DEFAULT_BUMP: patch sshutils-0.0.15/.github/workflows/codeql-analysis.yml000066400000000000000000000044321433456374200227010ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ main ] pull_request: # The branches below must be a subset of the branches above branches: [ main ] schedule: - cron: '41 11 * * 5' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support steps: - name: Checkout repository uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v2 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 sshutils-0.0.15/.github/workflows/pr-check.yml000066400000000000000000000010351433456374200213010ustar00rootroot00000000000000name: PR check on: pull_request: branches: - main jobs: lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: golangci/golangci-lint-action@v3 test: name: Test strategy: matrix: os: - ubuntu-latest - macos-latest - windows-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: go-version: ^1.19 - run: go test -race -timeout 1m sshutils-0.0.15/.gitignore000066400000000000000000000004151433456374200154560ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ sshutils-0.0.15/.golangci.yml000066400000000000000000000006011433456374200160470ustar00rootroot00000000000000linters: enable-all: true disable: - wsl - nlreturn - ireturn - varnamelen issues: exclude-rules: - path: ^serialization\.go$ linters: - maligned - path: _test\.go$ linters: - funlen - maligned - cyclop - goerr113 - maintidx - gocognit - gosec - gocyclo - nestif sshutils-0.0.15/LICENSE000066400000000000000000000261351433456374200145020ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. sshutils-0.0.15/README.md000066400000000000000000000001641433456374200147460ustar00rootroot00000000000000# sshutils Go SSH utils library to complement golang.org/x/crypto/ssh https://pkg.go.dev/github.com/jaksi/sshutils sshutils-0.0.15/conn.go000066400000000000000000000137771433456374200147710ustar00rootroot00000000000000package sshutils import ( "encoding/hex" "errors" "fmt" "net" "golang.org/x/crypto/ssh" ) var ( ErrEstablishSSH = errors.New("failed to establish SSH connection") ErrSendRequest = errors.New("failed to send request") ErrChannelOpen = errors.New("failed to open channel") ) type Listener struct { net.Listener config ssh.ServerConfig } func (listener *Listener) Accept() (*Conn, error) { conn, err := listener.Listener.Accept() if err != nil { return nil, fmt.Errorf("failed to accept connection: %w", err) } sshConn, sshNewChannels, sshRequests, err := ssh.NewServerConn(conn, &listener.config) if err != nil { conn.Close() return nil, fmt.Errorf("%w: %v", ErrEstablishSSH, err) } return handleConn(sshConn, sshNewChannels, sshRequests), nil } func Listen(address string, config *ssh.ServerConfig) (*Listener, error) { l, err := net.Listen("tcp", address) if err != nil { return nil, fmt.Errorf("failed to listen: %w", err) } return &Listener{l, *config}, nil } type Conn struct { ssh.Conn NewChannels <-chan *NewChannel Requests <-chan *GlobalRequest nextChannelID int } func (conn *Conn) RawChannel(name string, payload []byte) (*Channel, error) { sshChannel, sshRequests, err := conn.Conn.OpenChannel(name, payload) if err != nil { return nil, fmt.Errorf("%w: %v", ErrChannelOpen, err) } return handleChannel(sshChannel, sshRequests, conn, name), nil } func (conn *Conn) Channel(name string, payload Payload) (*Channel, error) { var data []byte if payload != nil { data = payload.Marshal() } return conn.RawChannel(name, data) } func (conn *Conn) RawRequest(name string, wantReply bool, payload []byte) (bool, []byte, error) { accepted, reply, err := conn.SendRequest(name, wantReply, payload) if err != nil { return false, nil, fmt.Errorf("%w: %v", ErrSendRequest, err) } return accepted, reply, nil } func (conn *Conn) Request(name string, wantReply bool, payload Payload) (bool, []byte, error) { var data []byte if payload != nil { data = payload.Marshal() } return conn.RawRequest(name, wantReply, data) } func (conn *Conn) String() string { return hex.EncodeToString(conn.SessionID()) } func Dial(address string, config *ssh.ClientConfig) (*Conn, error) { conn, err := net.Dial("tcp", address) if err != nil { return nil, fmt.Errorf("failed to dial: %w", err) } sshConn, sshNewChannels, sshRequests, err := ssh.NewClientConn(conn, address, config) if err != nil { conn.Close() return nil, fmt.Errorf("%w: %v", ErrEstablishSSH, err) } return handleConn(sshConn, sshNewChannels, sshRequests), nil } func handleConn(sshConn ssh.Conn, sshNewChannels <-chan ssh.NewChannel, sshRequests <-chan *ssh.Request) *Conn { newChannels := make(chan *NewChannel) requests := make(chan *GlobalRequest) connection := &Conn{ Conn: sshConn, NewChannels: newChannels, Requests: requests, nextChannelID: 0, } go func() { for sshNewChannels != nil || sshRequests != nil { select { case newChannel, ok := <-sshNewChannels: if !ok { close(newChannels) sshNewChannels = nil continue } newChannels <- &NewChannel{newChannel, connection} case request, ok := <-sshRequests: if !ok { close(requests) sshRequests = nil continue } requests <- &GlobalRequest{request, connection} } } }() return connection } type NewChannel struct { ssh.NewChannel conn *Conn } func (newChannel *NewChannel) AcceptChannel() (*Channel, error) { sshChannel, sshRequests, err := newChannel.Accept() if err != nil { return nil, fmt.Errorf("%w: %v", ErrChannelOpen, err) } return handleChannel(sshChannel, sshRequests, newChannel.conn, newChannel.ChannelType()), nil } func (newChannel *NewChannel) UnmarshalPayload() (Payload, error) { return UnmarshalNewChannelPayload(newChannel) } func (newChannel *NewChannel) ConnMetadata() ssh.ConnMetadata { return newChannel.conn } func (newChannel *NewChannel) String() string { return newChannel.ChannelType() } type Channel struct { ssh.Channel Requests <-chan *ChannelRequest channelID string channelType string conn *Conn } func (channel *Channel) ChannelID() string { return channel.channelID } func (channel *Channel) ChannelType() string { return channel.channelType } func (channel *Channel) ConnMetadata() ssh.ConnMetadata { return channel.conn } func (channel *Channel) RawRequest(name string, wantReply bool, payload []byte) (bool, error) { accepted, err := channel.SendRequest(name, wantReply, payload) if err != nil { return false, fmt.Errorf("%w: %v", ErrSendRequest, err) } return accepted, nil } func (channel *Channel) Request(name string, wantReply bool, payload Payload) (bool, error) { var data []byte if payload != nil { data = payload.Marshal() } return channel.RawRequest(name, wantReply, data) } func (channel *Channel) String() string { return channel.channelID } func handleChannel(sshChannel ssh.Channel, sshRequests <-chan *ssh.Request, conn *Conn, name string) *Channel { requests := make(chan *ChannelRequest) channel := &Channel{sshChannel, requests, fmt.Sprint(conn.nextChannelID), name, conn} go func() { for request := range sshRequests { requests <- &ChannelRequest{request, channel} } close(requests) }() conn.nextChannelID++ return channel } type GlobalRequest struct { *ssh.Request conn *Conn } func (request *GlobalRequest) UnmarshalPayload() (Payload, error) { return UnmarshalGlobalRequestPayload(request.Request) } func (request *GlobalRequest) ConnMetadata() ssh.ConnMetadata { return request.conn } func (request *GlobalRequest) String() string { return request.Request.Type } type ChannelRequest struct { *ssh.Request channel *Channel } func (request *ChannelRequest) UnmarshalPayload() (Payload, error) { return UnmarshalChannelRequestPayload(request.Request) } func (request *ChannelRequest) Channel() *Channel { return request.channel } func (request *ChannelRequest) ConnMetadata() ssh.ConnMetadata { return request.channel.conn } func (request *ChannelRequest) String() string { return request.Request.Type } sshutils-0.0.15/conn_test.go000066400000000000000000000227401433456374200160160ustar00rootroot00000000000000package sshutils_test import ( "encoding/hex" "strings" "sync" "testing" "github.com/jaksi/sshutils" "golang.org/x/crypto/ssh" ) func TestListen_InUse(t *testing.T) { t.Parallel() //nolint:exhaustivestruct,exhaustruct serverConfig := &ssh.ServerConfig{} listener1, err := sshutils.Listen("localhost:0", serverConfig) if err != nil { t.Fatalf("Listen() error = %v", err) } defer listener1.Close() listener2, err := sshutils.Listen(listener1.Addr().String(), serverConfig) if expectedError := "failed to listen: listen tcp"; err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Listen() error = %v, want %q", err, expectedError) } if listener2 != nil { defer listener2.Close() } } func TestAcceptDial_FailedToEstablish(t *testing.T) { t.Parallel() //nolint:exhaustivestruct,exhaustruct serverConfig := &ssh.ServerConfig{} listener, err := sshutils.Listen("localhost:0", serverConfig) if err != nil { t.Fatalf("Listen() error = %v", err) } defer listener.Close() //nolint:exhaustivestruct,exhaustruct clientConfig := &ssh.ClientConfig{} errChan := make(chan error) go func() { conn, err := sshutils.Dial(listener.Addr().String(), clientConfig) if conn != nil { conn.Close() } errChan <- err }() conn, err := listener.Accept() if conn != nil { defer conn.Close() } expectedError := "failed to establish SSH connection: ssh: server has no host keys" if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Accept() error = %v, want %q", err, expectedError) } expectedError = "failed to establish SSH connection: ssh: must specify HostKeyCallback" if err = <-errChan; err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Dial() error = %v, want %q", err, expectedError) } } func TestDial_Error(t *testing.T) { t.Parallel() //nolint:exhaustivestruct,exhaustruct clientConfig := &ssh.ClientConfig{} conn, err := sshutils.Dial("localhost:0", clientConfig) if conn != nil { defer conn.Close() } expectedError := "failed to dial: dial tcp" if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Dial() error = %v, want %q", err, expectedError) } } func TestConn(t *testing.T) { t.Parallel() //nolint:exhaustivestruct,exhaustruct serverConfig := &ssh.ServerConfig{ NoClientAuth: true, } key, err := sshutils.GenerateHostKey(nil, sshutils.Ed25519) if err != nil { t.Fatalf("GenerateHostKey() error = %v", err) } serverConfig.AddHostKey(key) //nolint:exhaustivestruct,exhaustruct clientConfig := &ssh.ClientConfig{ HostKeyCallback: ssh.InsecureIgnoreHostKey(), } listener, err := sshutils.Listen("localhost:0", serverConfig) if err != nil { t.Fatalf("Listen() error = %v", err) } defer listener.Close() clientConnChan := make(chan *sshutils.Conn) go func() { clientConn, err := sshutils.Dial(listener.Addr().String(), clientConfig) if err != nil { t.Errorf("Dial() error = %v", err) } clientConnChan <- clientConn }() serverConn, err := listener.Accept() if err != nil { t.Fatalf("Accept() error = %v", err) } defer serverConn.Close() clientConn := <-clientConnChan if clientConn == nil { t.Fatal("Dial() = nil") } expectedString := hex.EncodeToString(clientConn.SessionID()) if clientConn.String() != expectedString { t.Errorf("String() = %v, want %v", clientConn.String(), expectedString) } if serverConn.String() != expectedString { t.Errorf("String() = %v, want %v", serverConn.String(), expectedString) } _, _, err = clientConn.Request("tcpip-forward", false, &sshutils.TcpipForwardRequestPayload{"foo", 42}) if err != nil { t.Errorf("Request() error = %v", err) } request := <-serverConn.Requests expectedString = "tcpip-forward: foo:42" if payload, err := request.UnmarshalPayload(); err != nil { t.Errorf("UnmarshalPayload() error = %v", err) } else if payload.String() != expectedString { t.Errorf("String() = %v, want %v", payload.String(), expectedString) } expectedVersion := "SSH-2.0-Go" if string(request.ConnMetadata().ClientVersion()) != expectedVersion { t.Errorf("ClientVersion() = %v, want %v", string(request.ConnMetadata().ClientVersion()), expectedVersion) } expectedString = "tcpip-forward" if request.String() != expectedString { t.Errorf("String() = %v, want %v", request.String(), expectedString) } channels := make(chan *sshutils.Channel) go func() { newChannel := <-clientConn.NewChannels if err := newChannel.Reject(ssh.Prohibited, "foo"); err != nil { t.Errorf("Reject() error = %v", err) channels <- nil return } newChannel = <-clientConn.NewChannels expectedString := "tun: ethernet, interface: 0" if payload, err := newChannel.UnmarshalPayload(); err != nil { t.Errorf("UnmarshalPayload() error = %v", err) } else if payload.String() != expectedString { t.Errorf("String() = %v, want %v", payload.String(), expectedString) } if string(newChannel.ConnMetadata().ClientVersion()) != expectedVersion { t.Errorf("ClientVersion() = %v, want %v", string(newChannel.ConnMetadata().ClientVersion()), expectedVersion) } expectedString = "tun@openssh.com" if newChannel.String() != expectedString { t.Errorf("String() = %v, want %v", newChannel.String(), expectedString) } channel, err := newChannel.AcceptChannel() if err != nil { t.Errorf("AcceptChannel() error = %v", err) channels <- nil return } channels <- channel }() _, err = serverConn.Channel("foo", nil) expectedError := "failed to open channel: ssh: rejected: administratively prohibited (foo)" if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Channel() error = %v, want %v", err, expectedError) } serverChannel, err := serverConn.Channel("tun@openssh.com", &sshutils.TunChannelPayload{sshutils.TunChannelModeEthernet, 0}) if err != nil { t.Errorf("Channel() error = %v", err) } clientChannel := <-channels if clientChannel == nil { t.Fatal("AcceptChannel() = nil") } if serverChannel != nil && clientChannel != nil { expectedChannelID := "0" if serverChannel.ChannelID() != expectedChannelID { t.Errorf("ChannelID() = %v, want %v", serverChannel.ChannelID(), expectedChannelID) } if clientChannel.ChannelID() != expectedChannelID { t.Errorf("ChannelID() = %v, want %v", clientChannel.ChannelID(), expectedChannelID) } expectedType := "tun@openssh.com" if serverChannel.ChannelType() != expectedType { t.Errorf("ChannelType() = %v, want %v", serverChannel.ChannelType(), expectedType) } if clientChannel.ChannelType() != expectedType { t.Errorf("ChannelType() = %v, want %v", clientChannel.ChannelType(), expectedType) } if string(serverChannel.ConnMetadata().ClientVersion()) != expectedVersion { t.Errorf("ClientVersion() = %v, want %v", string(serverChannel.ConnMetadata().ClientVersion()), expectedVersion) } if string(clientChannel.ConnMetadata().ClientVersion()) != expectedVersion { t.Errorf("ClientVersion() = %v, want %v", string(clientChannel.ConnMetadata().ClientVersion()), expectedVersion) } expectedString = "0" if serverChannel.String() != expectedString { t.Errorf("String() = %v, want %v", serverChannel.String(), expectedString) } if clientChannel.String() != expectedString { t.Errorf("String() = %v, want %v", clientChannel.String(), expectedString) } _, err = serverChannel.Request("env", false, &sshutils.EnvRequestPayload{"foo", "bar"}) if err != nil { t.Errorf("Request() error = %v", err) } channelRequest := <-clientChannel.Requests expectedString = "env: foo=bar" if payload, err := channelRequest.UnmarshalPayload(); err != nil { t.Errorf("UnmarshalPayload() error = %v", err) } else if payload.String() != expectedString { t.Errorf("String() = %v, want %v", payload.String(), expectedString) } if channelRequest.Channel().ChannelID() != expectedChannelID { t.Errorf("ChannelID() = %v, want %v", channelRequest.Channel().ChannelID(), expectedChannelID) } if string(channelRequest.ConnMetadata().ClientVersion()) != expectedVersion { t.Errorf("ClientVersion() = %v, want %v", string(channelRequest.ConnMetadata().ClientVersion()), expectedVersion) } expectedString = "env" if channelRequest.String() != expectedString { t.Errorf("String() = %v, want %v", channelRequest.String(), expectedString) } serverChannel.Close() _, err = clientChannel.Request("foo", true, nil) expectedError := "failed to send request: EOF" if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Request() error = %v, want %v", err, expectedError) } } var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() newChannel := <-clientConn.NewChannels clientConn.Close() _, err := newChannel.AcceptChannel() expectedError := "failed to open channel" if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("AcceptChannel() error = %v, want %v", err, expectedError) } }() _, err = serverConn.Channel("closing", nil) expectedError = "failed to open channel: ssh:" if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Channel() error = %v, want %v", err, expectedError) } wg.Wait() _, _, err = serverConn.Request("foo", true, nil) expectedError = "failed to send request: EOF" if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Request() error = %v, want %q", err, expectedError) } _, err = clientConn.Channel("bar", nil) expectedError = "failed to open channel: read tcp" if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Channel() error = %v, want %q", err, expectedError) } } sshutils-0.0.15/go.mod000066400000000000000000000001731433456374200145750ustar00rootroot00000000000000module github.com/jaksi/sshutils go 1.19 require golang.org/x/crypto v0.2.0 require golang.org/x/sys v0.2.0 // indirect sshutils-0.0.15/go.sum000066400000000000000000000005751433456374200146300ustar00rootroot00000000000000golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE= golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= sshutils-0.0.15/host_keys.go000066400000000000000000000046251433456374200160340ustar00rootroot00000000000000package sshutils import ( "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rsa" "crypto/x509" "encoding/pem" "errors" "fmt" "io" "os" "golang.org/x/crypto/ssh" ) const ( rsaKeyBitSize = 2048 hostKeyFilePerms = 0o600 ) var ( ErrInvalidKey = errors.New("invalid key") ErrInvalidKeyFile = errors.New("invalid key file") ErrUnsupportedKeyType = errors.New("unsupported key type") ) type KeyType int const ( RSA = iota ECDSA Ed25519 ) func (t KeyType) String() string { switch t { case RSA: return "rsa" case ECDSA: return "ecdsa" case Ed25519: return "ed25519" default: return fmt.Sprintf("unknown type (%d)", t) } } type HostKey struct { ssh.Signer key interface{} } func (key *HostKey) String() string { return ssh.FingerprintSHA256(key.PublicKey()) } func hostKeyFromKey(key interface{}) (*HostKey, error) { signer, err := ssh.NewSignerFromKey(key) if err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidKey, err) } return &HostKey{ Signer: signer, key: key, }, nil } func GenerateHostKey(rand io.Reader, t KeyType) (*HostKey, error) { var key interface{} var err error switch t { case RSA: key, err = rsa.GenerateKey(rand, rsaKeyBitSize) case ECDSA: key, err = ecdsa.GenerateKey(elliptic.P256(), rand) case Ed25519: _, key, err = ed25519.GenerateKey(rand) default: return nil, fmt.Errorf("%w: %v", ErrUnsupportedKeyType, t) } if err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidKey, err) } return hostKeyFromKey(key) } func LoadHostKey(fileName string) (*HostKey, error) { keyBytes, err := os.ReadFile(fileName) if err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidKeyFile, err) } key, err := ssh.ParseRawPrivateKey(keyBytes) if err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidKeyFile, err) } return hostKeyFromKey(key) } func (key *HostKey) Save(fileName string) error { file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, hostKeyFilePerms) //nolint:nosnakecase if err != nil { return fmt.Errorf("%w: %v", ErrInvalidKeyFile, err) } defer file.Close() keyBytes, err := x509.MarshalPKCS8PrivateKey(key.key) if err != nil { return fmt.Errorf("%w: %v", ErrInvalidKey, err) } if _, err = file.Write(pem.EncodeToMemory(&pem.Block{ Type: "PRIVATE KEY", Headers: nil, Bytes: keyBytes, })); err != nil { return fmt.Errorf("%w: %v", ErrInvalidKeyFile, err) } return nil } sshutils-0.0.15/host_keys_test.go000066400000000000000000000132741433456374200170730ustar00rootroot00000000000000package sshutils_test import ( "crypto/rand" "fmt" "os" "path" "strings" "testing" "github.com/jaksi/sshutils" "golang.org/x/crypto/ssh" ) func TestGenerateHostKey(t *testing.T) { t.Parallel() for _, tt := range []struct { name string keyType sshutils.KeyType keyTypeString string publicKeyType string err string }{ { "rsa", sshutils.RSA, "rsa", "ssh-rsa", "", }, { "ecdsa", sshutils.ECDSA, "ecdsa", "ecdsa-sha2-nistp256", "", }, { "ed25519", sshutils.Ed25519, "ed25519", "ssh-ed25519", "", }, { "unknown", sshutils.KeyType(42), "unknown type (42)", "", "unsupported key type: unknown type (42)", }, } { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if tt.keyType.String() != tt.keyTypeString { t.Errorf("expected key type string %v, got %v", tt.keyTypeString, tt.keyType.String()) } key, err := sshutils.GenerateHostKey(rand.Reader, tt.keyType) if err != nil || tt.err != "" { if (err != nil && (tt.err == "" || !strings.HasPrefix(err.Error(), tt.err))) || (err == nil && tt.err != "") { t.Errorf("GenerateHostKey() error = %v, want %q", err, tt.err) } return } if key.PublicKey().Type() != tt.publicKeyType { t.Errorf("GenerateHostKey() type = %v, want %v", key.PublicKey().Type(), tt.publicKeyType) } expectedFingerprintPrefix := "SHA256:" if !strings.HasPrefix(key.String(), expectedFingerprintPrefix) { t.Errorf("GenerateHostKey() fingerprint = %v, want %v", key.String(), expectedFingerprintPrefix) } if key.String() != ssh.FingerprintSHA256(key.PublicKey()) { t.Errorf("GenerateHostKey() fingerprint = %v, want %v", key.String(), ssh.FingerprintSHA256(key.PublicKey())) } keyFile := path.Join(t.TempDir(), tt.name) if err := key.Save(keyFile); err != nil { t.Errorf("Save() error = %v", err) } key2, err := sshutils.LoadHostKey(keyFile) if err != nil { t.Errorf("LoadHostKey() error = %v", err) } if key.String() != key2.String() { t.Errorf("LoadHostKey() fingerprint = %v, want %v", key2.String(), key.String()) } }) } } func TestGenerateHostKey_Error(t *testing.T) { t.Parallel() _, err := sshutils.GenerateHostKey(&fakeRandReader{true}, sshutils.RSA) if expectedError := "invalid key: fake error"; err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Response() error = %v, want %v", err, expectedError) } } func TestLoadHostKey_MissingFile(t *testing.T) { t.Parallel() _, err := sshutils.LoadHostKey("missing") expectedError := "invalid key file: open missing" if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("LoadHostKey() error = %v, want %v", err, expectedError) } } func TestLoadHostKey_InvalidKey(t *testing.T) { t.Parallel() keyFile := path.Join(t.TempDir(), "invalid") if err := os.WriteFile(keyFile, []byte("invalid"), 0o600); err != nil { t.Fatalf("WriteFile() error = %v", err) } _, err := sshutils.LoadHostKey(keyFile) expectedError := "invalid key file: ssh: no key found" if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("LoadHostKey() error = %v, want %v", err, expectedError) } } func TestLoadHostKey_UnsupportedKey(t *testing.T) { t.Parallel() keyFile := path.Join(t.TempDir(), "unsupported") if err := os.WriteFile(keyFile, []byte(`-----BEGIN DSA PRIVATE KEY----- MIH4AgEAAkEA/xHcnZwDuXk9xo1J7rBYQWXztGW7uOZ6DOAeJGBed2KUJlo3q2ld +k37ETPH3hy9uLEmnSQJOl9BRarNKvLIgQIVANab841m1OON+WIJR9b0GPWn1A8n AkAilkYpzVX7Xnm+iXsxRRRuMzdPmkKuED+drzYv44cKV7OfeE9mB1Em0FoAUUSE Rn9NGYrCV2oCIplAQJtFseZNAkEAjUtS8B8IKksHv3Y8cmfoLfWgNKyPNov5R+0U f64EsZ7vJrIacpDPVXi1llIjQpWFZPo7nRpJ0SA2C5YouNJzygIUQdjs5FHSqHm+ MykJo7li7Fc1OeQ= -----END DSA PRIVATE KEY-----`), 0o600); err != nil { t.Fatalf("WriteFile() error = %v", err) } _, err := sshutils.LoadHostKey(keyFile) expectedError := "invalid key: ssh: unsupported DSA key size 512" if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("LoadHostKey() error = %v, want %v", err, expectedError) } } func TestSave_InvalidFile(t *testing.T) { t.Parallel() key, err := sshutils.GenerateHostKey(rand.Reader, sshutils.RSA) if err != nil { t.Fatalf("GenerateHostKey() error = %v", err) } keyFile := t.TempDir() expectedError := fmt.Sprintf("invalid key file: open %v", keyFile) if err := key.Save(keyFile); err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Save() error = %v, want %v", err, expectedError) } } func TestSave_InvalidKey(t *testing.T) { t.Parallel() tempDir := t.TempDir() keyFile := path.Join(tempDir, "invalid") if err := os.WriteFile(keyFile, []byte(`-----BEGIN DSA PRIVATE KEY----- MIIBuwIBAAKBgQCVoARJpctxbLuBogHVoSTbL6E5cEemlqm8t5Wdp8yqi1vJSEnj +U0dPEgLW/k6vV7XLA5Aus6lY67zc70U7RBy+GiYjihtZwLZcV1XDuFvWwme70xS 6LfohTcOY/HyHMfEMDVfUn+l0jUTsHPFyESxEBbGLnOs2/KcXYFfKWndAwIVAJG6 uD3Yi9y/xvJLRQlK8Z3qyOBZAoGACkRxWQzwPG/K9iY+3aEGTyjP+JGXsyvsH3bZ pglIT0/wlyLQFmpggGN64dw0sj3MlQYkZrKBiU8gQ0VPDw5XEgzzRg0/w5ogIjQc SmOOQWrJx1ksk/Bve/rLqySizlWTr6HcAjowV3HLIyd2AkdVlER0fcZ0+Ktm/K0j Y5PP6jACgYEAk3O45B8rzmsM3NaaGS2lJKMn1iPxdbAdS783kR2Dgh0BYYq4/qFV /07jSrNUmf9CQqgLkvQkkPeIKI1pMdrC7d4ZMucSP0/GPoOAJayqfewo9tQUm6/i KO7YddFBgX0A8RD8Ta0PQqB9zP6RWnSAJwJWqfpjP5J9E1NJ930DihcCFBsjAkOk 1KFY3BSoV0jtyTfwcSh5 -----END DSA PRIVATE KEY-----`), 0o600); err != nil { t.Fatalf("WriteFile() error = %v", err) } key, err := sshutils.LoadHostKey(keyFile) if err != nil { t.Fatalf("LoadHostKey() error = %v", err) } expectedError := "invalid key: x509: unknown key type" if err := key.Save(path.Join(tempDir, "key")); err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Save() error = %v, want %v", err, expectedError) } } sshutils-0.0.15/serialization.go000066400000000000000000000577371433456374200167150ustar00rootroot00000000000000package sshutils import ( "bytes" "encoding/hex" "errors" "fmt" "io" "net" "sort" "strings" "golang.org/x/crypto/ssh" ) var errInvalidPayload = errors.New("invalid payload") type Payload interface { fmt.Stringer Unmarshal(data []byte) error Marshal() []byte } type RawPayload []byte func (p *RawPayload) String() string { return hex.EncodeToString(*p) } func (p *RawPayload) Unmarshal(data []byte) error { *p = data return nil } func (p *RawPayload) Marshal() []byte { return *p } type UnknownPayload struct { RawPayload RequestType string } func (payload *UnknownPayload) String() string { return fmt.Sprintf("unknown type (%v), payload: %v", payload.RequestType, payload.RawPayload.String()) } /* Channel open payloads */ /* session https://www.rfc-editor.org/rfc/rfc4254.html#section-6.1 */ type SessionChannelPayload struct{} func (payload *SessionChannelPayload) String() string { return "session" } func (payload *SessionChannelPayload) Unmarshal(data []byte) error { if len(data) != 0 { return fmt.Errorf("%w: non-empty payload", errInvalidPayload) } return nil } func (payload *SessionChannelPayload) Marshal() []byte { return nil } /* x11 https://www.rfc-editor.org/rfc/rfc4254#section-6.3.2 */ type X11ChannelPayload struct { OriginatorAddress string OriginatorPort uint32 } func (payload *X11ChannelPayload) String() string { return fmt.Sprintf("x11: %v", net.JoinHostPort(payload.OriginatorAddress, fmt.Sprintf("%v", payload.OriginatorPort))) } func (payload *X11ChannelPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *X11ChannelPayload) Marshal() []byte { return ssh.Marshal(payload) } /* forwarded-tcpip https://www.rfc-editor.org/rfc/rfc4254.html#section-7.2 */ type ForwardedTcpipChannelPayload struct { Address string Port uint32 OriginatorAddress string OriginatorPort uint32 } func (payload *ForwardedTcpipChannelPayload) String() string { return fmt.Sprintf("forwarded-tcpip: %v -> %v", net.JoinHostPort(payload.OriginatorAddress, fmt.Sprintf("%v", payload.OriginatorPort)), net.JoinHostPort(payload.Address, fmt.Sprintf("%v", payload.Port))) } func (payload *ForwardedTcpipChannelPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *ForwardedTcpipChannelPayload) Marshal() []byte { return ssh.Marshal(payload) } /* direct-tcpip https://www.rfc-editor.org/rfc/rfc4254.html#section-7.2 */ type DirectTcpipChannelPayload struct { Address string Port uint32 OriginatorAddress string OriginatorPort uint32 } func (payload *DirectTcpipChannelPayload) String() string { return fmt.Sprintf("direct-tcpip: %v -> %v", net.JoinHostPort(payload.OriginatorAddress, fmt.Sprintf("%v", payload.OriginatorPort)), net.JoinHostPort(payload.Address, fmt.Sprintf("%v", payload.Port))) } func (payload *DirectTcpipChannelPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *DirectTcpipChannelPayload) Marshal() []byte { return ssh.Marshal(payload) } /* tun@openssh.com https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?rev=HEAD section 2.3 */ type TunChannelMode uint32 const ( TunChannelModePointToPoint TunChannelMode = 1 TunChannelModeEthernet TunChannelMode = 2 ) func (mode TunChannelMode) String() string { switch mode { case TunChannelModePointToPoint: return "point-to-point" case TunChannelModeEthernet: return "ethernet" } return fmt.Sprintf("unknown mode (%v)", uint32(mode)) } type TunChannelPayload struct { TunnelMode TunChannelMode Interface uint32 } func (payload *TunChannelPayload) String() string { return fmt.Sprintf("tun: %v, interface: %v", payload.TunnelMode, payload.Interface) } func (payload *TunChannelPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *TunChannelPayload) Marshal() []byte { return ssh.Marshal(payload) } /* direct-streamlocal@openssh.com https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?rev=HEAD section 2.4 */ type DirectStreamlocalChannelPayload struct { Path string Reserved1 string Reserved2 uint32 } func (payload *DirectStreamlocalChannelPayload) String() string { return fmt.Sprintf("direct-streamlocal: %v", payload.Path) } func (payload *DirectStreamlocalChannelPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *DirectStreamlocalChannelPayload) Marshal() []byte { return ssh.Marshal(payload) } /* forwarded-streamlocal@openssh.com https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?rev=HEAD section 2.4 */ type ForwardedStreamlocalChannelPayload struct { Path string Reserved string } func (payload *ForwardedStreamlocalChannelPayload) String() string { return fmt.Sprintf("forwarded-streamlocal: %v", payload.Path) } func (payload *ForwardedStreamlocalChannelPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *ForwardedStreamlocalChannelPayload) Marshal() []byte { return ssh.Marshal(payload) } func UnmarshalNewChannelPayload(newChannel ssh.NewChannel) (Payload, error) { var payload Payload switch newChannel.ChannelType() { case "session": payload = new(SessionChannelPayload) case "x11": payload = new(X11ChannelPayload) case "forwarded-tcpip": payload = new(ForwardedTcpipChannelPayload) case "direct-tcpip": payload = new(DirectTcpipChannelPayload) case "tun@openssh.com": payload = new(TunChannelPayload) case "direct-streamlocal@openssh.com": payload = new(DirectStreamlocalChannelPayload) case "forwarded-streamlocal@openssh.com": payload = new(ForwardedStreamlocalChannelPayload) default: payload = &UnknownPayload{nil, newChannel.ChannelType()} } if err := payload.Unmarshal(newChannel.ExtraData()); err != nil { return nil, fmt.Errorf("failed to unmarshal new channel payload: %w", err) } return payload, nil } /* Global request payloads */ /* tcpip-forward https://www.rfc-editor.org/rfc/rfc4254.html#section-7.1 */ type tcpipRequestPayload struct { Address string Port uint32 } type TcpipForwardRequestPayload tcpipRequestPayload func (payload *TcpipForwardRequestPayload) String() string { return fmt.Sprintf("tcpip-forward: %v", net.JoinHostPort(payload.Address, fmt.Sprint(payload.Port))) } func (payload *TcpipForwardRequestPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *TcpipForwardRequestPayload) Marshal() []byte { return ssh.Marshal(payload) } func (payload *TcpipForwardRequestPayload) Response(port uint32) []byte { return ssh.Marshal(struct{ uint32 }{port}) } /* cancel-tcpip-forward https://www.rfc-editor.org/rfc/rfc4254.html#section-7.1 */ type CancelTcpipForwardRequestPayload tcpipRequestPayload func (payload *CancelTcpipForwardRequestPayload) String() string { return fmt.Sprintf("cancel-tcpip-forward: %v", net.JoinHostPort(payload.Address, fmt.Sprint(payload.Port))) } func (payload *CancelTcpipForwardRequestPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *CancelTcpipForwardRequestPayload) Marshal() []byte { return ssh.Marshal(payload) } /* no-more-sessions@openssh.com https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?rev=HEAD section 2.2 */ type NoMoreSessionsRequestPayload struct{} func (payload *NoMoreSessionsRequestPayload) String() string { return "no-more-sessions" } func (payload *NoMoreSessionsRequestPayload) Unmarshal(data []byte) error { if len(data) != 0 { return fmt.Errorf("%w: non-empty payload", errInvalidPayload) } return nil } func (payload *NoMoreSessionsRequestPayload) Marshal() []byte { return nil } /* streamlocal-forward@openssh.com https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?rev=HEAD section 2.4 */ type streamlocalForwardRequestPayload struct { Path string } type StreamlocalForwardRequestPayload streamlocalForwardRequestPayload func (payload *StreamlocalForwardRequestPayload) String() string { return fmt.Sprintf("streamlocal-forward: %v", payload.Path) } func (payload *StreamlocalForwardRequestPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *StreamlocalForwardRequestPayload) Marshal() []byte { return ssh.Marshal(payload) } /* cancel-streamlocal-forward@openssh.com https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?rev=HEAD section 2.4 */ type CancelStreamlocalForwardRequestPayload streamlocalForwardRequestPayload func (payload *CancelStreamlocalForwardRequestPayload) String() string { return fmt.Sprintf("cancel-streamlocal-forward: %v", payload.Path) } func (payload *CancelStreamlocalForwardRequestPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *CancelStreamlocalForwardRequestPayload) Marshal() []byte { return ssh.Marshal(payload) } /* hostkeys-00@openssh.com https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?rev=HEAD section 2.5 */ type PublicKeys []ssh.PublicKey func (publicKeys PublicKeys) String() string { fingerprints := make([]string, len(publicKeys)) for i, publicKey := range publicKeys { fingerprints[i] = ssh.FingerprintSHA256(publicKey) } return fmt.Sprintf("[%v]", strings.Join(fingerprints, ", ")) } func unmarshalBytes(data []byte) ([][]byte, error) { var result [][]byte for len(data) > 0 { var b struct { Bytes string Rest []byte `ssh:"rest"` } if err := ssh.Unmarshal(data, &b); err != nil { return nil, fmt.Errorf("failed to unmarshal bytes: %w", err) } result = append(result, []byte(b.Bytes)) data = b.Rest } return result, nil } func unmarshalPublicKeys(data []byte) (PublicKeys, error) { publicKeyBytes, err := unmarshalBytes(data) if err != nil { return nil, fmt.Errorf("failed to unmarshal public keys: %w", err) } publicKeys := make(PublicKeys, len(publicKeyBytes)) for i, b := range publicKeyBytes { publicKeys[i], err = ssh.ParsePublicKey(b) if err != nil { return nil, fmt.Errorf("failed to parse public key: %w", err) } } return publicKeys, nil } func marshalBytes(payload [][]byte) []byte { var result []byte for _, b := range payload { result = append(result, ssh.Marshal(struct{ string }{string(b)})...) } return result } func marshalPublicKeys(publicKeys PublicKeys) []byte { publicKeyBytes := make([][]byte, len(publicKeys)) for i, publicKey := range publicKeys { publicKeyBytes[i] = publicKey.Marshal() } return marshalBytes(publicKeyBytes) } type hostkeysRequestPayload struct { Hostkeys PublicKeys } type HostkeysRequestPayload hostkeysRequestPayload func (payload *HostkeysRequestPayload) String() string { return fmt.Sprintf("hostkeys: %v", payload.Hostkeys) } func (payload *HostkeysRequestPayload) Unmarshal(data []byte) error { publicKeys, err := unmarshalPublicKeys(data) if err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } payload.Hostkeys = publicKeys return nil } func (payload *HostkeysRequestPayload) Marshal() []byte { return marshalPublicKeys(payload.Hostkeys) } /* hostkeys-prove-00@openssh.com https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?rev=HEAD section 2.5 */ type HostkeysProveRequestPayload hostkeysRequestPayload func (payload *HostkeysProveRequestPayload) String() string { return fmt.Sprintf("hostkeys-prove: %v", payload.Hostkeys) } func (payload *HostkeysProveRequestPayload) Unmarshal(data []byte) error { publicKeys, err := unmarshalPublicKeys(data) if err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } payload.Hostkeys = publicKeys return nil } func (payload *HostkeysProveRequestPayload) Marshal() []byte { return marshalPublicKeys(payload.Hostkeys) } func hostkeySignatureData(hostkey ssh.PublicKey, sessionID []byte) []byte { return ssh.Marshal(struct { requestType, sessionID, hostkey string }{ "hostkeys-prove-00@openssh.com", string(sessionID), string(hostkey.Marshal()), }) } func (payload *HostkeysProveRequestPayload) Response( rand io.Reader, hostKeys []*HostKey, sessionID []byte, ) ([]byte, error) { responseBytes := make([][]byte, len(payload.Hostkeys)) for i, requestKey := range payload.Hostkeys { var signature *ssh.Signature var err error for _, hostKey := range hostKeys { if bytes.Equal(requestKey.Marshal(), hostKey.PublicKey().Marshal()) { signature, err = hostKey.Sign(rand, hostkeySignatureData(hostKey.PublicKey(), sessionID)) if err != nil { return nil, fmt.Errorf("failed to sign data: %w", err) } break } } if signature == nil { return nil, fmt.Errorf("%w: no matching host key", errInvalidPayload) } responseBytes[i] = ssh.Marshal(signature) } return marshalBytes(responseBytes), nil } func (payload *HostkeysProveRequestPayload) VerifyResponse(response []byte, sessionID []byte) error { signatureBytes, err := unmarshalBytes(response) if err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } if len(signatureBytes) != len(payload.Hostkeys) { return fmt.Errorf("%w: invalid number of signatures", errInvalidPayload) } for i, b := range signatureBytes { signature := new(ssh.Signature) if err := ssh.Unmarshal(b, signature); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } if err := payload.Hostkeys[i].Verify(hostkeySignatureData(payload.Hostkeys[i], sessionID), signature); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } } return nil } func UnmarshalGlobalRequestPayload(request *ssh.Request) (Payload, error) { var payload Payload switch request.Type { case "tcpip-forward": payload = new(TcpipForwardRequestPayload) case "cancel-tcpip-forward": payload = new(CancelTcpipForwardRequestPayload) case "no-more-sessions@openssh.com": payload = new(NoMoreSessionsRequestPayload) case "streamlocal-forward@openssh.com": payload = new(StreamlocalForwardRequestPayload) case "cancel-streamlocal-forward@openssh.com": payload = new(CancelStreamlocalForwardRequestPayload) case "hostkeys-00@openssh.com": payload = new(HostkeysRequestPayload) case "hostkeys-prove-00@openssh.com": payload = new(HostkeysProveRequestPayload) default: payload = &UnknownPayload{nil, request.Type} } if err := payload.Unmarshal(request.Payload); err != nil { return nil, fmt.Errorf("failed to unmarshal global request payload: %w", err) } return payload, nil } /* Global request payloads */ /* pty-req https://www.rfc-editor.org/rfc/rfc4254.html#section-6.2 */ type PtyRequestPayload struct { Term string Width uint32 Height uint32 WidthPx uint32 HeightPx uint32 TerminalModes ssh.TerminalModes } func (payload *PtyRequestPayload) String() string { return fmt.Sprintf("pty-req: %v, %vx%v", payload.Term, payload.Width, payload.Height) } type rawPtyRequestPayload struct { Term string Width uint32 Height uint32 WidthPx uint32 HeightPx uint32 TerminalModes string } func (payload *PtyRequestPayload) Unmarshal(data []byte) error { var raw rawPtyRequestPayload if err := ssh.Unmarshal(data, &raw); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } rawTerminalModes := []byte(raw.TerminalModes) terminalModes := ssh.TerminalModes{} for len(rawTerminalModes) > 0 { var opcode struct { Opcode byte Rest []byte `ssh:"rest"` } _ = ssh.Unmarshal(rawTerminalModes, &opcode) if !(opcode.Opcode > 0 && opcode.Opcode < 160) { break } var argument struct { Argument uint32 Rest []byte `ssh:"rest"` } if err := ssh.Unmarshal(opcode.Rest, &argument); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } terminalModes[opcode.Opcode] = argument.Argument rawTerminalModes = argument.Rest } payload.Term = raw.Term payload.Width = raw.Width payload.Height = raw.Height payload.WidthPx = raw.WidthPx payload.HeightPx = raw.HeightPx payload.TerminalModes = terminalModes return nil } func (payload *PtyRequestPayload) Marshal() []byte { var raw rawPtyRequestPayload raw.Term = payload.Term raw.Width = payload.Width raw.Height = payload.Height raw.WidthPx = payload.WidthPx raw.HeightPx = payload.HeightPx terminalModes := []byte{} opcodes := make([]int, 0, len(payload.TerminalModes)) for opcode := range payload.TerminalModes { opcodes = append(opcodes, int(opcode)) } sort.Ints(opcodes) for _, opcode := range opcodes { terminalModes = append(terminalModes, ssh.Marshal(struct { byte uint32 }{byte(opcode), payload.TerminalModes[uint8(opcode)]})...) } terminalModes = append(terminalModes, ssh.Marshal(struct{ byte }{0})...) raw.TerminalModes = string(terminalModes) return ssh.Marshal(&raw) } /* x11-req https://www.rfc-editor.org/rfc/rfc4254.html#section-6.3.1 */ type X11RequestPayload struct { SingleConnection bool AuthenticationProtocol string AuthenticationCookie string ScreenNumber uint32 } func (payload *X11RequestPayload) String() string { return fmt.Sprintf("x11-req: %v", payload.ScreenNumber) } func (payload *X11RequestPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *X11RequestPayload) Marshal() []byte { return ssh.Marshal(payload) } /* env https://www.rfc-editor.org/rfc/rfc4254.html#section-6.4 */ type EnvRequestPayload struct { Name string Value string } func (payload *EnvRequestPayload) String() string { return fmt.Sprintf("env: %v=%v", payload.Name, payload.Value) } func (payload *EnvRequestPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *EnvRequestPayload) Marshal() []byte { return ssh.Marshal(payload) } /* shell https://www.rfc-editor.org/rfc/rfc4254.html#section-6.5 */ type ShellRequestPayload struct{} func (payload *ShellRequestPayload) String() string { return "shell" } func (payload *ShellRequestPayload) Unmarshal(data []byte) error { if len(data) != 0 { return fmt.Errorf("%w: non-empty payload", errInvalidPayload) } return nil } func (payload *ShellRequestPayload) Marshal() []byte { return nil } /* exec https://www.rfc-editor.org/rfc/rfc4254.html#section-6.5 */ type ExecRequestPayload struct { Command string } func (payload *ExecRequestPayload) String() string { return fmt.Sprintf("exec: %v", payload.Command) } func (payload *ExecRequestPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *ExecRequestPayload) Marshal() []byte { return ssh.Marshal(payload) } /* subsystem https://www.rfc-editor.org/rfc/rfc4254.html#section-6.5 */ type SubsystemRequestPayload struct { Subsystem string } func (payload *SubsystemRequestPayload) String() string { return fmt.Sprintf("subsystem: %v", payload.Subsystem) } func (payload *SubsystemRequestPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *SubsystemRequestPayload) Marshal() []byte { return ssh.Marshal(payload) } /* window-change https://www.rfc-editor.org/rfc/rfc4254.html#section-6.7 */ type WindowChangeRequestPayload struct { Width uint32 Height uint32 WidthPx uint32 HeightPx uint32 } func (payload *WindowChangeRequestPayload) String() string { return fmt.Sprintf("window-change: %vx%v", payload.Width, payload.Height) } func (payload *WindowChangeRequestPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *WindowChangeRequestPayload) Marshal() []byte { return ssh.Marshal(payload) } /* xon-xoff https://www.rfc-editor.org/rfc/rfc4254.html#section-6.8 */ type XonXoffRequestPayload struct { ClientCanDo bool } func (payload *XonXoffRequestPayload) String() string { return fmt.Sprintf("xon-xoff: %v", payload.ClientCanDo) } func (payload *XonXoffRequestPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *XonXoffRequestPayload) Marshal() []byte { return ssh.Marshal(payload) } /* signal https://www.rfc-editor.org/rfc/rfc4254.html#section-6.9 */ type SignalRequestPayload struct { Name string } func (payload *SignalRequestPayload) String() string { return fmt.Sprintf("signal: %v", payload.Name) } func (payload *SignalRequestPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *SignalRequestPayload) Marshal() []byte { return ssh.Marshal(payload) } /* exit-status https://www.rfc-editor.org/rfc/rfc4254.html#section-6.10 */ type ExitStatusRequestPayload struct { ExitStatus uint32 } func (payload *ExitStatusRequestPayload) String() string { return fmt.Sprintf("exit-status: %v", payload.ExitStatus) } func (payload *ExitStatusRequestPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *ExitStatusRequestPayload) Marshal() []byte { return ssh.Marshal(payload) } /* exit-signal https://www.rfc-editor.org/rfc/rfc4254.html#section-6.10 */ type ExitSignalRequestPayload struct { Name string CoreDumped bool Message string Language string } func (payload *ExitSignalRequestPayload) String() string { return fmt.Sprintf("exit-signal: %v", payload.Name) } func (payload *ExitSignalRequestPayload) Unmarshal(data []byte) error { if err := ssh.Unmarshal(data, payload); err != nil { return fmt.Errorf("%w: %v", errInvalidPayload, err) } return nil } func (payload *ExitSignalRequestPayload) Marshal() []byte { return ssh.Marshal(payload) } /* eow@openssh.com https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?rev=HEAD section 2.1 */ type EowRequestPayload struct{} func (payload *EowRequestPayload) String() string { return "eow" } func (payload *EowRequestPayload) Unmarshal(data []byte) error { if len(data) != 0 { return fmt.Errorf("%w: non-empty payload", errInvalidPayload) } return nil } func (payload *EowRequestPayload) Marshal() []byte { return nil } //nolint:cyclop func UnmarshalChannelRequestPayload(request *ssh.Request) (Payload, error) { var payload Payload switch request.Type { case "pty-req": payload = new(PtyRequestPayload) case "x11-req": payload = new(X11RequestPayload) case "env": payload = new(EnvRequestPayload) case "shell": payload = new(ShellRequestPayload) case "exec": payload = new(ExecRequestPayload) case "subsystem": payload = new(SubsystemRequestPayload) case "window-change": payload = new(WindowChangeRequestPayload) case "xon-xoff": payload = new(XonXoffRequestPayload) case "signal": payload = new(SignalRequestPayload) case "exit-status": payload = new(ExitStatusRequestPayload) case "exit-signal": payload = new(ExitSignalRequestPayload) case "eow@openssh.com": payload = new(EowRequestPayload) default: payload = &UnknownPayload{nil, request.Type} } if err := payload.Unmarshal(request.Payload); err != nil { return nil, fmt.Errorf("failed to unmarshal channel request payload: %w", err) } return payload, nil } sshutils-0.0.15/serialization_test.go000066400000000000000000000574441433456374200177470ustar00rootroot00000000000000package sshutils_test import ( "bytes" "crypto/rand" "errors" "fmt" "reflect" "strings" "testing" "github.com/jaksi/sshutils" "golang.org/x/crypto/ssh" ) type fakeNewChannel struct { channelType string extraData []byte } func (newChannel *fakeNewChannel) Accept() (ssh.Channel, <-chan *ssh.Request, error) { panic("not implemented") } func (newChannel *fakeNewChannel) Reject(reason ssh.RejectionReason, message string) error { panic("not implemented") } func (newChannel *fakeNewChannel) ChannelType() string { return newChannel.channelType } func (newChannel *fakeNewChannel) ExtraData() []byte { return newChannel.extraData } func testPayload( t *testing.T, rawPayload []byte, payload sshutils.Payload, err error, expectedPayload sshutils.Payload, expectedString string, expectedError string, ) { t.Helper() if err != nil || expectedError != "" { if (err != nil && (expectedError == "" || !strings.HasPrefix(err.Error(), expectedError))) || (err == nil && expectedError != "") { t.Errorf("Unmarshal() error = %v, want %q", err, expectedError) } return } if !reflect.DeepEqual(payload, expectedPayload) { t.Errorf("Unmarshal() = %#v, want %#v", payload, expectedPayload) } if str := payload.String(); str != expectedString { t.Errorf("String() = %v, want %v", str, expectedString) } if data := payload.Marshal(); !bytes.Equal(data, rawPayload) { t.Errorf("Marshal() = %v, want %v", data, rawPayload) } } func TestNewChannelPayload(t *testing.T) { t.Parallel() for _, tt := range []struct { name string newChannel *fakeNewChannel payload sshutils.Payload str string err string }{ { "session", &fakeNewChannel{ "session", []byte{}, }, &sshutils.SessionChannelPayload{}, "session", "", }, { "session_invalid_payload", &fakeNewChannel{ "session", []byte{42}, }, nil, "", "failed to unmarshal new channel payload: invalid payload: non-empty payload", }, { "x11", &fakeNewChannel{ "x11", ssh.Marshal(struct { a string b uint32 }{"foo", 42}), }, &sshutils.X11ChannelPayload{"foo", 42}, "x11: foo:42", "", }, { "x11_invalid_payload", &fakeNewChannel{ "x11", []byte{}, }, nil, "", "failed to unmarshal new channel payload: invalid payload: ssh: parse error", }, { "forwarded_tcpip", &fakeNewChannel{ "forwarded-tcpip", ssh.Marshal(struct { a string b uint32 c string d uint32 }{"foo", 42, "bar", 43}), }, &sshutils.ForwardedTcpipChannelPayload{"foo", 42, "bar", 43}, "forwarded-tcpip: bar:43 -> foo:42", "", }, { "forwarded_tcpip_invalid_payload", &fakeNewChannel{ "forwarded-tcpip", []byte{}, }, nil, "", "failed to unmarshal new channel payload: invalid payload: ssh: parse error", }, { "direct_tcpip", &fakeNewChannel{ "direct-tcpip", ssh.Marshal(struct { a string b uint32 c string d uint32 }{"foo", 42, "bar", 43}), }, &sshutils.DirectTcpipChannelPayload{"foo", 42, "bar", 43}, "direct-tcpip: bar:43 -> foo:42", "", }, { "direct_tcpip_invalid_payload", &fakeNewChannel{ "direct-tcpip", []byte{}, }, nil, "", "failed to unmarshal new channel payload: invalid payload: ssh: parse error", }, { "tun_ppp", &fakeNewChannel{ "tun@openssh.com", ssh.Marshal(struct { a uint32 b uint32 }{1, 42}), }, &sshutils.TunChannelPayload{1, 42}, "tun: point-to-point, interface: 42", "", }, { "tun_ethernet", &fakeNewChannel{ "tun@openssh.com", ssh.Marshal(struct { a uint32 b uint32 }{2, 42}), }, &sshutils.TunChannelPayload{2, 42}, "tun: ethernet, interface: 42", "", }, { "tun_unknown", &fakeNewChannel{ "tun@openssh.com", ssh.Marshal(struct { a uint32 b uint32 }{3, 42}), }, &sshutils.TunChannelPayload{3, 42}, "tun: unknown mode (3), interface: 42", "", }, { "tun_invalid_payload", &fakeNewChannel{ "tun@openssh.com", []byte{}, }, nil, "", "failed to unmarshal new channel payload: invalid payload: ssh: parse error", }, { "direct_streamlocal", &fakeNewChannel{ "direct-streamlocal@openssh.com", ssh.Marshal(struct { a string b string c uint32 }{"foo", "bar", 42}), }, &sshutils.DirectStreamlocalChannelPayload{"foo", "bar", 42}, "direct-streamlocal: foo", "", }, { "direct_streamlocal_invalid_payload", &fakeNewChannel{ "direct-streamlocal@openssh.com", []byte{}, }, nil, "", "failed to unmarshal new channel payload: invalid payload: ssh: parse error", }, { "forwarded_streamlocal", &fakeNewChannel{ "forwarded-streamlocal@openssh.com", ssh.Marshal(struct { a string b string }{"foo", "bar"}), }, &sshutils.ForwardedStreamlocalChannelPayload{"foo", "bar"}, "forwarded-streamlocal: foo", "", }, { "forwarded_streamlocal_invalid_payload", &fakeNewChannel{ "forwarded-streamlocal@openssh.com", []byte{}, }, nil, "", "failed to unmarshal new channel payload: invalid payload: ssh: parse error", }, { "unknown", &fakeNewChannel{ "lorem_ipsum", []byte{42, 43}, }, &sshutils.UnknownPayload{sshutils.RawPayload{42, 43}, "lorem_ipsum"}, "unknown type (lorem_ipsum), payload: 2a2b", "", }, } { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() payload, err := sshutils.UnmarshalNewChannelPayload(tt.newChannel) testPayload(t, tt.newChannel.extraData, payload, err, tt.payload, tt.str, tt.err) }) } } func TestGlobalRequestPayload(t *testing.T) { t.Parallel() key1, err := sshutils.GenerateHostKey(&fakeRandReader{false}, sshutils.Ed25519) if err != nil { t.Fatalf("GenerateHostKey() error = %v", err) } key2, err := sshutils.GenerateHostKey(&fakeRandReader{false}, sshutils.ECDSA) if err != nil { t.Fatalf("GenerateHostKey() error = %v", err) } for _, tt := range []struct { name string globalRequest *ssh.Request payload sshutils.Payload str string err string }{ { "tcpip_forward", &ssh.Request{ Type: "tcpip-forward", WantReply: true, Payload: ssh.Marshal(struct { a string b uint32 }{"foo", 42}), }, &sshutils.TcpipForwardRequestPayload{"foo", 42}, "tcpip-forward: foo:42", "", }, { "tcpip_forward_invalid_payload", &ssh.Request{ Type: "tcpip-forward", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal global request payload: invalid payload: ssh: parse error", }, { "cancel_tcpip_forward", &ssh.Request{ Type: "cancel-tcpip-forward", WantReply: true, Payload: ssh.Marshal(struct { a string b uint32 }{"foo", 42}), }, &sshutils.CancelTcpipForwardRequestPayload{"foo", 42}, "cancel-tcpip-forward: foo:42", "", }, { "cancel_tcpip_forward_invalid_payload", &ssh.Request{ Type: "cancel-tcpip-forward", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal global request payload: invalid payload: ssh: parse error", }, { "no-more-sessions", &ssh.Request{ Type: "no-more-sessions@openssh.com", WantReply: true, Payload: []byte{}, }, &sshutils.NoMoreSessionsRequestPayload{}, "no-more-sessions", "", }, { "no-more-sessions_invalid_payload", &ssh.Request{ Type: "no-more-sessions@openssh.com", WantReply: true, Payload: []byte{42}, }, nil, "", "failed to unmarshal global request payload: invalid payload: non-empty payload", }, { "streamlocal_forward", &ssh.Request{ Type: "streamlocal-forward@openssh.com", WantReply: true, Payload: ssh.Marshal(struct { a string }{"foo"}), }, &sshutils.StreamlocalForwardRequestPayload{"foo"}, "streamlocal-forward: foo", "", }, { "streamlocal_forward_invalid_payload", &ssh.Request{ Type: "streamlocal-forward@openssh.com", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal global request payload: invalid payload: ssh: parse error", }, { "cancel_streamlocal_forward", &ssh.Request{ Type: "cancel-streamlocal-forward@openssh.com", WantReply: true, Payload: ssh.Marshal(struct { a string }{"foo"}), }, &sshutils.CancelStreamlocalForwardRequestPayload{"foo"}, "cancel-streamlocal-forward: foo", "", }, { "cancel_streamlocal_forward_invalid_payload", &ssh.Request{ Type: "cancel-streamlocal-forward@openssh.com", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal global request payload: invalid payload: ssh: parse error", }, { "hostkeys-00", &ssh.Request{ Type: "hostkeys-00@openssh.com", WantReply: true, Payload: ssh.Marshal(struct { a string b string }{string(key1.PublicKey().Marshal()), string(key2.PublicKey().Marshal())}), }, &sshutils.HostkeysRequestPayload{sshutils.PublicKeys{key1.PublicKey(), key2.PublicKey()}}, fmt.Sprintf("hostkeys: [%v, %v]", ssh.FingerprintSHA256(key1.PublicKey()), ssh.FingerprintSHA256(key2.PublicKey())), "", }, { "hostkeys-00_invalid_payload", &ssh.Request{ Type: "hostkeys-00@openssh.com", WantReply: true, Payload: []byte{42}, }, nil, "", "failed to unmarshal global request payload: invalid payload: failed to unmarshal public keys: " + "failed to unmarshal bytes: ssh: unmarshal error", }, { "hostkeys-00_invalid_public_key", &ssh.Request{ Type: "hostkeys-00@openssh.com", WantReply: true, Payload: ssh.Marshal(struct { a string }{"foo"}), }, nil, "", "failed to unmarshal global request payload: invalid payload: failed to parse public key: ssh: short read", }, { "hostkeys-prove-00", &ssh.Request{ Type: "hostkeys-prove-00@openssh.com", WantReply: true, Payload: ssh.Marshal(struct { a string b string }{string(key1.PublicKey().Marshal()), string(key2.PublicKey().Marshal())}), }, &sshutils.HostkeysProveRequestPayload{sshutils.PublicKeys{key1.PublicKey(), key2.PublicKey()}}, fmt.Sprintf("hostkeys-prove: [%v, %v]", ssh.FingerprintSHA256(key1.PublicKey()), ssh.FingerprintSHA256(key2.PublicKey())), "", }, { "hostkeys-prove-00_invalid_payload", &ssh.Request{ Type: "hostkeys-prove-00@openssh.com", WantReply: true, Payload: []byte{42}, }, nil, "", "failed to unmarshal global request payload: invalid payload: failed to unmarshal public keys: " + "failed to unmarshal bytes: ssh: unmarshal error", }, { "unknown", &ssh.Request{ Type: "lorem_ipsum", WantReply: true, Payload: []byte{42, 43}, }, &sshutils.UnknownPayload{sshutils.RawPayload{42, 43}, "lorem_ipsum"}, "unknown type (lorem_ipsum), payload: 2a2b", "", }, } { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() payload, err := sshutils.UnmarshalGlobalRequestPayload(tt.globalRequest) testPayload(t, tt.globalRequest.Payload, payload, err, tt.payload, tt.str, tt.err) }) } } func TestTcpipForwardRequestPayload(t *testing.T) { t.Parallel() payload := &sshutils.TcpipForwardRequestPayload{"foo", 42} response := payload.Response(43) expectedResponse := ssh.Marshal(struct{ uint32 }{43}) if !bytes.Equal(response, expectedResponse) { t.Errorf("Response() = %v, want %v", response, expectedResponse) } } type fakeRandReader struct { fail bool } func (r *fakeRandReader) Read(p []byte) (int, error) { if r.fail { return 0, errors.New("fake error") } for i := range p { p[i] = 42 } return len(p), nil } func TestHostkeysProveRequestPayload(t *testing.T) { t.Parallel() key1, err := sshutils.GenerateHostKey(&fakeRandReader{false}, sshutils.Ed25519) if err != nil { t.Fatalf("GenerateHostKey() error = %v", err) } signature1, err := key1.Sign(&fakeRandReader{false}, ssh.Marshal(struct { a string b string c string }{"hostkeys-prove-00@openssh.com", "foo", string(key1.PublicKey().Marshal())})) if err != nil { t.Fatalf("Sign() error = %v", err) } key2, err := sshutils.GenerateHostKey(&fakeRandReader{false}, sshutils.ECDSA) if err != nil { t.Fatalf("GenerateHostKey() error = %v", err) } signature2, err := key2.Sign(&fakeRandReader{false}, ssh.Marshal(struct { a string b string c string }{"hostkeys-prove-00@openssh.com", "foo", string(key2.PublicKey().Marshal())})) if err != nil { t.Fatalf("Sign() error = %v", err) } payload := &sshutils.HostkeysProveRequestPayload{sshutils.PublicKeys{key1.PublicKey(), key2.PublicKey()}} response, err := payload.Response(&fakeRandReader{false}, []*sshutils.HostKey{key1, key2}, []byte("foo")) if err != nil { t.Fatalf("Response() error = %v", err) } expectedResponse := ssh.Marshal(struct { a string b string }{ string(ssh.Marshal(signature1)), string(ssh.Marshal(signature2)), }) if !bytes.Equal(response, expectedResponse) { t.Errorf("Response() = %v, want %v", response, expectedResponse) } if err := payload.VerifyResponse(response, []byte("foo")); err != nil { t.Errorf("VerifyResponse() error = %v", err) } } func TestHostkeysProveRequestPayloadResponse_NotFound(t *testing.T) { t.Parallel() key, err := sshutils.GenerateHostKey(&fakeRandReader{false}, sshutils.Ed25519) if err != nil { t.Fatalf("GenerateHostKey() error = %v", err) } payload := &sshutils.HostkeysProveRequestPayload{sshutils.PublicKeys{key.PublicKey()}} expectedError := "invalid payload: no matching host key" _, err = payload.Response(&fakeRandReader{false}, []*sshutils.HostKey{}, []byte("foo")) if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Response() error = %v, want %v", err, expectedError) } } func TestHostkeysProveRequestPayloadResponse_SignError(t *testing.T) { t.Parallel() key, err := sshutils.GenerateHostKey(&fakeRandReader{false}, sshutils.ECDSA) if err != nil { t.Fatalf("GenerateHostKey() error = %v", err) } payload := &sshutils.HostkeysProveRequestPayload{sshutils.PublicKeys{key.PublicKey()}} expectedError := "failed to sign data: fake error" _, err = payload.Response(&fakeRandReader{true}, []*sshutils.HostKey{key}, []byte("foo")) if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("Response() error = %v, want %v", err, expectedError) } } func TestHostkeysProveRequestPayloadVerifyResponse_InvalidPayload(t *testing.T) { t.Parallel() payload := &sshutils.HostkeysProveRequestPayload{sshutils.PublicKeys{}} expectedError := "invalid payload: failed to unmarshal bytes: ssh: unmarshal error" err := payload.VerifyResponse([]byte{42}, []byte("foo")) if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("VerifyResponse() error = %v, want %v", err, expectedError) } } func TestHostkeysProveRequestPayloadVerifyResponse_InvalidCount(t *testing.T) { t.Parallel() key, err := sshutils.GenerateHostKey(&fakeRandReader{false}, sshutils.Ed25519) if err != nil { t.Fatalf("GenerateHostKey() error = %v", err) } payload := &sshutils.HostkeysProveRequestPayload{sshutils.PublicKeys{key.PublicKey()}} expectedError := "invalid payload: invalid number of signatures" err = payload.VerifyResponse([]byte{}, []byte("foo")) if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("VerifyResponse() error = %v, want %v", err, expectedError) } } func TestHostkeysProveRequestPayloadVerifyResponse_InvalidSignaturePayload(t *testing.T) { t.Parallel() key, err := sshutils.GenerateHostKey(&fakeRandReader{false}, sshutils.Ed25519) if err != nil { t.Fatalf("GenerateHostKey() error = %v", err) } payload := &sshutils.HostkeysProveRequestPayload{sshutils.PublicKeys{key.PublicKey()}} expectedError := "invalid payload: ssh: unmarshal error" err = payload.VerifyResponse(ssh.Marshal(struct{ string }{"foo"}), []byte("foo")) if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("VerifyResponse() error = %v, want %v", err, expectedError) } } func TestHostkeysProveRequestPayloadVerifyResponse_InvalidSignature(t *testing.T) { t.Parallel() key1, err := sshutils.GenerateHostKey(&fakeRandReader{false}, sshutils.Ed25519) if err != nil { t.Fatalf("GenerateHostKey() error = %v", err) } key2, err := sshutils.GenerateHostKey(rand.Reader, sshutils.Ed25519) if err != nil { t.Fatalf("GenerateHostKey() error = %v", err) } payload1 := &sshutils.HostkeysProveRequestPayload{sshutils.PublicKeys{key1.PublicKey()}} payload2 := &sshutils.HostkeysProveRequestPayload{sshutils.PublicKeys{key2.PublicKey()}} response, err := payload2.Response(&fakeRandReader{false}, []*sshutils.HostKey{key2}, []byte("foo")) if err != nil { t.Fatalf("Response() error = %v", err) } expectedError := "invalid payload: ssh: signature did not verify" err = payload1.VerifyResponse(response, []byte("foo")) if err == nil || !strings.HasPrefix(err.Error(), expectedError) { t.Errorf("VerifyResponse() error = %v, want %v", err, expectedError) } } func TestChannelRequestPayload(t *testing.T) { t.Parallel() for _, tt := range []struct { name string channelRequest *ssh.Request payload sshutils.Payload str string err string }{ { "pty-req", &ssh.Request{ Type: "pty-req", WantReply: true, Payload: ssh.Marshal(struct { a string b uint32 c uint32 d uint32 e uint32 f string }{"foo", 42, 43, 44, 45, string([]byte{1, 0, 0, 0, 42, 0})}), }, &sshutils.PtyRequestPayload{"foo", 42, 43, 44, 45, ssh.TerminalModes{1: 42}}, "pty-req: foo, 42x43", "", }, { "pty-req_invalid_payload", &ssh.Request{ Type: "pty-req", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal channel request payload: invalid payload: ssh: parse error", }, { "pty-req_invalid_terminal_modes", &ssh.Request{ Type: "pty-req", WantReply: true, Payload: ssh.Marshal(struct { a string b uint32 c uint32 d uint32 e uint32 f string }{"foo", 42, 43, 44, 45, string([]byte{1})}), }, nil, "", "failed to unmarshal channel request payload: invalid payload: ssh: parse error", }, { "x11-req", &ssh.Request{ Type: "x11-req", WantReply: true, Payload: ssh.Marshal(struct { a bool b string c string d uint32 }{true, "foo", "bar", 42}), }, &sshutils.X11RequestPayload{true, "foo", "bar", 42}, "x11-req: 42", "", }, { "x11-req_invalid_payload", &ssh.Request{ Type: "x11-req", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal channel request payload: invalid payload: ssh: parse error", }, { "env", &ssh.Request{ Type: "env", WantReply: true, Payload: ssh.Marshal(struct { a string b string }{"foo", "bar"}), }, &sshutils.EnvRequestPayload{"foo", "bar"}, "env: foo=bar", "", }, { "env_invalid_payload", &ssh.Request{ Type: "env", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal channel request payload: invalid payload: ssh: parse error", }, { "shell", &ssh.Request{ Type: "shell", WantReply: true, Payload: []byte{}, }, &sshutils.ShellRequestPayload{}, "shell", "", }, { "shell_invalid_payload", &ssh.Request{ Type: "shell", WantReply: true, Payload: []byte{42}, }, nil, "", "failed to unmarshal channel request payload: invalid payload: non-empty payload", }, { "exec", &ssh.Request{ Type: "exec", WantReply: true, Payload: ssh.Marshal(struct { a string }{"foo"}), }, &sshutils.ExecRequestPayload{"foo"}, "exec: foo", "", }, { "exec_invalid_payload", &ssh.Request{ Type: "exec", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal channel request payload: invalid payload: ssh: parse error", }, { "subsystem", &ssh.Request{ Type: "subsystem", WantReply: true, Payload: ssh.Marshal(struct { a string }{"foo"}), }, &sshutils.SubsystemRequestPayload{"foo"}, "subsystem: foo", "", }, { "subsystem_invalid_payload", &ssh.Request{ Type: "subsystem", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal channel request payload: invalid payload: ssh: parse error", }, { "window-change", &ssh.Request{ Type: "window-change", WantReply: true, Payload: ssh.Marshal(struct { a uint32 b uint32 c uint32 d uint32 }{42, 43, 44, 45}), }, &sshutils.WindowChangeRequestPayload{42, 43, 44, 45}, "window-change: 42x43", "", }, { "window-change_invalid_payload", &ssh.Request{ Type: "window-change", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal channel request payload: invalid payload: ssh: parse error", }, { "xon-xoff", &ssh.Request{ Type: "xon-xoff", WantReply: true, Payload: ssh.Marshal(struct { a bool }{true}), }, &sshutils.XonXoffRequestPayload{true}, "xon-xoff: true", "", }, { "xon-xoff_invalid_payload", &ssh.Request{ Type: "xon-xoff", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal channel request payload: invalid payload: ssh: parse error", }, { "signal", &ssh.Request{ Type: "signal", WantReply: true, Payload: ssh.Marshal(struct { a string }{"foo"}), }, &sshutils.SignalRequestPayload{"foo"}, "signal: foo", "", }, { "signal_invalid_payload", &ssh.Request{ Type: "signal", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal channel request payload: invalid payload: ssh: parse error", }, { "exit-status", &ssh.Request{ Type: "exit-status", WantReply: true, Payload: ssh.Marshal(struct { a uint32 }{42}), }, &sshutils.ExitStatusRequestPayload{42}, "exit-status: 42", "", }, { "exit-status_invalid_payload", &ssh.Request{ Type: "exit-status", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal channel request payload: invalid payload: ssh: parse error", }, { "exit-signal", &ssh.Request{ Type: "exit-signal", WantReply: true, Payload: ssh.Marshal(struct { a string b bool c string d string }{"foo", true, "bar", "baz"}), }, &sshutils.ExitSignalRequestPayload{"foo", true, "bar", "baz"}, "exit-signal: foo", "", }, { "exit-signal_invalid_payload", &ssh.Request{ Type: "exit-signal", WantReply: true, Payload: []byte{}, }, nil, "", "failed to unmarshal channel request payload: invalid payload: ssh: parse error", }, { "eow", &ssh.Request{ Type: "eow@openssh.com", WantReply: true, Payload: []byte{}, }, &sshutils.EowRequestPayload{}, "eow", "", }, { "eow_invalid_payload", &ssh.Request{ Type: "eow@openssh.com", WantReply: true, Payload: []byte{42}, }, nil, "", "failed to unmarshal channel request payload: invalid payload: non-empty payload", }, { "unknown", &ssh.Request{ Type: "lorem_ipsum", WantReply: true, Payload: []byte{42, 43}, }, &sshutils.UnknownPayload{sshutils.RawPayload{42, 43}, "lorem_ipsum"}, "unknown type (lorem_ipsum), payload: 2a2b", "", }, } { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() payload, err := sshutils.UnmarshalChannelRequestPayload(tt.channelRequest) testPayload(t, tt.channelRequest.Payload, payload, err, tt.payload, tt.str, tt.err) }) } }