pax_global_header00006660000000000000000000000064147270214330014515gustar00rootroot0000000000000052 comment=b8723aa6aa285f3bdf8370914db8cd432dcc5134 golang-github-hiddeco-sshsig-0.1.0/000077500000000000000000000000001472702143300171355ustar00rootroot00000000000000golang-github-hiddeco-sshsig-0.1.0/.github/000077500000000000000000000000001472702143300204755ustar00rootroot00000000000000golang-github-hiddeco-sshsig-0.1.0/.github/dependabot.yaml000066400000000000000000000004471472702143300234730ustar00rootroot00000000000000version: 2 updates: # Enable version updates for Go Modules - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" # Enable version updates for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" golang-github-hiddeco-sshsig-0.1.0/.github/workflows/000077500000000000000000000000001472702143300225325ustar00rootroot00000000000000golang-github-hiddeco-sshsig-0.1.0/.github/workflows/codeql.yaml000066400000000000000000000027421472702143300246720ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [ "main" ] pull_request: branches: [ "main" ] schedule: - cron: '25 6 * * 3' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] steps: - name: Checkout code uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2.2.7 with: languages: ${{ matrix.language }} # xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # xref: https://codeql.github.com/codeql-query-help/go/ queries: security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, Go, 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@a589d4087ea22a0a48fc153d1b461886e262e0f2 # v2.2.7 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@a589d4087ea22a0a48fc153d1b461886e262e0f2 # v2.2.7 with: category: "/language:${{matrix.language}}" golang-github-hiddeco-sshsig-0.1.0/.github/workflows/test.yaml000066400000000000000000000017451472702143300244040ustar00rootroot00000000000000name: Test on: push: branches: - main pull_request: branches: - main paths-ignore: - README.md schedule: - cron: '55 5 * * 1-5' jobs: versions: name: Go ${{ matrix.go-version }} (${{ matrix.platform }}) runs-on: ${{ matrix.platform }} permissions: contents: read strategy: fail-fast: false matrix: go-version: [1.19.x, 1.20.x] platform: [ubuntu-latest, macos-latest, windows-latest] steps: - name: Install Go uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0 - name: Run fmt and vet run: make fmt vet - name: Check if directory is clean run: git diff --exit-code - name: Print SSH version run: ssh -V - name: Test run: make test golang-github-hiddeco-sshsig-0.1.0/.gitignore000066400000000000000000000000471472702143300211260ustar00rootroot00000000000000# Output of the go coverage tool *.out golang-github-hiddeco-sshsig-0.1.0/LICENSE000066400000000000000000000261351472702143300201510ustar00rootroot00000000000000 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. golang-github-hiddeco-sshsig-0.1.0/Makefile000066400000000000000000000011071472702143300205740ustar00rootroot00000000000000COVERAGE_REPORT ?= coverage.out .PHONY: help help: ## Display this help screen @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) .PHONY: all all: fmt vet test ## Run fmt, vet and test .PHONY: test test: ## Run tests with race detector and coverage go test -v -race -coverprofile=$(COVERAGE_REPORT) ./... .PHONY: fmt fmt: ## Run go fmt go fmt ./... .PHONY: vet vet: ## Run go vet go vet ./... golang-github-hiddeco-sshsig-0.1.0/README.md000066400000000000000000000037761472702143300204310ustar00rootroot00000000000000# sshsig [![Go Reference](https://pkg.go.dev/badge/github.com/openssh/sshsig.svg)][godoc] [![Go Report Card](https://goreportcard.com/badge/github.com/hiddeco/sshsig)][goreport] This Go library implements the [`SSHSIG` wire protocol][sshsig-protocol], and can be used to sign and verify messages using SSH keys. Compared to other implementations, this library does all the following: - Accepts an `io.Reader` as input for signing and verifying messages. - Performs simple public key fingerprint and namespace mismatch checks in `Verify`. Malicious input will still fail signature verification, but this provides more useful error messages. - Properly uses `ssh-sha2-512` as signature algorithm when signing with an RSA private key, as [described in the protocol][sshsig-rsa-req]. - Does not accept a `Sign` operation without a `namespace` as [specified in the protocol][sshsig-namespace-req]. - Allows `Verify` operations to be performed without a `namespace`, ensuring compatibility with loose implementations. - Provides `Armor` and `Unarmor` functions to encode/decode the signature to/from an (armored) PEM format. For more information about the use of this library, see the [Go Reference][godoc]. ## Acknowledgements There are several other implementations of the `SSHSIG` protocol in Go, from which this library has borrowed ideas: - [go-sshsig][go-sshsig] by Paul Tagliamonte - [Sigstore Rekor][rekor-ssh] from the Sigstore project [sshsig-protocol]: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig [sshsig-rsa-req]: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig#L69-L72 [sshsig-namespace-req]: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig#L57 [go-sshsig]: https://github.com/paultag/go-sshsig/tree/a684343203bd83859fbe5783fc976948b4413010 [rekor-ssh]: https://github.com/sigstore/rekor/tree/v1.0.1/pkg/pki/ssh [godoc]: https://pkg.go.dev/github.com/hiddeco/sshsig [goreport]: https://goreportcard.com/report/github.com/hiddeco/sshsig golang-github-hiddeco-sshsig-0.1.0/armor.go000066400000000000000000000013731472702143300206100ustar00rootroot00000000000000package sshsig import ( "encoding/pem" "errors" "fmt" ) // PEMType is the PEM type of an armored SSH signature. const PEMType = "SSH SIGNATURE" // Armor returns a PEM-encoded Signature. It does not perform any validation. func Armor(s *Signature) []byte { return pem.EncodeToMemory(&pem.Block{ Type: PEMType, Bytes: s.Marshal(), }) } // Unarmor decodes a PEM-encoded signature into a Signature. It returns an // error if the PEM block is invalid or the signature parsing fails. func Unarmor(b []byte) (*Signature, error) { p, _ := pem.Decode(b) if p == nil { return nil, errors.New("invalid PEM block") } if p.Type != PEMType { return nil, fmt.Errorf("invalid PEM type %q: expected %q", p.Type, PEMType) } return ParseSignature(p.Bytes) } golang-github-hiddeco-sshsig-0.1.0/armor_test.go000066400000000000000000000014071472702143300216450ustar00rootroot00000000000000package sshsig_test import ( "testing" "github.com/stretchr/testify/assert" "github.com/hiddeco/sshsig" ) func TestUnarmor(t *testing.T) { t.Run("invalid PEM block", func(t *testing.T) { got, err := sshsig.Unarmor(nil) assert.ErrorContains(t, err, "invalid PEM block") assert.Nil(t, got) }) t.Run("invalid PEM type", func(t *testing.T) { got, err := sshsig.Unarmor([]byte("-----BEGIN FOO-----\n-----END FOO-----\n")) assert.ErrorContains(t, err, `invalid PEM type "FOO"`) assert.Nil(t, got) }) t.Run("invalid PEM data", func(t *testing.T) { got, err := sshsig.Unarmor([]byte("-----BEGIN " + sshsig.PEMType + "-----\n-----END " + sshsig.PEMType + "-----\n")) assert.ErrorContains(t, err, "ssh: parse error in message") assert.Nil(t, got) }) } golang-github-hiddeco-sshsig-0.1.0/doc.go000066400000000000000000000003621472702143300202320ustar00rootroot00000000000000// Package sshsig provides an API to sign and verify messages using SSH keys. // It is an implementation of the SSH Signature format as described in // https://github.com/openssh/openssh-portable/blob/V_9_3_P1/PROTOCOL.sshsig. package sshsig golang-github-hiddeco-sshsig-0.1.0/example_test.go000066400000000000000000000035361472702143300221650ustar00rootroot00000000000000package sshsig_test import ( "bytes" "fmt" "golang.org/x/crypto/ssh" "github.com/hiddeco/sshsig" ) func ExampleSign() { // Load the private key to sign with. signer, err := ssh.ParsePrivateKey([]byte(ecdsaPrivateKey)) if err != nil { panic(err) } // Sign a message with the private key, using an SHA-512 hash and the // namespace "file". message := []byte("Hello world!") sig, err := sshsig.Sign(bytes.NewReader(message), signer, sshsig.HashSHA512, "file") if err != nil { panic(err) } // Print the signature in armored (PEM) format. armored := sshsig.Armor(sig) fmt.Printf("%s", armored) } func ExampleVerify() { // Load a public key to verify with. pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(ed25519PublicKey)) if err != nil { panic(err) } // Load the armored (PEM) signature to verify. armored := []byte(`-----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1 NgAAAEEEOIYuWF0v/w8XVrOLUa30nMhLwiXdsf4aow88kfpnfA/Zn+Xhr9nRh97e tNV1/Kqv1VE/On/YH+094IhlatyELQAAAARmaWxlAAAAAAAAAAZzaGE1MTIAAABk AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAABJAAAAIBXp90537Om8Xbv0iTxVwvSy iZmhAca7kPt0uSg0IVtTAAAAIQCbN+co4miAJ7t9XLIQuOaOQCM5P0AxRCdsMG4e BnAL0w== -----END SSH SIGNATURE-----`) sig, err := sshsig.Unarmor(armored) if err != nil { panic(err) } // Verify the signature, using the same hash algorithm and namespace as // used to sign the message. If the signature is valid, no error is // returned. message := []byte("Hello world!") if err := sshsig.Verify(bytes.NewReader(message), sig, pub, sig.HashAlgorithm, sig.Namespace); err != nil { panic(err) } // When more strict verification is required, the hash algorithm and/or // namespace can be checked against the expected values. if err := sshsig.Verify(bytes.NewReader(message), sig, pub, sshsig.HashSHA512, "file"); err != nil { panic(err) } } golang-github-hiddeco-sshsig-0.1.0/go.mod000066400000000000000000000001671472702143300202470ustar00rootroot00000000000000module github.com/hiddeco/sshsig go 1.13 require ( github.com/stretchr/testify v1.8.2 golang.org/x/crypto v0.7.0 ) golang-github-hiddeco-sshsig-0.1.0/go.sum000066400000000000000000000117001472702143300202670ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-github-hiddeco-sshsig-0.1.0/hash.go000066400000000000000000000054451472702143300204170ustar00rootroot00000000000000package sshsig import ( "crypto" "errors" "fmt" "hash" ) // supportedHashAlgorithm is a map of supported hash algorithms, as defined in // the protocol. // xref: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig#L66-L67 var supportedHashAlgorithms = map[HashAlgorithm]crypto.Hash{ HashSHA256: crypto.SHA256, HashSHA512: crypto.SHA512, } var ( // ErrUnsupportedHashAlgorithm is returned by Sign and Verify if the hash // algorithm is not supported. ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm") // ErrUnavailableHashAlgorithm is returned by Sign and Verify if the hash // algorithm is not available. ErrUnavailableHashAlgorithm = errors.New("unavailable hash algorithm") ) const ( // HashSHA256 is the SHA-256 hash algorithm. HashSHA256 HashAlgorithm = "sha256" // HashSHA512 is the SHA-512 hash algorithm. HashSHA512 HashAlgorithm = "sha512" ) // HashAlgorithm represents an algorithm used to compute a hash of a // message. type HashAlgorithm string // Supported returns ErrUnsupportedHashAlgorithm if the hash algorithm is not // supported, nil otherwise. Use Available if the intention is to make use of // the hash algorithm, as it also checks if the hash algorithm is available. func (h HashAlgorithm) Supported() error { if _, ok := supportedHashAlgorithms[h]; !ok { // TODO(hidde): if the number of supported hash algorithms grows, it // might be worth generating the list of supported algorithms. return fmt.Errorf("%w %q: must be %q or %q", ErrUnsupportedHashAlgorithm, h.String(), HashSHA256, HashSHA512) } return nil } // Available returns ErrUnsupportedHashAlgorithm if the hash algorithm is not // supported, ErrUnavailableHashAlgorithm if the hash algorithm is not available, // nil otherwise. func (h HashAlgorithm) Available() error { if err := h.Supported(); err != nil { return err } if !supportedHashAlgorithms[h].Available() { return fmt.Errorf("%w %q", ErrUnavailableHashAlgorithm, h.String()) } return nil } // Hash returns a hash.Hash for the hash algorithm. If the hash algorithm is // not available, it panics. The library itself ensures that the hash algorithm // is available before calling this function. func (h HashAlgorithm) Hash() hash.Hash { if h == "" { panic("sshsig: hash algorithm not specified") } if err := h.Available(); err != nil { panic("sshsig: " + err.Error()) } return supportedHashAlgorithms[h].New() } // String returns the string representation of the hash algorithm. func (h HashAlgorithm) String() string { return string(h) } // SupportedHashAlgorithms returns a list of supported hash algorithms. func SupportedHashAlgorithms() []HashAlgorithm { var algorithms []HashAlgorithm for algorithm := range supportedHashAlgorithms { algorithms = append(algorithms, algorithm) } return algorithms } golang-github-hiddeco-sshsig-0.1.0/internal_test.go000066400000000000000000000047601472702143300223460ustar00rootroot00000000000000package sshsig import ( "bytes" "crypto/ed25519" "crypto/rand" "crypto/rsa" "testing" "github.com/stretchr/testify/assert" "golang.org/x/crypto/ssh" ) // This file contains "white box" tests that test certain things that are not // exposed through the public API, but are still important to test. func TestParseSignature(t *testing.T) { t.Run("invalid blob data", func(t *testing.T) { got, err := ParseSignature(blob{}.Marshal()) assert.ErrorIs(t, err, ErrUnsupportedSignatureVersion) assert.Nil(t, got) }) t.Run("invalid signature", func(t *testing.T) { got, err := ParseSignature(blob{ Version: sigVersion, Signature: "invalid", HashAlgorithm: HashSHA256.String(), }.Marshal()) assert.ErrorContains(t, err, "ssh: unmarshal error for field Format of type Signature") assert.Nil(t, got) }) t.Run("invalid private key", func(t *testing.T) { got, err := ParseSignature(blob{ Version: sigVersion, Signature: string(ssh.Marshal(&ssh.Signature{})), HashAlgorithm: HashSHA256.String(), }.Marshal()) assert.ErrorContains(t, err, "ssh: short read") assert.Nil(t, got) }) t.Run("invalid RSA signature", func(t *testing.T) { pk, err := rsa.GenerateKey(rand.Reader, 1024) assert.NoError(t, err) pub, err := ssh.NewPublicKey(&pk.PublicKey) assert.NoError(t, err) got, err := ParseSignature(blob{ Version: sigVersion, PublicKey: string(pub.Marshal()), HashAlgorithm: HashSHA256.String(), Signature: string(ssh.Marshal(&ssh.Signature{ Format: ssh.KeyAlgoRSA, })), }.Marshal()) assert.ErrorContains(t, err, `invalid signature format "ssh-rsa"`) assert.Nil(t, got) }) } func TestVerify_WithoutNamespace(t *testing.T) { testMessage := []byte("And now for something completely different.") // Deliberately generate a signature with an empty namespace, // which is not allowed through the public API. algo := HashSHA256 h := algo.Hash() h.Write(testMessage) mh := h.Sum(nil) _, cSigner, err := ed25519.GenerateKey(rand.Reader) assert.NoError(t, err) signer, err := ssh.NewSignerFromKey(cSigner) assert.NoError(t, err) sd := signedData{ Namespace: "", HashAlgorithm: algo.String(), Hash: string(mh), } sig, err := signer.Sign(rand.Reader, sd.Marshal()) assert.NoError(t, err) // Verify the namespace-less signature. err = Verify(bytes.NewReader(testMessage), &Signature{ PublicKey: signer.PublicKey(), Signature: sig, }, signer.PublicKey(), algo, sd.Namespace) assert.NoError(t, err) } golang-github-hiddeco-sshsig-0.1.0/openssh_test.go000066400000000000000000000275631472702143300222170ustar00rootroot00000000000000package sshsig_test import ( "bytes" "fmt" "io" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "testing" "github.com/stretchr/testify/assert" "golang.org/x/crypto/ssh" "github.com/hiddeco/sshsig" ) const ( // ed25519PrivateKey is an ED25519 private key, generated with: // `ssh-keygen -t ed25519 -C "sshsig@example.com"` ed25519PrivateKey = `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACDTcDBzPJS3L3vhzHSpo2mp0Z5HThNEpt2VMZI7+S04IAAAAJjcTWiZ3E1o mQAAAAtzc2gtZWQyNTUxOQAAACDTcDBzPJS3L3vhzHSpo2mp0Z5HThNEpt2VMZI7+S04IA AAAEAAQVJdHf/P7QGmNhr/QhAA82Gees/wN41nUfr515ujCNNwMHM8lLcve+HMdKmjaanR nkdOE0Sm3ZUxkjv5LTggAAAAEnNzaHNpZ0BleGFtcGxlLmNvbQECAw== -----END OPENSSH PRIVATE KEY-----` // ed25519PublicKey is the public key corresponding to ed25519PrivateKey. ed25519PublicKey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNwMHM8lLcve+HMdKmjaanRnkdOE0Sm3ZUxkjv5LTgg sshsig@example.com` // ecdsaPrivateKey is a ECDSA-P256 private key, generated with: // `ssh-keygen -t ecdsa -b 256 -C "sshsig@example.com"` ecdsaPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQ4hi5YXS//DxdWs4tRrfScyEvCJd2x /hqjDzyR+md8D9mf5eGv2dGH3t601XX8qq/VUT86f9gf7T3giGVq3IQtAAAAsPbhCNX24Q jVAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDiGLlhdL/8PF1az i1Gt9JzIS8Il3bH+GqMPPJH6Z3wP2Z/l4a/Z0Yfe3rTVdfyqr9VRPzp/2B/tPeCIZWrchC 0AAAAgat7A5GYa+yEHE/QWotjwVO3cPxGuyn6ErMUKhIzzetwAAAASc3Noc2lnQGV4YW1w bGUuY29tAQIDBAUG -----END OPENSSH PRIVATE KEY-----` // ecdsaPublicKey is the public key corresponding to ecdsaPrivateKey. ecdsaPublicKey = `ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDiGLlhdL/8PF1azi1Gt9JzIS8Il3bH+GqMPPJH6Z3wP2Z/l4a/Z0Yfe3rTVdfyqr9VRPzp/2B/tPeCIZWrchC0= sshsig@example.com` // rsaPrivateKey is a 1024-bit RSA key, generated with // `ssh-keygen -t rsa -b 1024 -C "sshsig@example.com"` rsaPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn NhAAAAAwEAAQAAAIEAtqER/SEhWnXVYnijqazzf8LkA4bjSrCNUUSg8nn0H9R/f7jb0au7 6ba/ap4RmmzxKzzpkI1eUrEcPG6/g8N/VYFEU6pszHP2lhjFcbF3Y2zNFm9ygaaTtx61EY 7Rtr7W9SkqtE4yeo0Wnnlc1sV9JVcKTndIRSuQogMKyXeF9tEAAAIItMvAebTLwHkAAAAH c3NoLXJzYQAAAIEAtqER/SEhWnXVYnijqazzf8LkA4bjSrCNUUSg8nn0H9R/f7jb0au76b a/ap4RmmzxKzzpkI1eUrEcPG6/g8N/VYFEU6pszHP2lhjFcbF3Y2zNFm9ygaaTtx61EY7R tr7W9SkqtE4yeo0Wnnlc1sV9JVcKTndIRSuQogMKyXeF9tEAAAADAQABAAAAgQCu9ozHVz Ae+/icSDtzWNBHPC05+8ZRTed1TixrYM6yl+A2OqHNs5tpgrzLpffzXB+IbujMpcMRsb/9 XZR45Zhcb8Zg6yUOeb9zAoTGYLmIBcKEVRe23AkBY0UDordM758oHmX37Etxr8ij/mg7Uq TPthJkdd8XxO47gT91OrYfyQAAAEAdPeOlb222qWeY1mC8hKTESPAho+DZxBKCy93fNhUD 4M55ef2CQsxYreDnfFDNJOxgfFXUU403wYLPMJJ0lMDfAAAAQQDwVpAPLN3fVYNidS8H0x AUfNkjLYfE5k4O2TmeYXSbcrCVzUjvb/4ZcCJWSechfJGNX5qyGTrE0ho54Q4HVu03AAAA QQDCh8deIWcBdCmDRjO3mE1xoav3fCi3BVH7qodIRuYy1hV3xOSUjwnO5mC1YmeTfyL0uR 4XBqbl1cLmti+/bwA3AAAAEnNzaHNpZ0BleGFtcGxlLmNvbQ== -----END OPENSSH PRIVATE KEY-----` // rsaPublicKey is the public key corresponding to rsaPrivateKey. rsaPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC2oRH9ISFaddVieKOprPN/wuQDhuNKsI1RRKDyefQf1H9/uNvRq7vptr9qnhGabPErPOmQjV5SsRw8br+Dw39VgURTqmzMc/aWGMVxsXdjbM0Wb3KBppO3HrURjtG2vtb1KSq0TjJ6jRaeeVzWxX0lVwpOd0hFK5CiAwrJd4X20Q== sshsig@example.com` // otherPrivateKey is a ED25519 key to test failure cases with, generated with: // ssh-keygen -t ed25519 -C "sshsig-other@example.com" otherPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACDTcDBzPJS3L3vhzHSpo2mp0Z5HThNEpt2VMZI7+S04IAAAAJjcTWiZ3E1o mQAAAAtzc2gtZWQyNTUxOQAAACDTcDBzPJS3L3vhzHSpo2mp0Z5HThNEpt2VMZI7+S04IA AAAEAAQVJdHf/P7QGmNhr/QhAA82Gees/wN41nUfr515ujCNNwMHM8lLcve+HMdKmjaanR nkdOE0Sm3ZUxkjv5LTggAAAAEnNzaHNpZ0BleGFtcGxlLmNvbQECAw== -----END OPENSSH PRIVATE KEY-----` // otherPublicKey is the public key corresponding to otherPrivateKey. otherPublicKey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOrvP89uyupCbqyFcCz1nNtKuLT8YIUkj0Vhf/xYamSs sshsig-other@example.com` ) func TestSignToOpenSSH(t *testing.T) { if _, err := exec.LookPath("ssh-keygen"); err != nil { t.Skip("skipping: missing ssh-keygen in PATH") } var ( testNamespace = "file" testMessage = []byte("I like your game but we have to change the rules.") ) tests := []struct { name string publicKey string privateKey string }{ {"ed25519", ed25519PublicKey, ed25519PrivateKey}, {"ecdsa", ecdsaPublicKey, ecdsaPrivateKey}, {"rsa", rsaPublicKey, rsaPrivateKey}, } for _, tt := range tests { tt := tt for _, a := range sshsig.SupportedHashAlgorithms() { algo := a t.Run(fmt.Sprintf("%s-%s", tt.name, algo), func(t *testing.T) { // Make test go brrrr... t.Parallel() // Temporary directory used as working directory for ssh-keygen. tmp := t.TempDir() // Load the private key. signer, err := ssh.ParsePrivateKey([]byte(tt.privateKey)) assert.NoError(t, err) // Sign a message. sig, err := sshsig.Sign(bytes.NewReader(testMessage), signer, algo, testNamespace) assert.NoError(t, err) // Write the PEM to a file. sigFile := filepath.Join(tmp, "sig") assert.NoError(t, os.WriteFile(sigFile, sshsig.Armor(sig), 0o600)) // Construct allowed_signers file. id, row := allowedSigner(t, tt.publicKey) idOther, rowOther := allowedSigner(t, otherPublicKey) allowedSigners := fmt.Sprintf("%s\n%s", row, rowOther) allowedSignersFile := filepath.Join(tmp, "allowed_signers") assert.NoError(t, os.WriteFile(allowedSignersFile, []byte(allowedSigners), 0o600)) // Check the signature. _, err = execOpenSSH(t, tmp, bytes.NewReader(testMessage), "-Y", "check-novalidate", "-f", allowedSignersFile, "-n", testNamespace, "-s", sigFile) assert.NoError(t, err) // Verify the signature. _, err = execOpenSSH(t, tmp, bytes.NewReader(testMessage), "-Y", "verify", "-f", allowedSignersFile, "-I", id, "-n", testNamespace, "-s", "sig") assert.NoError(t, err) // Different key. out, err := execOpenSSH(t, tmp, bytes.NewReader(testMessage), "-Y", "verify", "-f", allowedSignersFile, "-I", idOther, "-n", testNamespace, "-s", sigFile) assert.Error(t, err, out) // Different namespace. out, err = execOpenSSH(t, tmp, bytes.NewReader(testMessage), "-Y", "verify", "-f", allowedSignersFile, "-I", id, "-n", "other", "-s", sigFile) assert.Error(t, err, out) // Different data. out, err = execOpenSSH(t, tmp, bytes.NewReader([]byte("other")), "-Y", "verify", "-f", allowedSignersFile, "-I", id, "-n", testNamespace, "-s", sigFile) assert.Error(t, err, out) }) } } } func TestVerifyFromOpenSSH(t *testing.T) { if _, err := exec.LookPath("ssh-keygen"); err != nil { t.Skip("skipping: missing ssh-keygen in PATH") } var ( testNamespace = "file" testMessage = []byte("I never failed to convince an audience that the best thing they could do was to go away.") sshVersion = getSSHVersion(t) // Only ssh-keygen 8.9 and later allow selection of hash at sshsig // signing time. This is unfortunately not available in the version of // OpenSSH that ships with macOS in GitHub Actions. // xref: https://www.openssh.com/txt/release-8.9 supportsHashSelection = sshVersion >= 8.9 ) tests := []struct { name string publicKey string privateKey string }{ {"ed25519", ed25519PublicKey, ed25519PrivateKey}, {"ecdsa", ecdsaPublicKey, ecdsaPrivateKey}, {"rsa", rsaPublicKey, rsaPrivateKey}, } for _, tt := range tests { tt := tt for _, a := range sshsig.SupportedHashAlgorithms() { algo := a t.Run(fmt.Sprintf("%s-%s", tt.name, algo), func(t *testing.T) { if !supportsHashSelection && algo == sshsig.HashSHA256 { t.Skipf("skipping: ssh-keygen %v does not allow selection of hash at sshsig signing time", sshVersion) } // Make test go brrrr... t.Parallel() // Temporary directory used as working directory for ssh-keygen. tmp := t.TempDir() // Write the private key to a file, has to end with newline or // ssh-keygen will complain with "couldn't load". keyFile := filepath.Join(tmp, "id") assert.NoError(t, os.WriteFile(keyFile, []byte(tt.privateKey+"\n"), 0o600)) // Write the public key to a file as well. This is required // because OpenSSH <8.3 does not support reading the public key // from the private key file. pubFile := filepath.Join(tmp, "id.pub") assert.NoError(t, os.WriteFile(pubFile, []byte(tt.publicKey+"\n"), 0o600)) // Write the message to a file. msgFile := filepath.Join(tmp, "message") assert.NoError(t, os.WriteFile(msgFile, testMessage, 0o600)) // Sign the message. args := []string{"-Y", "sign", "-n", testNamespace, "-f", keyFile} if supportsHashSelection { args = append(args, "-O", "hashalg="+algo.String()) } _, err := execOpenSSH(t, tmp, nil, append(args, msgFile)...) assert.NoError(t, err) // Read and unmarshal signature. sigB, err := os.ReadFile(msgFile + ".sig") assert.NoError(t, err) sig, err := sshsig.Unarmor(sigB) assert.NoError(t, err) // Load the public key. pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(tt.publicKey)) assert.NoError(t, err) // Verify the signature. err = sshsig.Verify(bytes.NewReader(testMessage), sig, pub, sig.HashAlgorithm, testNamespace) assert.NoError(t, err) // Different key. otherPub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(otherPublicKey)) assert.NoError(t, err) err = sshsig.Verify(bytes.NewReader(testMessage), sig, otherPub, sig.HashAlgorithm, testNamespace) assert.ErrorIs(t, err, sshsig.ErrPublicKeyMismatch) // Different algorithm. err = sshsig.Verify(bytes.NewReader(testMessage), sig, pub, oppositeAlgorithm(algo), testNamespace) assert.Error(t, err) // Different namespace. err = sshsig.Verify(bytes.NewReader(testMessage), sig, pub, sig.HashAlgorithm, "other") assert.ErrorIs(t, err, sshsig.ErrNamespaceMismatch) // Different data. err = sshsig.Verify(bytes.NewReader([]byte("other")), sig, pub, sig.HashAlgorithm, testNamespace) assert.Error(t, err) }) } } } // allowedSigner returns the identifier (comment) of the key, and the row for // the allowed_signers file. func allowedSigner(t *testing.T, publicKey string) (id, row string) { t.Helper() fields := strings.Fields(publicKey) if len(fields) != 3 { t.Fatalf("public key is missing element: %s", publicKey) } id = fields[2] row = fmt.Sprintf("%s %s %s", id, fields[0], fields[1]) return } // execOpenSSH executes ssh-keygen with the given arguments in the given dir, // and returns the combined output. When stdin is not nil, it is passed to // ssh-keygen. If ssh-keygen returns an error, the error is wrapped with the // combined output. func execOpenSSH(t *testing.T, dir string, stdin io.Reader, args ...string) ([]byte, error) { t.Helper() t.Logf("ssh-keygen %s", args) cmd := exec.Command("ssh-keygen", args...) cmd.Dir = dir cmd.Stdin = stdin b, err := cmd.CombinedOutput() if err != nil { return nil, fmt.Errorf("%w: %s", err, string(b)) } return b, nil } func getSSHVersion(t *testing.T) float64 { t.Helper() out, err := exec.Command("ssh", "-V").CombinedOutput() if err != nil { t.Fatalf("failed to get SSH version: %s", out) } re := regexp.MustCompile(`OpenSSH.*?_(\d+\.\d+)(p\d+)?`) matches := re.FindStringSubmatch(string(out)) if len(matches) < 2 { t.Fatalf("failed to parse SSH version: %s", out) } v, err := strconv.ParseFloat(matches[1], 64) if err != nil { t.Fatalf("failed to extract SSH version: %s", out) } return v } // oppositeAlgorithm returns the opposite hash algorithm. func oppositeAlgorithm(algo sshsig.HashAlgorithm) sshsig.HashAlgorithm { switch algo { case sshsig.HashSHA256: return sshsig.HashSHA512 case sshsig.HashSHA512: return sshsig.HashSHA256 default: panic("unknown hash algorithm") } } golang-github-hiddeco-sshsig-0.1.0/sign.go000066400000000000000000000125251472702143300204310ustar00rootroot00000000000000package sshsig import ( crand "crypto/rand" "errors" "fmt" "io" "golang.org/x/crypto/ssh" ) // ErrMissingNamespace is returned by Sign if the namespace value is missing. var ErrMissingNamespace = errors.New("missing namespace") // Signature represents the SSH signature of a message. It can be marshaled // into an SSH wire format using Marshal, or into an armored (PEM) format using // Armor. // // Manually construction of this type is not recommended. Use ParseSignature or // Unarmor instead to retrieve a Signature from a wire or armored (PEM) format. type Signature struct { // Version is the version of the signature format. // It currently supports version 1, any other value will be rejected with // ErrUnsupportedSignatureVersion. Version uint32 // PublicKey is the public key used to create the Signature. PublicKey ssh.PublicKey // Namespace is the domain of the signature, and is used to prevent signature // reuse across different applications. Namespace string // HashAlgorithm is the hash algorithm used to hash the Signature message. HashAlgorithm HashAlgorithm // Signature is the SSH signature of the hash of the message. Signature *ssh.Signature } // Marshal returns the Signature in SSH wire format. func (s Signature) Marshal() []byte { return blob{ Version: s.Version, PublicKey: string(s.PublicKey.Marshal()), Namespace: s.Namespace, HashAlgorithm: s.HashAlgorithm.String(), Signature: string(ssh.Marshal(s.Signature)), }.Marshal() } // ParseSignature parses a signature in SSH wire format into a Signature. // It returns an error if the signature is invalid. func ParseSignature(b []byte) (*Signature, error) { var sig blob if err := ssh.Unmarshal(b, &sig); err != nil { return nil, err } if err := sig.Validate(); err != nil { return nil, err } sshSig := ssh.Signature{} if err := ssh.Unmarshal([]byte(sig.Signature), &sshSig); err != nil { return nil, err } pub, err := ssh.ParsePublicKey([]byte(sig.PublicKey)) if err != nil { return nil, err } // For RSA signatures, the signature algorithm must be "rsa-sha2-512" or // "rsa-sha2-256". // xref: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig#L69-L72 if pub.Type() == ssh.KeyAlgoRSA && sshSig.Format != ssh.KeyAlgoRSASHA256 && sshSig.Format != ssh.KeyAlgoRSASHA512 { return nil, fmt.Errorf("invalid signature format %q: expected %q or %q", sshSig.Format, ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512) } return &Signature{ Version: sig.Version, PublicKey: pub, Namespace: sig.Namespace, HashAlgorithm: HashAlgorithm(sig.HashAlgorithm), Signature: &sshSig, }, nil } // Sign generates a signature of the message from the io.Reader using the // given ssh.Signer private key. The signature hash is computed using the provided // HashAlgorithm. // // The purpose of the namespace value is to specify an unambiguous interpretation // domain for the signature, e.g. file signing. This prevents cross-protocol // attacks caused by signatures intended for one intended domain being accepted // in another. The namespace must not be empty, or ErrMissingNamespace will be // returned. // // When the signer is an RSA key, the signature algorithm will always be // "rsa-sha2-512". This is the same default used by OpenSSH, and is required by // the SSH signature wire protocol. // // Sign returns a Signature containing the signed message and metadata, or an // error if the signing process fails. func Sign(m io.Reader, signer ssh.Signer, h HashAlgorithm, namespace string) (*Signature, error) { return SignWithRand(m, crand.Reader, signer, h, namespace) } // SignWithRand is like Sign, but uses the provided rand io.Reader to create any // necessary random values. Most callers likely want to use Sign instead. func SignWithRand(m, rand io.Reader, signer ssh.Signer, h HashAlgorithm, namespace string) (*Signature, error) { if namespace == "" { // xref: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig#LL57C13-L57C13 return nil, ErrMissingNamespace } if err := h.Available(); err != nil { return nil, err } hf := h.Hash() if _, err := io.Copy(hf, m); err != nil { return nil, err } mh := hf.Sum(nil) var ( sd = signedData{ Namespace: namespace, HashAlgorithm: h.String(), Hash: string(mh), } sig *ssh.Signature err error ) switch signer.PublicKey().Type() { case ssh.KeyAlgoRSA: // For RSA signatures, the signature algorithm must be "rsa-sha2-512" or // "rsa-sha2-256". We use the same "rsa-sha2-512" default as OpenSSH. // xref: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig#L69-L72 // xref: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/ssh-keygen.c#L1804-L1805 algo := ssh.KeyAlgoRSASHA512 // This should always succeed as an SSH signer must implement the // AlgorithmSigner, but we check anyway. as, ok := signer.(ssh.AlgorithmSigner) if !ok { return nil, fmt.Errorf("signer does not support non-default signature algorithm %q", algo) } if sig, err = as.SignWithAlgorithm(rand, sd.Marshal(), algo); err != nil { return nil, err } default: if sig, err = signer.Sign(rand, sd.Marshal()); err != nil { return nil, err } } return &Signature{ Version: sigVersion, PublicKey: signer.PublicKey(), Namespace: namespace, HashAlgorithm: h, Signature: sig, }, nil } golang-github-hiddeco-sshsig-0.1.0/sign_test.go000066400000000000000000000116611472702143300214700ustar00rootroot00000000000000package sshsig_test import ( "bytes" "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" "errors" "fmt" "testing" "testing/iotest" "github.com/stretchr/testify/assert" "golang.org/x/crypto/ssh" "github.com/hiddeco/sshsig" ) func TestSign(t *testing.T) { t.Run("empty namespace", func(t *testing.T) { got, err := sshsig.Sign(nil, nil, sshsig.HashSHA256, "") assert.ErrorIs(t, err, sshsig.ErrMissingNamespace) assert.Nil(t, got) }) t.Run("unsupported hash", func(t *testing.T) { got, err := sshsig.Sign(nil, nil, "invalid", "test") assert.ErrorIs(t, err, sshsig.ErrUnsupportedHashAlgorithm) assert.Nil(t, got) }) t.Run("message read error", func(t *testing.T) { mockErr := errors.New("read error") got, err := sshsig.Sign(iotest.ErrReader(mockErr), nil, sshsig.HashSHA256, "test") assert.ErrorIs(t, err, mockErr) assert.Nil(t, got) }) } func TestSignVerify(t *testing.T) { var ( testNamespace = "file" testMessage = []byte(`The problem with most conspiracy theories is that they seem to believe that for a group of people to behave in a way detrimental to the common good requires intent.`) ) keyTypes := []struct { name string generate func() (crypto.PublicKey, crypto.Signer, error) }{ {"ed25519", func() (crypto.PublicKey, crypto.Signer, error) { return ed25519.GenerateKey(rand.Reader) }}, {"ecdsa-p256", func() (crypto.PublicKey, crypto.Signer, error) { pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, nil, err } return &pk.PublicKey, pk, nil }}, {"ecdsa-p384", func() (crypto.PublicKey, crypto.Signer, error) { pk, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) if err != nil { return nil, nil, err } return &pk.PublicKey, pk, nil }}, {"ecdsa-p521", func() (crypto.PublicKey, crypto.Signer, error) { pk, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) if err != nil { return nil, nil, err } return &pk.PublicKey, pk, nil }}, {"rsa-1024", func() (crypto.PublicKey, crypto.Signer, error) { pk, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { return nil, nil, err } return &pk.PublicKey, pk, nil }}, {"rsa-2048", func() (crypto.PublicKey, crypto.Signer, error) { pk, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, nil, err } return &pk.PublicKey, pk, nil }}, {"rsa-3072", func() (crypto.PublicKey, crypto.Signer, error) { pk, err := rsa.GenerateKey(rand.Reader, 3072) if err != nil { return nil, nil, err } return &pk.PublicKey, pk, nil }}, // rsa-4096 and rsa-8192 are technically also possibilities, but take a long time to generate... } for _, a := range sshsig.SupportedHashAlgorithms() { algo := a for _, k := range keyTypes { key := k t.Run(fmt.Sprintf("%s-%s", algo, key.name), func(t *testing.T) { // Make test go brrrr... t.Parallel() // Generate a key to sign with. cPub, cSigner, err := key.generate() assert.NoError(t, err) pub, err := ssh.NewPublicKey(cPub) assert.NoError(t, err) signer, err := ssh.NewSignerFromSigner(cSigner) assert.NoError(t, err) // Sign with the first key. sig, err := sshsig.Sign(bytes.NewReader(testMessage), signer, algo, testNamespace) assert.NoError(t, err) // Confirm signature algorithm overwrite for RSA. if pub.Type() == ssh.KeyAlgoRSA { assert.Equal(t, sig.Signature.Format, ssh.KeyAlgoRSASHA512) } else { assert.Equal(t, sig.Signature.Format, pub.Type()) } // Round trip as much as possible. armored := sshsig.Armor(sig) assert.NotNil(t, armored) sigFromArmored, err := sshsig.Unarmor(armored) assert.NoError(t, err) // Verify the signature. assert.NoError(t, sshsig.Verify(bytes.NewReader(testMessage), sigFromArmored, pub, algo, testNamespace)) // Verify against other message (should fail). err = sshsig.Verify(bytes.NewReader([]byte("faulty")), sig, pub, algo, testNamespace) assert.Error(t, err) assert.NotErrorIs(t, err, sshsig.ErrPublicKeyMismatch) // Verify against other hash algorithm (should fail). assert.Error(t, sshsig.Verify(bytes.NewReader(testMessage), sig, pub, oppositeAlgorithm(algo), testNamespace)) // Generate a second key to verify (and fail) with. cOtherPub, _, err := key.generate() assert.NoError(t, err) otherPub, err := ssh.NewPublicKey(cOtherPub) assert.NoError(t, err) // Ensure the fingerprints are different. assert.False(t, ssh.FingerprintSHA256(pub) == ssh.FingerprintSHA256(otherPub)) // Verify against other key (should fail). assert.ErrorIs(t, sshsig.Verify(bytes.NewReader(testMessage), sig, otherPub, algo, testNamespace), sshsig.ErrPublicKeyMismatch) // Verify against other namespace (should fail). oterNamespace := "faulty" assert.Error(t, sshsig.Verify(bytes.NewReader(testMessage), sig, otherPub, algo, oterNamespace)) }) } } } golang-github-hiddeco-sshsig-0.1.0/verify.go000066400000000000000000000036751472702143300210030ustar00rootroot00000000000000package sshsig import ( "errors" "io" "golang.org/x/crypto/ssh" ) var ( // ErrPublicKeyMismatch is returned by Verify if the public key in the signature // does not match the public key used to verify the signature. ErrPublicKeyMismatch = errors.New("public key does not match") // ErrNamespaceMismatch is returned by Verify if the namespace in the signature // does not match the namespace used to verify the signature. ErrNamespaceMismatch = errors.New("namespace does not match") ) // Verify verifies the message from the io.Reader matches the Signature using // the given ssh.PublicKey and HashAlgorithm. // // The purpose of the namespace value is to specify an unambiguous interpretation // domain for the signature, e.g. file signing. This prevents cross-protocol // attacks caused by signatures intended for one intended domain being accepted // in another. Unlike Sign, the namespace value is not required to allow // verification of signatures created by looser implementations. // // Verify returns an error if the verification process fails. func Verify(m io.Reader, sig *Signature, pub ssh.PublicKey, h HashAlgorithm, namespace string) error { // Check that the public key in the signature matches the public key used to // verify the signature. If this is e.g. tricked in to a hash collision, it // will still be caught by the verification. if ssh.FingerprintSHA256(pub) != ssh.FingerprintSHA256(sig.PublicKey) { return ErrPublicKeyMismatch } // Check that namespace matches the namespace in the Signature. // If this is malformed, it will still be caught by the verification. if sig.Namespace != namespace { return ErrNamespaceMismatch } if err := h.Available(); err != nil { return err } hf := h.Hash() if _, err := io.Copy(hf, m); err != nil { return err } mh := hf.Sum(nil) return pub.Verify(signedData{ Namespace: namespace, HashAlgorithm: h.String(), Hash: string(mh), }.Marshal(), sig.Signature) } golang-github-hiddeco-sshsig-0.1.0/verify_test.go000066400000000000000000000025731472702143300220360ustar00rootroot00000000000000package sshsig_test import ( "crypto/ed25519" "crypto/rand" "errors" "testing" "testing/iotest" "github.com/stretchr/testify/assert" "golang.org/x/crypto/ssh" "github.com/hiddeco/sshsig" ) func TestVerify(t *testing.T) { t.Run("different namespace", func(t *testing.T) { cPub, _, err := ed25519.GenerateKey(rand.Reader) assert.NoError(t, err) pub, err := ssh.NewPublicKey(cPub) assert.NoError(t, err) namespaceA := "foo" namespaceB := "bar" err = sshsig.Verify(nil, &sshsig.Signature{ PublicKey: pub, Namespace: namespaceA, }, pub, sshsig.HashSHA256, namespaceB) assert.ErrorIs(t, err, sshsig.ErrNamespaceMismatch) }) t.Run("unsupported hash algorithm", func(t *testing.T) { cPub, _, err := ed25519.GenerateKey(rand.Reader) assert.NoError(t, err) pub, err := ssh.NewPublicKey(cPub) assert.NoError(t, err) err = sshsig.Verify(nil, &sshsig.Signature{ PublicKey: pub, }, pub, "unsupported", "") assert.ErrorIs(t, err, sshsig.ErrUnsupportedHashAlgorithm) }) t.Run("message read error", func(t *testing.T) { cPub, _, err := ed25519.GenerateKey(rand.Reader) assert.NoError(t, err) pub, err := ssh.NewPublicKey(cPub) assert.NoError(t, err) mockErr := errors.New("read error") err = sshsig.Verify(iotest.ErrReader(mockErr), &sshsig.Signature{ PublicKey: pub, }, pub, sshsig.HashSHA256, "") assert.ErrorIs(t, err, mockErr) }) } golang-github-hiddeco-sshsig-0.1.0/wire.go000066400000000000000000000044761472702143300204450ustar00rootroot00000000000000package sshsig import ( "errors" "fmt" "golang.org/x/crypto/ssh" ) var ( // ErrUnsupportedSignatureVersion is returned when the signature version is // not supported. ErrUnsupportedSignatureVersion = errors.New("unsupported signature version") // ErrInvalidMagicPreamble is returned when the magic preamble is invalid. ErrInvalidMagicPreamble = errors.New("invalid magic preamble") ) // sigVersion is the supported version of the SSH signature format. // xref: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig#L35 const sigVersion = 1 // magicPreamble is the six-byte sequence "SSHSIG". It is included to // ensure that manual signatures can never be confused with any message // signed during SSH user or host authentication. // xref: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig#L89-L91 var magicPreamble = [6]byte{'S', 'S', 'H', 'S', 'I', 'G'} // signedData represents data that is signed. // xref: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig#L79 type signedData struct { Namespace string Reserved string HashAlgorithm string Hash string } // Marshal returns the signed data in SSH wire format. func (s signedData) Marshal() []byte { return append(magicPreamble[:], ssh.Marshal(s)...) } // blob represents the SSH signature blob. // xref: https://github.com/openssh/openssh-portable/blob/V_9_2_P1/PROTOCOL.sshsig#L32 type blob struct { // MagicPreamble is included in the struct to ensure we can unmarshal the // blob correctly. MagicPreamble [6]byte Version uint32 PublicKey string Namespace string Reserved string HashAlgorithm string Signature string } // Validate returns an error if the blob is invalid. This does not check the // signature itself. func (b blob) Validate() error { if b.Version != sigVersion { return fmt.Errorf("%w %d: expected %d", ErrUnsupportedSignatureVersion, b.Version, sigVersion) } if b.MagicPreamble != magicPreamble { return fmt.Errorf("%w %q: expected %q", ErrInvalidMagicPreamble, b.MagicPreamble, magicPreamble) } if err := HashAlgorithm(b.HashAlgorithm).Supported(); err != nil { return err } return nil } // Marshal returns the blob in SSH wire format. func (b blob) Marshal() []byte { copy(b.MagicPreamble[:], magicPreamble[:]) return ssh.Marshal(b) } golang-github-hiddeco-sshsig-0.1.0/wire_test.go000066400000000000000000000024501472702143300214720ustar00rootroot00000000000000package sshsig import ( "testing" "github.com/stretchr/testify/assert" ) func Test_signedData_Marshal(t *testing.T) { t.Run("has magic preamble", func(t *testing.T) { s := signedData{} m := s.Marshal() assert.Equal(t, magicPreamble[:], m[:6]) }) } func Test_blob_Valid(t *testing.T) { t.Run("unsupported version", func(t *testing.T) { b := blob{ Version: 0, } assert.ErrorIs(t, b.Validate(), ErrUnsupportedSignatureVersion) }) t.Run("invalid magic preamble", func(t *testing.T) { b := blob{ Version: sigVersion, MagicPreamble: [6]byte{'a', 'b', 'c', 'd', 'e', 'f'}, } assert.ErrorIs(t, b.Validate(), ErrInvalidMagicPreamble) }) t.Run("unsupported hash algorithm", func(t *testing.T) { b := blob{ Version: sigVersion, MagicPreamble: magicPreamble, HashAlgorithm: "foo", } assert.ErrorIs(t, b.Validate(), ErrUnsupportedHashAlgorithm) }) t.Run("valid", func(t *testing.T) { b := blob{ Version: sigVersion, MagicPreamble: magicPreamble, HashAlgorithm: "sha256", } assert.NoError(t, b.Validate()) }) } func Test_blob_Marshal(t *testing.T) { t.Run("has magic preamble", func(t *testing.T) { b := blob{ Version: 1, MagicPreamble: magicPreamble, } m := b.Marshal() assert.Equal(t, magicPreamble[:], m[:6]) }) }