├── .github └── workflows │ ├── certs │ ├── cocert.pub │ ├── cocert0.key │ └── cocert1.key │ ├── publish.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .ko.yaml ├── .res ├── arch.excalidraw ├── arch.png ├── logo.png ├── usage.gif ├── use-case.excalidraw └── use-case.png ├── LEARNING.md ├── README.md ├── cmd ├── combine.go ├── decrypt.go ├── encrypt.go ├── generate.go ├── root.go ├── sign.go ├── split.go └── verify.go ├── go.mod ├── go.sum ├── main.go ├── pkg ├── password │ └── password.go └── signed │ ├── extractors.go │ ├── extractors_test.go │ ├── generators.go │ ├── generators_test.go │ ├── loaders.go │ ├── signer.go │ ├── types.go │ └── verifier.go └── test ├── combine.exp ├── combine_splitted.exp ├── cosign.key ├── e2e.bats ├── generate.exp ├── sign.exp ├── sign_pk.exp └── split.exp /.github/workflows/certs/cocert.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAnGt2ksIcVCDCxaF1TpJMTjhvwgSk 3 | 39O1COufymnb99jJ7YT0SIbeNuzn9OtUceShJHPq911DzAfEcjsWQB8QzK4BdYbS 4 | GGDD1A+07V8jCes9iXSPU8LT3iN11er2X9wqkg0WawVfAvI1oo4bP0KEwiTuuMxA 5 | nde7+i9mzNuAy/De0HM= 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /.github/workflows/certs/cocert0.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6 3 | OCwicCI6MX0sInNhbHQiOiI5ZEN5QUl4YjFxaEpnRTRNUDdUTjhuNmsvMHRFbmRo 4 | Mjd6eXdNNGx4TDdVPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94 5 | Iiwibm9uY2UiOiI5VVA5ZVhSRlk3aUZUYVBBTVM1SEZzMnVsVmE5bnByUSJ9LCJj 6 | aXBoZXJ0ZXh0IjoibUhZUE1rR3J4bTM5SmdZNDJKRWlTdGFTOXJ6YjZHVjVwcXBo 7 | R1BuRytXQTkwUHN0cHE2Q2V4d0NXTlN2NVRrMHQ2OEtlQjcyUzA0ODFuc29JUUQw 8 | ZDN4K0dxMEhWZjJrVzIxc1U5V043RmgzeVNOeVo0bGlxUmE1RHFBT2E1L3JpWDZi 9 | SXZDRmdEUHU5TTAveEJxRGs4Ym9IU2d1WlFQSEZaNjkrTWdPZmxFYkZkOC8zNzhP 10 | Sy95aVJ2S2Vwb2lsZmRLOXpZblZLcmZMWE4yY29SeDdVYUVkUG5GVjAzR0w0b05m 11 | SDJCdHVyVGFhTjU0SFlHVFhHYzVNVWMxMlpJbkJycldHZXRReTZNQTNDN3FnTThh 12 | ZVB6RmVQZ2lNTmQwS1dLS0ZkMDVDaWZ1RVV1WUExYmQyQkV5MjJCN0t6TG5teElQ 13 | bHRvSGpTV3FkY1ZEMGZJdTFncUdUUSt0UWJxSDc0YnNUZDNPYkFJdWJudnNMOGZF 14 | R1l6VnpsSm96cFN3bXhUMFhOMS8xb29oMlp3SEJpcnRnUkZ1azBEYW5nZlVBZjA5 15 | R29CWVZQbjJ5Q0tuME50ekJSKzV5UTR3QVJCd25KRW96R3VpWk5DWjh4Vi9CaC95 16 | Q2k1N1pHSjlhQ2xnUEFXaDFSNy9FWGtDM1RIQU14NzFsYnlSZ1hPN2oyV2xBZEpP 17 | cGVVQ3RuZFZZQWYrWGtRcnRqNzlQVE5VZ0k1NDVOL0VFaU9hV1k3UHJybXVHWS9a 18 | cWVraFc2d2YzeVFEY0xQRFRLejYrRDJWeUFNVUlIbG12L21teEo4VTNiZFpmZTBp 19 | aFpoR25xZ2JCTGRvU3g3ZlJ5RWFCK2FUV0xsUEluRzNwajFRdWh4ajY1NFJ5UjFB 20 | OCtCYnJwRU52cHQzTUVGWDVWYmxVdlhRN3FtVVgyUGVzWnB5M0JxMTJ6bHN6elZZ 21 | M1pSOHkvdjF0OElTc05MV1IxdVN4UGNUR3pBRm5ORW9mOUxtWXlaUXlDRlg4MUQ5 22 | In0= 23 | -----END ENCRYPTED PRIVATE KEY----- 24 | -------------------------------------------------------------------------------- /.github/workflows/certs/cocert1.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6 3 | OCwicCI6MX0sInNhbHQiOiJRemZ1NVB2SU8xTmV3NW9Nb2J1VUFxSitBRXV0aVNQ 4 | TVhGRGRDaFhWNUVrPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94 5 | Iiwibm9uY2UiOiJwNjR1VDcyVHY4MjkzOFB3V3hqWkRwY0pCTjROZVRWNCJ9LCJj 6 | aXBoZXJ0ZXh0IjoicHZaaGNBRVMvdmJ6RzR4RFBHZDZoYm5GVktKbkQyN0tpeVlN 7 | ekZRWDUrTkpyY2tDMHhuR1RraWMyRGh2Z0RzUFZndE1CNWtpYUtwYlhzTkZmYXN2 8 | YWdMMzdnY1c0azhOdlA5VElUWHJBV0wyN052Q3dWUm5HTExyeGFHOTJYT01PV1ZU 9 | aXVKRkVJQmE5RjZOUWN0R0c2RzA5NENxdE9jYW1Yd3ZuYUFNUHVJWmZ3V2xoNmV0 10 | N29vZnF4K0cwMkdPbDlvalo1dm1BNTBZaEpPRVkvTFRKNHJWanZCVDZRREU2TzJ5 11 | QXBKb0E0WGJKMW42RzdheGNxdENOeEtrbXN6c3lVVW5tV2RkVG5pRERyV1Nqb2Ju 12 | YXZ5eFVhd2wvaVgrUUZUdnJQMHJyQ2R6QjMwVEVQOSt3bHdZTjdQQ1dBcFZVVGtV 13 | S2dHZk9DaEdVN1ZDSFp0VlVyWklNUEhtb2ZmeEx2WnA4ZlEvRTlSbjBjUy83UnJF 14 | TVZWR1psdmRtWG5YN1pMS0JYQUo3azVjaTVnQlA0b2FodS9abFFCYTVWTFVmZVJP 15 | MTBvdWpUSWxsU0FZSXQ0K3E1Y2gzMjlzUmsvZThoODhPbDFzL2Fsbm9Pd01CL0ZT 16 | QUV2TTNsQmVqMTBNck9hblZzc3dtTzU0NzFFUUVGbjI2NHh4RG9ZNHdBanBEN21I 17 | dHM2VUtna1N1eG9WakM1dHdpVzJsOTBnN3A2VitIK2Z2cUxlOGY3WDlXNlVDNWtS 18 | aU1JekF5OWxocUJuL2ttTXVib1lETWZvZFlUSm11SGF3NlpIQnNoNGFQWVFVT0Uy 19 | MkZvZExhS1l4VVdQcXQ5RENBNitPUlhIenc1d3V2VEhMZFZYVWgxb0h4WU5pb3g3 20 | Ty9oMGdyMWNpRW5wZHZEVXVMZDVPZ0hRbjg2RG4wd1NaRWNQMFc5a3pVNVNNbnpM 21 | VDU2UjF5UTFUSHhWa3k2SDRBMExxQ0dYa3RkUG5UTjVKWW1nQnNnR3ZjUVJZVjdW 22 | In0= 23 | -----END ENCRYPTED PRIVATE KEY----- 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Furkan Türkal 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | # this software and associated documentation files (the "Software"), to deal in 5 | # the Software without restriction, including without limitation the rights to 6 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | # the Software, and to permit persons to whom the Software is furnished to do so, 8 | # subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | name: Publish 21 | 22 | on: 23 | push: 24 | tags: 25 | - "v[0-9]+.[0-9]+.[0-9]+*" 26 | workflow_run: 27 | workflows: [ "Release" ] 28 | branches: [ main ] 29 | types: 30 | - completed 31 | 32 | jobs: 33 | publish: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: Get TAG 38 | id: get_tag 39 | run: echo ::set-output name=TAG::${GITHUB_REF#refs/tags/} 40 | - uses: sigstore/cosign-installer@main 41 | with: 42 | cosign-release: 'v0.4.0' 43 | - uses: actions/setup-go@v2 44 | with: 45 | go-version: '1.16.3' 46 | - name: Install ko 47 | run: | 48 | curl -L https://github.com/google/ko/releases/download/v0.8.2/ko_0.8.2_Linux_x86_64.tar.gz | tar xzf - ko && \ 49 | chmod +x ./ko && sudo mv ko /usr/local/bin/ 50 | - name: Login to Docker Registry 51 | uses: docker/login-action@v1 52 | with: 53 | username: ${{ secrets.DOCKER_USERNAME }} 54 | password: ${{ secrets.DOCKER_PASSWORD }} 55 | registry: ghcr.io 56 | - name: Sign & Publish 57 | run: | 58 | set -x 59 | 60 | curl https://gist.githubusercontent.com/Dentrax/ea76daab84bcd90953397b31f12a28f3/raw/4d307796d7b9c94b6e99f26afdbddce879a7fe0b/cocert2.key -o cocert2.key 61 | 62 | echo -n "${{secrets.PASSWORD_COCERT_KEY0}}" | go run . decrypt -f .github/workflows/certs/cocert0.key -o cocert0.key.decrypted 63 | echo -n "${{secrets.PASSWORD_COCERT_KEY2}}" | go run . decrypt -f cocert2.key -o cocert2.key.decrypted 64 | echo -n "${{secrets.PASSWORD_COCERT_MASTER}}" | go run . combine -F cocert0.key.decrypted -F cocert2.key.decrypted -o private.key -t "ENCRYPTED COSIGN PRIVATE KEY" 65 | 66 | GIT_HASH=$(git rev-parse HEAD) 67 | export KO_DOCKER_REPO=ghcr.io/dentrax/cocert 68 | 69 | ko publish --bare ./ 70 | echo -n "${{secrets.PASSWORD_COCERT_MASTER}}" | cosign sign -key private.key -a GIT_HASH=$GIT_HASH $KO_DOCKER_REPO -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Furkan Türkal 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | # this software and associated documentation files (the "Software"), to deal in 5 | # the Software without restriction, including without limitation the rights to 6 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | # the Software, and to permit persons to whom the Software is furnished to do so, 8 | # subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | name: Release 21 | 22 | on: 23 | push: 24 | tags: 25 | - "v[0-9]+.[0-9]+.[0-9]+*" 26 | 27 | jobs: 28 | goreleaser: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Setup Go 33 | uses: actions/setup-go@v2 34 | with: 35 | go-version: 1.16 36 | - name: Run GoReleaser 37 | uses: goreleaser/goreleaser-action@v2 38 | with: 39 | version: latest 40 | args: release --rm-dist 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Furkan Türkal 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | # this software and associated documentation files (the "Software"), to deal in 5 | # the Software without restriction, including without limitation the rights to 6 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | # the Software, and to permit persons to whom the Software is furnished to do so, 8 | # subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | name: Test 21 | 22 | on: 23 | push: 24 | paths-ignore: 25 | - '**.md' 26 | pull_request: 27 | paths-ignore: 28 | - '**.md' 29 | 30 | jobs: 31 | Build: 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | go-version: [1.16.x] 36 | platform: [ubuntu-latest] 37 | runs-on: ${{ matrix.platform }} 38 | steps: 39 | - name: Setup Go ${{ matrix.go }} 40 | uses: actions/setup-go@v1 41 | with: 42 | go-version: ${{ matrix.go-version }} 43 | - name: Fetch Repository 44 | uses: actions/checkout@v2 45 | - name: Tidy 46 | run: go mod tidy 47 | - name: Build 48 | run: go build -v . 49 | - name: Test 50 | run: go test ./... -v -race 51 | - name: Setup Prerequisites 52 | run: | 53 | sudo npm install -g bats --force 54 | sudo apt-get install expect 55 | - name: E2E Test 56 | run: pushd ./test; bats ./e2e.bats; popd 57 | 58 | golangci: 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v2 62 | - name: golangci-lint 63 | uses: golangci/golangci-lint-action@v2 64 | with: 65 | version: v1.39 66 | 67 | analyze: 68 | name: Analyze 69 | runs-on: ubuntu-latest 70 | steps: 71 | - uses: actions/checkout@v2 72 | - name: Initialize CodeQL 73 | uses: github/codeql-action/init@v1 74 | with: 75 | languages: go 76 | - name: Autobuild 77 | uses: github/codeql-action/autobuild@v1 78 | - name: Perform CodeQL Analysis 79 | uses: github/codeql-action/analyze@v1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### IDE 2 | .idea 3 | 4 | ### Go 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | .DS_Store 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | vendor/ 22 | 23 | *.cert 24 | 25 | cocert -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | concurrency: 4 3 | timeout: 5m 4 | 5 | linters: 6 | enable: 7 | - asciicheck 8 | - structcheck 9 | - varcheck 10 | - staticcheck 11 | - stylecheck 12 | - prealloc 13 | - gofmt 14 | - goimports 15 | - golint 16 | - gosec 17 | - ineffassign 18 | - vet 19 | - unused 20 | - unparam 21 | - unconvert 22 | - misspell 23 | - revive 24 | 25 | issues: 26 | exclude-rules: 27 | - path: test 28 | linters: 29 | - gosec -------------------------------------------------------------------------------- /.ko.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Furkan Türkal 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | # this software and associated documentation files (the "Software"), to deal in 5 | # the Software without restriction, including without limitation the rights to 6 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | # the Software, and to permit persons to whom the Software is furnished to do so, 8 | # subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | defaultBaseImage: gcr.io/distroless/static:nonroot -------------------------------------------------------------------------------- /.res/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dentrax/cocert/c1c2126cc65e492ff297db5494e7207be434684f/.res/arch.png -------------------------------------------------------------------------------- /.res/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dentrax/cocert/c1c2126cc65e492ff297db5494e7207be434684f/.res/logo.png -------------------------------------------------------------------------------- /.res/usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dentrax/cocert/c1c2126cc65e492ff297db5494e7207be434684f/.res/usage.gif -------------------------------------------------------------------------------- /.res/use-case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dentrax/cocert/c1c2126cc65e492ff297db5494e7207be434684f/.res/use-case.png -------------------------------------------------------------------------------- /LEARNING.md: -------------------------------------------------------------------------------- 1 | https://github.com/sigstore/cosign 2 | 3 | https://darutk.medium.com/illustrated-x-509-certificate-84aece2c5c2e 4 | 5 | https://github.com/hashicorp/vault/tree/master/sdk/helper/password 6 | 7 | https://en.wikipedia.org/wiki/Polynomial 8 | 9 | https://hackernoon.com/shamir-secret-sharing-vs-multi-sig-124a42bc1662 10 | 11 | https://en.bitcoin.it/w/index.php?title=Multi-signature&ref=hackernoon.com 12 | 13 | https://blog.keys.casa/shamirs-secret-sharing-security-shortcomings/ 14 | 15 | https://btcarmory.com/fragmented-backup-vuln/ 16 | 17 | https://twitter.com/coinbase/status/738837596709740544 18 | 19 | https://news.ycombinator.com/item?id=21600899 20 | 21 | https://github.com/ace0/q 22 | 23 | https://eprint.iacr.org/2012/377.pdf 24 | 25 | https://eprint.iacr.org/2021/005.pdf 26 | 27 | https://eprint.iacr.org/2019/114.pdf (Fast Multiparty Threshold ECDSA with Fast Trustless Setup) 28 | 29 | https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.124.4128&rep=rep1&type=pdf (ECDKG:A Distributed Key Generation Protocol Based on Elliptic Curve Discrete Logarithm) 30 | 31 | https://eprint.iacr.org/2016/1067.pdf (Scalable Bias-Resistant Distributed Randomness) 32 | 33 | https://arxiv.org/pdf/1902.10313.pdf 34 | 35 | https://www.researchgate.net/publication/319409122_An_Advanced_Encryption_Standard_Powered_Mutual_Authentication_Protocol_Based_on_Elliptic_Curve_Cryptography_for_RFID_Proven_on_WISP#pf9 36 | 37 | https://medium.com/loopring-protocol/learning-cryptography-finite-fields-ced3574a53fe 38 | 39 | https://reposhub.com/java/distributed-applications/ZenGo-X-awesome-tss.html 40 | 41 | https://github.com/dedis/kyber/tree/master/share/dkg 42 | 43 | https://github.com/drand/drand 44 | 45 | https://github.com/jchavannes/go-pgp 46 | 47 | https://github.com/ace0/q 48 | 49 | https://medium.com/asecuritysite-when-bob-met-alice/galios-fields-gf-2-n-34d817a2fba9 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

cocert

4 | 5 |
6 | 7 | An experimental tool for splitting and distributing your private keys safely* 8 | 9 |
10 | 11 |
12 | 13 |

14 | MIT 15 | GitHub release 16 | Go Report 17 | Build Status 18 |

19 | 20 |
21 | 22 | *cocert*, generates [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) - [P521](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) key and uses a technique known as [Shamir's Secret Sharing](https://en.wikipedia.org/wiki/Shamir's_Secret_Sharing) algorithm to split the master key into _x_ shares, any _y_ of which are required to reconstruct the master private key. Private keys are stored in [PEM-encoded](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) [PKCS8](https://en.wikipedia.org/wiki/PKCS_8) format, which are [encrypted](https://pkg.go.dev/github.com/theupdateframework/go-tuf/encrypted) by [The Update Framework (TUF)](https://github.com/theupdateframework/go-tuf). Each private key is splitted using [Shamir](https://pkg.go.dev/github.com/hashicorp/vault/shamir) [Split](https://pkg.go.dev/github.com/hashicorp/vault/shamir#Split). To [Combine](https://pkg.go.dev/github.com/hashicorp/vault/shamir#Combine) private key files into single one, it is necessary to enter decrypt password if it has been encrypted by TUF. 23 | 24 | *cocert does not support any [Distributed Key Generation (DGK)](https://en.wikipedia.org/wiki/Distributed_key_generation) algorithm, yet. 25 | 26 | _This repository is [signed](https://github.com/Dentrax/cocert/blob/9d9f18743c9602289dfec3c98d49b68c549d40bf/.github/workflows/publish.yml#L56-L69) via [cosign](https://github.com/sigstore/cosign), by using `cocert` itself_ 27 | 28 | ![GIF](.res/usage.gif) 29 | 30 | [Asciinema](https://asciinema.org/a/411543) 31 | 32 | # High Level Architecture 33 | 34 | ![Screenshot](.res/arch.png) 35 | 36 | # Use-Case Example 37 | 38 | * What happens if your private key is exposed by either public 3rd-party cloud service provider or internal security breach? 39 | 40 | Your private key would have compromised and supply chain attacks would inevitable. What would happen if we not trust just one key, however, distribute our key to multiple secure environments? We would avoid supply chain attacks, that said, even if one of our private keys is compromised, we still need two more keys to combine and get the final private key. 41 | 42 | ![Screenshot](.res/use-case.png) 43 | 44 | # Installation 45 | 46 | * Go 47 | ```bash 48 | $ go install github.com/Dentrax/cocert@latest 49 | ``` 50 | 51 | * Docker 52 | ```bash 53 | $ docker pull ghcr.io/dentrax/cocert 54 | ``` 55 | 56 | # Verify 57 | 58 | ## Prerequities 59 | 1. [cosign](https://github.com/sigstore/cosign) 60 | 2. [crane](https://github.com/google/go-containerregistry/tree/main/cmd/crane) 61 | 62 | ## Check 63 | 64 | ```bash 65 | # 1. Download the public key 66 | $ curl https://raw.githubusercontent.com/Dentrax/cocert/main/.github/workflows/certs/cocert.pub -o cocert.pub 67 | 68 | # 2. Verify 69 | $ cosign verify -key cocert.pub ghcr.io/dentrax/cocert | jq 70 | 71 | # 3. Make sure verified commit matches the digest of the latest image 72 | $ crane digest ghcr.io/dentrax/cocert 73 | ``` 74 | 75 | # Usage 76 | 77 | ```bash 78 | Usage: 79 | cocert [command] 80 | 81 | Available Commands: 82 | combine Combine the cert integrity on the supplied PEM files 83 | decrypt Decrypt the target private keys using TUF 84 | encrypt Encrypt the target private keys using TUF 85 | generate Generates TUF encrypted keys using ECDSA and splits into PKCS8-PKIX key-pairs 86 | help Help about any command 87 | sign Sign the given payload and create a certificate from Fulcio 88 | split Split your existing private key into parts 89 | verify Verify the given payload on the supplied signature 90 | 91 | Flags: 92 | -h, --help help for cocert 93 | ``` 94 | 95 | ## Use-Case Demonstration 96 | 97 | 1. Generate 98 | ```bash 99 | $ cocert generate --parts 3 --threshold 2 100 | 101 | Generating TUF encrypted Shamir PEMs... 102 | Create new password for private key: (master) 103 | Confirm password: (master) 104 | Extracting PEMs to files... 105 | Do you want to encrypt each key using TUF? (y/n) [n]: y 106 | Create new password for cocert0.key key: (foo) 107 | Create new password for cocert1.key key: (bar) 108 | Create new password for cocert2.key key: (baz) 109 | ``` 110 | 111 | 2.1. Sign with Private Key 112 | ```bash 113 | $ cocert sign -f cocert0.key -f cocert1.key -p "Foo Bar Baz" 114 | 115 | (Press Enter to continue without decrypt...) 116 | Enter your password for cocert0.key: (foo) 117 | Enter your password for cocert1.key: (bar) 118 | Enter your master key: (master) 119 | Signed: MIGIAkIBCisWXRLBRcv/...+3pccRjm+nUNA== 120 | ``` 121 | 122 | 2.2. Sign with [Fulcio](https://github.com/sigstore/fulcio) (Keyless) 123 | ```bash 124 | $ cocert sign -f cocert0.key -f cocert1.key -p "Foo Bar Baz" -o my.cert 125 | 126 | (Press Enter to continue without decrypt...) 127 | Enter your password for cocert0.key: (foo) 128 | Enter your password for cocert1.key: (bar) 129 | Enter your master key: (master) 130 | Your browser will now be opened to: 131 | https://oauth2.sigstore.dev/auth/auth?access_type=online&client_id=sigstore&code_challenge=CODE&code_challenge_method=S256&nonce=NONCE&redirect_uri=http%3A%2F%2Flocalhost%3A5556%2Fauth%2Fcallback&response_type=code&scope=openid+email&state=STATE 132 | Signed: MIGIAkIBCisWXRLBRcv/...+3pccRjm+nUNA== 133 | ``` 134 | 135 | 3.1. Verify with Public Key 136 | ```bash 137 | $ cocert verify -f cocert.pub -p "Foo Bar Baz" -k "MIGIAkIBCisWXRLBRcv/...+3pccRjm+nUNA==" 138 | ``` 139 | 140 | 3.2. Verify with Certificate 141 | ```bash 142 | $ cocert verify -c my.cert -p "Foo Bar Baz" -k "MIGIAkIBCisWXRLBRcv/...+3pccRjm+nUNA==" 143 | ``` 144 | 145 | **Bonus:** Splitting 146 | ```bash 147 | # 1. Generate the your custom private key 148 | $ cosign generate-key-pair 149 | 150 | Enter password for private key: (qux) 151 | Private key written to cosign.key 152 | Public key written to cosign.pub 153 | 154 | # 2. Split the key 155 | $ cocert split -f private.key --parts 3 --threshold 2 156 | 157 | Create new password for cocert0.key key: (foo) 158 | Create new password for cocert1.key key: (bar) 159 | Create new password for cocert2.key key: (baz) 160 | 161 | # 3. Test with combine 162 | $ cocert combine -f cocert0.key -f cocert1.key -o cosign.key 163 | 164 | Enter your password for cocert0.key: (foo) 165 | Enter your password for cocert1.key: (bar) 166 | Decrypting TUF encrypted PEMs... 167 | Enter your master key: (qux) 168 | Combined 169 | ``` 170 | 171 | ## Encrypt & Decrypt Keys 172 | 173 | * Encrypt 174 | ```bash 175 | $ cocert encrypt -f cocert0.key -o "cocert0.key.encrypted" 176 | 177 | Enter your password for : (foo2) 178 | Confirm password: (foo2) 179 | ``` 180 | 181 | * Decrypt 182 | ```bash 183 | $ cocert decrypt -f cocert0.key.encrypted -o "cocert0.key.decrypted" 184 | # [[ $(md5 -q cocert0.key) -eq $(md5 -q cocert0.key.decrypted) ]] 185 | 186 | Enter your password for : (foo2) 187 | 188 | $ cocert decrypt -f cocert0.key.decrypted -o "cocert0.key.unencrypted" 189 | # You can pass empty password for 'cocert0.key.unencrypted' key 190 | 191 | Enter your password for : (foo) 192 | ``` 193 | 194 | * Combine 195 | 196 | ```bash 197 | $ cocert combine -f cocert0.key.unencrypted -f cocert1.key 198 | 199 | Loading PEMs from files... 200 | (Press Enter to continue without decrypt...) 201 | Enter your password for cocert0.key.unencrypted: (PASS) 202 | Enter your password for cocert1.key: (bar) 203 | Decrypting TUF encrypted PEMs... 204 | Enter your master key: (master) 205 | Combined 206 | ``` 207 | 208 | # Special Thanks 209 | 210 | | Package | Author | License | 211 | | :------------------------------------------------------------ | :------------------------------------------------------ | :------------------------------------------------------------------------------------------- | 212 | | [cosign](https://github.com/sigstore/cosign) | [sigstore](https://github.com/sigstore) | [Apache License 2.0](https://github.com/sigstore/cosign/blob/main/LICENSE) | 213 | | [go-tuf](https://github.com/theupdateframework/go-tuf) | [The Update Framework](https://github.com/theupdateframework) | [BSD](https://github.com/theupdateframework/go-tuf/blob/master/LICENSE) | 214 | | [Vault](https://github.com/hashicorp/vault) | [HashiCorp](https://github.com/hashicorp) | [Mozilla Public License 2.0](https://github.com/hashicorp/vault/blob/master/LICENSE) | 215 | | [prompter](https://github.com/Songmu/prompter) | [Songmu](https://github.com/Songmu) | [MIT](https://github.com/Songmu/prompter/blob/main/LICENSE) | 216 | 217 | - Thanks to everyone who contributed these libraries and [others](https://github.com/Dentrax/cocert/blob/main/go.mod) that made this project possible. 218 | 219 | # License 220 | 221 | *cocert* was created by [Furkan 'Dentrax' Türkal](https://twitter.com/furkanturkaI) 222 | 223 | The base project code is licensed under [MIT](https://opensource.org/licenses/MIT) unless otherwise specified. Please see the **[LICENSE](https://github.com/Dentrax/cocert/blob/main/LICENSE)** file for more information. 224 | 225 | Best Regards -------------------------------------------------------------------------------- /cmd/combine.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package cmd 21 | 22 | import ( 23 | "fmt" 24 | "os" 25 | 26 | "github.com/Dentrax/cocert/pkg/password" 27 | "github.com/Dentrax/cocert/pkg/signed" 28 | 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | var ( 33 | combineEncryptedFiles []string 34 | combineUnEncryptedFiles []string 35 | combineOutput string 36 | combinePEMType string 37 | ) 38 | 39 | var cmdCombine = &cobra.Command{ 40 | Use: "combine {-f file}...", 41 | Short: "Combine the cert integrity on the supplied PEM files", 42 | Long: ` 1. Scan every given certs from path 43 | 2. Read every file and decode PEM 44 | 3. Use Shamir to Combine all private keys 45 | 4. Use TUF to decrypt private key 46 | `, 47 | Run: func(cmd *cobra.Command, args []string) { 48 | fmt.Fprintln(os.Stdout, "Loading PEMs from files...") 49 | s, err := signed.LoadAndCombinePrivateKeysFromPaths(password.GetPass, combineEncryptedFiles, combineUnEncryptedFiles) 50 | not(err) 51 | 52 | fmt.Fprintln(os.Stdout, "Decrypting TUF encrypted PEMs...") 53 | _, err = signed.DecryptTUFEncryptedPrivateKey(s, password.GetPass) 54 | not(err) 55 | 56 | if combineOutput != "" { 57 | err = signed.EncodePEMToFileOrOutputWithType(combineOutput, s, combinePEMType) 58 | not(err) 59 | } 60 | 61 | fmt.Fprintln(os.Stdout, "Combined") 62 | }, 63 | } 64 | 65 | func init() { 66 | cmdCombine.PersistentFlags().StringSliceVarP(&combineEncryptedFiles, "encrypted-file", "f", []string{}, "splitted encrypted file to combine (ask prompt password)") 67 | cmdCombine.PersistentFlags().StringSliceVarP(&combineUnEncryptedFiles, "unencrypted-file", "F", []string{}, "unecrypted file to combine") 68 | cmdCombine.PersistentFlags().StringVarP(&combineOutput, "output", "o", "", "print PEM content to output file") 69 | cmdCombine.PersistentFlags().StringVarP(&combinePEMType, "type", "t", "", "overwrite PEM type header") 70 | _ = cmdCombine.MarkPersistentFlagRequired("file") 71 | 72 | rootCmd.AddCommand(cmdCombine) 73 | } 74 | -------------------------------------------------------------------------------- /cmd/decrypt.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package cmd 21 | 22 | import ( 23 | "fmt" 24 | 25 | "github.com/Dentrax/cocert/pkg/signed" 26 | 27 | "github.com/spf13/cobra" 28 | "github.com/theupdateframework/go-tuf/encrypted" 29 | ) 30 | 31 | var ( 32 | decryptFile, decryptInput, decryptOutput, decryptKey string 33 | ) 34 | 35 | var cmdDecrypt = &cobra.Command{ 36 | Use: "decrypt {-f file | -i STDIN} [-k key] [-o output]", 37 | Short: "Decrypt the target private keys using TUF", 38 | Long: ``, 39 | PreRunE: func(cmd *cobra.Command, args []string) error { 40 | if decryptFile != "" && decryptInput != "" { 41 | return fmt.Errorf("--file and --input are mutually exclusive, can not be set at same time") 42 | } 43 | if decryptFile == "" && decryptInput == "" { 44 | return fmt.Errorf("one of the --file or --input are required") 45 | } 46 | return nil 47 | }, 48 | Run: func(cmd *cobra.Command, args []string) { 49 | b, err := signed.DecodePEMFromFile("", readFileOrInput(decryptFile, decryptInput), decidePassFunc(decryptKey), encrypted.Decrypt, false, false) 50 | not(err) 51 | 52 | err = signed.EncodePEMToFileOrOutput(decryptOutput, b) 53 | not(err) 54 | }, 55 | } 56 | 57 | func init() { 58 | cmdDecrypt.Flags().StringVarP(&decryptFile, "file", "f", "", "file path to decrypt") 59 | cmdDecrypt.Flags().StringVarP(&decryptInput, "input", "i", "", "file content to decrypt") 60 | cmdDecrypt.Flags().StringVarP(&decryptOutput, "output", "o", "", "print PEM content to output file") 61 | cmdDecrypt.Flags().StringVarP(&decryptKey, "key", "k", "", "decryption key (DO NOT RECOMMENDED - use non-echoed instead)") 62 | 63 | rootCmd.AddCommand(cmdDecrypt) 64 | } 65 | -------------------------------------------------------------------------------- /cmd/encrypt.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package cmd 21 | 22 | import ( 23 | "fmt" 24 | 25 | "github.com/Dentrax/cocert/pkg/signed" 26 | 27 | "github.com/spf13/cobra" 28 | "github.com/theupdateframework/go-tuf/encrypted" 29 | ) 30 | 31 | var ( 32 | encryptFile, encryptInput, encryptOutput, encryptKey string 33 | ) 34 | 35 | var cmdEncrypt = &cobra.Command{ 36 | Use: "encrypt {-f file | -i STDIN} [-k key] [-o output]", 37 | Short: "Encrypt the target private keys using TUF", 38 | Long: ``, 39 | PreRunE: func(cmd *cobra.Command, args []string) error { 40 | if encryptFile != "" && encryptInput != "" { 41 | return fmt.Errorf("--file and --input are mutually exclusive, can not be set at same time") 42 | } 43 | if encryptFile == "" && encryptInput == "" { 44 | return fmt.Errorf("one of the --file or --input are required") 45 | } 46 | return nil 47 | }, 48 | Run: func(cmd *cobra.Command, args []string) { 49 | b, err := signed.DecodePEMFromFile("", readFileOrInput(encryptFile, encryptInput), decidePassFunc(encryptKey), encrypted.Encrypt, true, true) 50 | not(err) 51 | 52 | err = signed.EncodePEMToFileOrOutput(encryptOutput, b) 53 | not(err) 54 | }, 55 | } 56 | 57 | func init() { 58 | cmdEncrypt.Flags().StringVarP(&encryptFile, "file", "f", "", "file path to decrypt") 59 | cmdEncrypt.Flags().StringVarP(&encryptInput, "input", "i", "", "file content to decrypt") 60 | cmdEncrypt.Flags().StringVarP(&encryptOutput, "output", "o", "", "print PEM content to output file") 61 | cmdEncrypt.Flags().StringVarP(&encryptKey, "key", "k", "", "decryption key (DO NOT RECOMMENDED - use non-echoed instead)") 62 | 63 | rootCmd.AddCommand(cmdEncrypt) 64 | } 65 | -------------------------------------------------------------------------------- /cmd/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package cmd 21 | 22 | import ( 23 | "fmt" 24 | "os" 25 | 26 | "github.com/Dentrax/cocert/pkg/password" 27 | "github.com/Dentrax/cocert/pkg/signed" 28 | 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | var ( 33 | parts, threshold uint8 34 | ) 35 | 36 | var cmdGenerate = &cobra.Command{ 37 | Use: "generate [-p parts] [-t threshold]", 38 | Short: "Generates TUF encrypted keys using ECDSA and splits into PKCS8-PKIX key-pairs", 39 | Long: `1. Generate ellipticP521 using ECDSA - elliptic.P521. 40 | 2. Generate a private key using x509.PKCS8 41 | 3. Generate a public key using x509.PKIX 42 | 4. Ask for password using 'vault/sdk/helper/password' and encrypt the private key by 'go-tuf/encrypted' algorithm 43 | 5. Generate PEM file from public key 44 | 6. Split the encrypted private key value using Shamir's Secret Sharing algorithm (vault/shamir) 45 | 7. Encode the all returned shares to PEM 46 | 8. Extract those to files 47 | 48 | * The parts and threshold must be at least 2, and less than 256. 49 | `, 50 | PreRun: func(cmd *cobra.Command, args []string) { 51 | if parts < 2 || parts > 255 { 52 | not(fmt.Errorf("the parts must be at least 2, and less than 256")) 53 | } 54 | if threshold < 2 || threshold > 255 { 55 | not(fmt.Errorf("the threshold must be at least 2, and less than 256")) 56 | } 57 | }, 58 | Run: func(cmd *cobra.Command, args []string) { 59 | fmt.Fprintln(os.Stdout, "Generating TUF encrypted Shamir PEMs...") 60 | keys, err := signed.GenerateShamirPEMsToMemAsArray(password.GetPass, int(parts), int(threshold)) 61 | not(err) 62 | 63 | fmt.Fprintln(os.Stdout, "Extracting PEMs to files...") 64 | err = signed.ExtractPEMsToCurrentDir(password.GetPass, password.GetPrompterYN, keys) 65 | not(err) 66 | }, 67 | } 68 | 69 | func init() { 70 | cmdGenerate.PersistentFlags().Uint8VarP(&parts, "parts", "p", 3, "generates a `parts` number of shares") 71 | cmdGenerate.PersistentFlags().Uint8VarP(&threshold, "threshold", "t", 2, "`threshold` count of which are required to reconstruct the secret") 72 | _ = cmdGenerate.MarkPersistentFlagRequired("parts") 73 | _ = cmdGenerate.MarkPersistentFlagRequired("threshold") 74 | 75 | rootCmd.AddCommand(cmdGenerate) 76 | } 77 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package cmd 21 | 22 | import ( 23 | "fmt" 24 | "io/ioutil" 25 | "os" 26 | 27 | "github.com/Dentrax/cocert/pkg/password" 28 | "github.com/Dentrax/cocert/pkg/signed" 29 | 30 | "github.com/spf13/cobra" 31 | ) 32 | 33 | var ( 34 | rootCmd = &cobra.Command{ 35 | Use: "cocert", 36 | Short: "cocert is a sigstore powered certificate generator built-on top of Shamir's Secret Sharing", 37 | } 38 | ) 39 | 40 | var ( 41 | readFileOrInput = func(file, input string) []byte { 42 | if file != "" { 43 | ff, err := ioutil.ReadFile(file) 44 | not(err) 45 | return ff 46 | } 47 | return []byte(input) 48 | } 49 | 50 | decidePassFunc = func(key string) signed.PassFunc { 51 | if len(key) > 0 { 52 | return func(confirm bool, message string, enforceTerminal bool) ([]byte, error) { 53 | return []byte(key), nil 54 | } 55 | } 56 | return password.GetPass 57 | } 58 | ) 59 | 60 | func Execute() error { 61 | return rootCmd.Execute() 62 | } 63 | 64 | func not(err error) { 65 | if err != nil { 66 | fmt.Fprintln(os.Stderr, err) 67 | os.Exit(1) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /cmd/sign.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package cmd 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | "io/ioutil" 26 | "os" 27 | 28 | "github.com/Dentrax/cocert/pkg/signed" 29 | 30 | "github.com/spf13/cobra" 31 | ) 32 | 33 | var ( 34 | signPrivateKeyFiles []string 35 | signPrivateKeyFile string 36 | signPayload string 37 | certOutput string 38 | signOutput string 39 | signKeyless bool 40 | ) 41 | 42 | var cmdSign = &cobra.Command{ 43 | Use: "sign {-f file}... {-p payload} [-o output]", 44 | Short: "Sign the given payload and create a certificate from Fulcio", 45 | Long: `Sign the given payload and create a certificate from Fulcio 46 | 47 | EXAMPLES 48 | # sign a payload using 3 private keys 49 | cocert sign -f cocert0.key -f cocert1.key -f cocert2.key -p "foo" 50 | 51 | # sign a payload using 2 private keys and create a cert file 52 | cocert sign -f cocert0.key -f cocert1.key -p "foo" -o my.cert 53 | `, 54 | PreRunE: func(cmd *cobra.Command, args []string) error { 55 | if len(signPrivateKeyFiles) > 0 && signPrivateKeyFile != "" { 56 | return fmt.Errorf("--file and --private-key are mutually exclusive, can not be set at same time") 57 | } 58 | if len(signPrivateKeyFiles) == 0 && signPrivateKeyFile == "" { 59 | return fmt.Errorf("one of the --file or --private-key are required") 60 | } 61 | if signPrivateKeyFile == "" && len(signPrivateKeyFiles) < 2 { 62 | return fmt.Errorf("least two --file required to combine splitted Shamir keys") 63 | } 64 | return nil 65 | }, 66 | Run: func(cmd *cobra.Command, args []string) { 67 | 68 | signer := func() signed.Signer { 69 | if signKeyless { 70 | s, err := signed.NewKeylessSigner(context.TODO(), signPrivateKeyFiles, signPrivateKeyFile) 71 | not(err) 72 | return s 73 | } 74 | s, err := signed.NewKeySigner(context.TODO(), signPrivateKeyFiles, signPrivateKeyFile) 75 | not(err) 76 | return s 77 | } 78 | 79 | encoded, err := signed.CreateSigner(context.TODO(), signer(), []byte(signPayload)) 80 | not(err) 81 | 82 | if certOutput != "" { 83 | err = ioutil.WriteFile(certOutput, []byte(signer().Cert), 0600) 84 | not(err) 85 | } 86 | 87 | if signOutput != "" { 88 | err = ioutil.WriteFile(signOutput, []byte(encoded), 0600) 89 | not(err) 90 | } 91 | 92 | fmt.Fprintln(os.Stdout, "Signed:", encoded) 93 | }, 94 | } 95 | 96 | func init() { 97 | cmdSign.Flags().StringSliceVarP(&signPrivateKeyFiles, "file", "f", []string{}, "splitted private key files (least 2 required)") 98 | cmdSign.Flags().StringVarP(&signPrivateKeyFile, "private-key", "F", "", "private key file") 99 | cmdSign.Flags().StringVarP(&signPayload, "payload", "p", "", "raw payload to sign") 100 | cmdSign.Flags().StringVarP(&certOutput, "output", "o", "", "output file for certificate") 101 | cmdSign.Flags().StringVarP(&signOutput, "sig-output", "O", "", "output file for signature") 102 | cmdSign.Flags().BoolVarP(&signKeyless, "keyless", "s", false, "use Fulcio to sign") 103 | _ = cmdSign.MarkFlagRequired("payload") 104 | 105 | rootCmd.AddCommand(cmdSign) 106 | } 107 | -------------------------------------------------------------------------------- /cmd/split.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package cmd 21 | 22 | import ( 23 | "fmt" 24 | "os" 25 | 26 | "github.com/Dentrax/cocert/pkg/password" 27 | "github.com/Dentrax/cocert/pkg/signed" 28 | 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | var ( 33 | splitPrivateKeyFile string 34 | splitParts, splitThreshold uint8 35 | ) 36 | 37 | var cmdSplit = &cobra.Command{ 38 | Use: "split [-p parts] [-t threshold]", 39 | Short: "Split your existing private key into parts", 40 | PreRun: func(cmd *cobra.Command, args []string) { 41 | if parts < 2 || parts > 255 { 42 | not(fmt.Errorf("the parts must be at least 2, and less than 256")) 43 | } 44 | if threshold < 2 || threshold > 255 { 45 | not(fmt.Errorf("the threshold must be at least 2, and less than 256")) 46 | } 47 | }, 48 | Run: func(cmd *cobra.Command, args []string) { 49 | fmt.Fprintln(os.Stdout, "Splitting private key to Shamir PEMs...") 50 | keys, err := signed.GenerateShamirPEMsToMemAsArrayFromCustomPrivateKey(splitPrivateKeyFile, int(splitParts), int(splitThreshold)) 51 | not(err) 52 | 53 | fmt.Fprintln(os.Stdout, "Extracting PEMs to files...") 54 | err = signed.ExtractPEMsToCurrentDir(password.GetPass, password.GetPrompterYN, keys) 55 | not(err) 56 | }, 57 | } 58 | 59 | func init() { 60 | cmdSplit.PersistentFlags().StringVarP(&splitPrivateKeyFile, "file", "f", "", "private key to split") 61 | cmdSplit.PersistentFlags().Uint8VarP(&splitParts, "parts", "p", 3, "splits a `parts` number of shares") 62 | cmdSplit.PersistentFlags().Uint8VarP(&splitThreshold, "threshold", "t", 2, "`threshold` count of which are required to reconstruct the secret") 63 | _ = cmdSplit.MarkPersistentFlagRequired("parts") 64 | _ = cmdSplit.MarkPersistentFlagRequired("threshold") 65 | 66 | rootCmd.AddCommand(cmdSplit) 67 | } 68 | -------------------------------------------------------------------------------- /cmd/verify.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package cmd 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | "os" 26 | 27 | "github.com/Dentrax/cocert/pkg/signed" 28 | 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | var ( 33 | filePubKey string 34 | filePubCert string 35 | fileVerifyPayload string 36 | fileVerifySignature string 37 | verifyPayload string 38 | verifySignature string 39 | ) 40 | 41 | var cmdVerify = &cobra.Command{ 42 | Use: "verify {-c cert | -f file} {-p payload | -t target} {-s signature | -k key}", 43 | Short: "Verify the given payload on the supplied signature", 44 | Long: ``, 45 | PreRunE: func(cmd *cobra.Command, args []string) error { 46 | if fileVerifyPayload != "" && verifyPayload != "" { 47 | return fmt.Errorf("--payload and --target are mutually exclusive, can not be set at same time") 48 | } 49 | if fileVerifyPayload == "" && verifyPayload == "" { 50 | return fmt.Errorf("one of the --payload or --target are required") 51 | } 52 | 53 | if fileVerifySignature != "" && verifySignature != "" { 54 | return fmt.Errorf("--signature and --key are mutually exclusive, can not be set at same time") 55 | } 56 | if fileVerifySignature == "" && verifySignature == "" { 57 | return fmt.Errorf("one of the --signature or --key are required") 58 | } 59 | 60 | return nil 61 | }, 62 | Run: func(cmd *cobra.Command, args []string) { 63 | v, err := signed.NewVerifier(filePubKey, filePubCert) 64 | not(err) 65 | 66 | err = signed.VerifyKey(context.TODO(), v, readFileOrInput(fileVerifyPayload, verifyPayload), readFileOrInput(fileVerifySignature, verifySignature)) 67 | not(err) 68 | 69 | fmt.Fprintln(os.Stdout, "Verified.") 70 | }, 71 | } 72 | 73 | func init() { 74 | cmdVerify.Flags().StringVarP(&filePubKey, "file", "f", "", "path to public key file") 75 | cmdVerify.Flags().StringVarP(&filePubCert, "cert", "c", "", "path to public certificate file") 76 | cmdVerify.Flags().StringVarP(&fileVerifyPayload, "target", "t", "", "read raw payload from file") 77 | cmdVerify.Flags().StringVarP(&fileVerifySignature, "signature", "s", "", "read raw base64 encoded signature from file") 78 | cmdVerify.Flags().StringVarP(&verifyPayload, "payload", "p", "", "raw payload") 79 | cmdVerify.Flags().StringVarP(&verifySignature, "key", "k", "", "raw base64 encoded signature") 80 | _ = cmdVerify.MarkFlagRequired("file") 81 | 82 | rootCmd.AddCommand(cmdVerify) 83 | } 84 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Dentrax/cocert 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Songmu/prompter v0.5.0 7 | github.com/hashicorp/vault v1.7.0 8 | github.com/hashicorp/vault/sdk v0.2.0 9 | github.com/mattn/go-isatty v0.0.12 10 | github.com/sigstore/cosign v0.3.1 11 | github.com/sigstore/sigstore v0.0.0-20210427115853-11e6eaab7cdc 12 | github.com/spf13/cobra v1.1.3 13 | github.com/stretchr/testify v1.7.0 14 | github.com/theupdateframework/go-tuf v0.0.0-20201230183259-aee6270feb55 15 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b 16 | golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 17 | ) 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | "os" 25 | 26 | "github.com/Dentrax/cocert/cmd" 27 | ) 28 | 29 | func main() { 30 | if err := cmd.Execute(); err != nil { 31 | fmt.Fprintln(os.Stderr, err) 32 | os.Exit(1) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/password/password.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package password 21 | 22 | import ( 23 | "fmt" 24 | "io/ioutil" 25 | "os" 26 | 27 | "github.com/hashicorp/vault/sdk/helper/password" 28 | "github.com/mattn/go-isatty" 29 | "golang.org/x/term" 30 | 31 | "github.com/Songmu/prompter" 32 | ) 33 | 34 | const CreateNewPasswordMsg string = "Create new password for private key: " 35 | const MasterPasswordMsg string = "Enter your master key: " 36 | 37 | func GetPrompterYN(message string, defaultToYes bool) bool { 38 | return prompter.YN(message, defaultToYes) 39 | } 40 | 41 | func GetPass(confirm bool, message string, enforceTerminal bool) ([]byte, error) { 42 | read := readPasswordFn(enforceTerminal) 43 | fmt.Fprint(os.Stderr, message) 44 | pw1, err := read() 45 | fmt.Fprintln(os.Stderr) 46 | if err != nil { 47 | return nil, err 48 | } 49 | if !confirm { 50 | return pw1, nil 51 | } 52 | fmt.Fprint(os.Stderr, "Confirm password: ") 53 | pw2, err := read() 54 | fmt.Fprintln(os.Stderr) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | if string(pw1) != string(pw2) { 60 | return nil, fmt.Errorf("passwords do not match") 61 | } 62 | return pw1, nil 63 | } 64 | 65 | func readPasswordFn(enforceTerminal bool) func() ([]byte, error) { 66 | switch { 67 | case term.IsTerminal(0) || enforceTerminal: 68 | isTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) 69 | if !isTerminal { 70 | return func() ([]byte, error) { 71 | return nil, fmt.Errorf("tty is not a terminal") 72 | } 73 | } 74 | 75 | return func() ([]byte, error) { 76 | value, err := password.Read(os.Stdin) 77 | if err != nil { 78 | return nil, err 79 | } 80 | return []byte(value), err 81 | } 82 | default: 83 | return func() ([]byte, error) { 84 | return ioutil.ReadAll(os.Stdin) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pkg/signed/extractors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package signed 21 | 22 | import ( 23 | "crypto" 24 | "crypto/ecdsa" 25 | "crypto/x509" 26 | "encoding/pem" 27 | "fmt" 28 | "io/ioutil" 29 | "os" 30 | "path/filepath" 31 | "strings" 32 | 33 | "github.com/Dentrax/cocert/pkg/password" 34 | 35 | "github.com/sigstore/sigstore/pkg/signature" 36 | "github.com/theupdateframework/go-tuf/encrypted" 37 | ) 38 | 39 | type EncryptPEMFile struct { 40 | Name string 41 | Data []byte 42 | } 43 | 44 | type TUFFunc func(text, pass []byte) ([]byte, error) 45 | 46 | func EncryptPEMsByTUF(pf PassFunc, prF PrompterFunc, keys *Keys) ([]EncryptPEMFile, error) { 47 | result := make([]EncryptPEMFile, len(keys.PrivatePEMBytes)) 48 | 49 | shouldDoEncrypt := prF("Do you want to encrypt each key using TUF?", false) 50 | 51 | for i, b := range keys.PrivatePEMBytes { 52 | name := fmt.Sprintf("cocert%d.key", i) 53 | 54 | if shouldDoEncrypt { 55 | p, err := pf(true, fmt.Sprintf("Create new password for %s key:", name), true) 56 | if err != nil { 57 | return nil, err 58 | } 59 | encBytes, err := encrypted.Encrypt(b, p) 60 | if err != nil { 61 | return nil, fmt.Errorf("failed to encrypt content for %s: %v", name, err) 62 | } 63 | b = encBytes 64 | } 65 | 66 | encoded := pem.EncodeToMemory(&pem.Block{ 67 | Bytes: b, 68 | Type: string(PemTypePrivate), 69 | }) 70 | 71 | result[i] = EncryptPEMFile{ 72 | Name: name, 73 | Data: encoded, 74 | } 75 | } 76 | 77 | return result, nil 78 | } 79 | 80 | func ExtractPEMsToCurrentDir(pf PassFunc, prF PrompterFunc, keys *Keys) error { 81 | files, err := EncryptPEMsByTUF(pf, prF, keys) 82 | if err != nil { 83 | return fmt.Errorf("failed to encrypt PEMs: %v", err) 84 | } 85 | 86 | for i, file := range files { 87 | if err := WriteFile(file.Name, file.Data); err != nil { 88 | return fmt.Errorf("unable to write private key %d: %v", i, err) 89 | } 90 | } 91 | 92 | if keys.PublicBytes != nil { 93 | if err := ioutil.WriteFile("cocert.pub", keys.PublicBytes, 0600); err != nil { 94 | return fmt.Errorf("unable to write public key: %v", err) 95 | } 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func WriteFile(filename string, data []byte) error { 102 | if err := ioutil.WriteFile(filename, data, 0600); err != nil { 103 | return fmt.Errorf("unable to write private key %s: %v", filename, err) 104 | } 105 | return nil 106 | } 107 | 108 | func EncodePEMToFileOrOutput(filename string, data []byte) error { 109 | return EncodePEMToFileOrOutputWithType(filename, data, string(PemTypePrivate)) 110 | } 111 | 112 | // EncodePEMToFileOrOutputWithType encodes the given data to PEM and 113 | // writes to file. 114 | func EncodePEMToFileOrOutputWithType(filename string, data []byte, pemType string) error { 115 | encoded := pem.EncodeToMemory(&pem.Block{ 116 | Bytes: data, 117 | Type: pemType, 118 | }) 119 | 120 | if filename != "" { 121 | err := WriteFile(filename, encoded) 122 | if err != nil { 123 | return fmt.Errorf("unable to encode to PEM: %v", err) 124 | } 125 | return nil 126 | } 127 | 128 | fmt.Fprintln(os.Stdout, string(encoded)) 129 | 130 | return nil 131 | } 132 | 133 | // DecodePEMBytes decodes given PEM bytes and checks equality the PemType 134 | func DecodePEMBytes(bytes []byte, pemType PemType) ([]byte, error) { 135 | p, _ := pem.Decode(bytes) 136 | if p == nil { 137 | return nil, fmt.Errorf("failed to decode PEM-encoded x509 certificate") 138 | } 139 | return p.Bytes, nil 140 | } 141 | 142 | func ReadFileAndDecodePEMFromPath(path string) ([]byte, error) { 143 | kb, err := ioutil.ReadFile(filepath.Clean(path)) 144 | if err != nil { 145 | return nil, fmt.Errorf("read file: %v", err) 146 | } 147 | 148 | p, err := DecodePEMBytes(kb, PemTypePrivate) 149 | if err != nil { 150 | return nil, fmt.Errorf("decode pem bytes: %v", err) 151 | } 152 | 153 | return p, nil 154 | } 155 | 156 | func ReadCustomPrivateKeyFileAndDecodePEMFromPath(path string) ([]byte, error) { 157 | bytes, err := ioutil.ReadFile(filepath.Clean(path)) 158 | if err != nil { 159 | return nil, fmt.Errorf("read file: %v", err) 160 | } 161 | p, _ := pem.Decode(bytes) 162 | if p == nil { 163 | return nil, fmt.Errorf("failed to decode PEM-encoded x509 certificate") 164 | } 165 | return p.Bytes, nil 166 | } 167 | 168 | func DecodePEMFromFile(name string, file []byte, pf PassFunc, tufFunc TUFFunc, passConfirm, enforceTerminal bool) ([]byte, error) { 169 | p, err := DecodePEMBytes(file, PemTypePrivate) 170 | if err != nil { 171 | return nil, fmt.Errorf("decode pem bytes: %v", err) 172 | } 173 | 174 | pass, err := pf(passConfirm, fmt.Sprintf("Enter your password for %s: ", name), enforceTerminal) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | decodedBytes, err := tufFunc(p, pass) 180 | if err != nil { 181 | e := err.Error() 182 | // which means PEM has not been encrypted, it is OK to continue 183 | if !strings.Contains(e, "invalid character") { 184 | return nil, fmt.Errorf("unable to decrypt PEM file: %v", err) 185 | } 186 | return p, nil 187 | } 188 | 189 | return decodedBytes, nil 190 | } 191 | 192 | func DecryptTUFEncryptedPrivateKey(ciphertext []byte, pf PassFunc) (*ecdsa.PrivateKey, error) { 193 | passphrase, err := pf(false, password.MasterPasswordMsg, false) 194 | if err != nil { 195 | return nil, fmt.Errorf("failed to get password: %v", err) 196 | } 197 | 198 | x509Encoded, err := encrypted.Decrypt(ciphertext, passphrase) 199 | if err != nil { 200 | return nil, fmt.Errorf("unable to decrypt X509 encoded TUF: %v", err) 201 | } 202 | 203 | pk, err := ParseECDSAPrivateKeyFromANS1(x509Encoded) 204 | if err != nil { 205 | return nil, fmt.Errorf("unable to extract ECDSA: %v", err) 206 | } 207 | 208 | return pk, nil 209 | } 210 | 211 | func DecryptTUFEncryptedKeys(ciphertext []byte, pf PassFunc) ([]byte, error) { 212 | pk, err := DecryptTUFEncryptedPrivateKey(ciphertext, pf) 213 | if err != nil { 214 | return nil, fmt.Errorf("decrypt ECDSA private key: %v", err) 215 | } 216 | 217 | return pk.D.Bytes(), nil 218 | } 219 | 220 | // ParseECDSAPrivateKeyFromANS1 parses given bytes to x509 PKCS8 and 221 | // returns *ecdsa.PrivateKey. 222 | func ParseECDSAPrivateKeyFromANS1(bytes []byte) (*ecdsa.PrivateKey, error) { 223 | pk, err := x509.ParsePKCS8PrivateKey(bytes) 224 | if err != nil { 225 | return nil, fmt.Errorf("failed to parse x509 certificate: %v", err) 226 | } 227 | 228 | epk, ok := pk.(*ecdsa.PrivateKey) 229 | if !ok { 230 | return nil, fmt.Errorf("invalid private key") 231 | } 232 | 233 | return epk, nil 234 | } 235 | 236 | // ParseECDSAPublicKeyFromPEM parses given PEM bytes to x509 Certificate, 237 | // casts to *ecdsa.PublicKey and returns ECDSAVerifier. 238 | func ParseECDSAPublicKeyFromPEM(bytes []byte) (PublicKey, error) { 239 | b, err := DecodePEMBytes(bytes, PemTypeCertificate) 240 | if err != nil { 241 | return nil, fmt.Errorf("decode pem bytes: %v", err) 242 | } 243 | 244 | cert, err := x509.ParseCertificate(b) 245 | if err != nil { 246 | return nil, fmt.Errorf("parse cert: %v", err) 247 | } 248 | 249 | pk, ok := cert.PublicKey.(*ecdsa.PublicKey) 250 | if !ok { 251 | return nil, fmt.Errorf("invalid public key format") 252 | } 253 | 254 | return signature.ECDSAVerifier{ 255 | Key: pk, 256 | HashAlg: crypto.SHA3_512, 257 | }, nil 258 | } 259 | 260 | // ParsePKIXPublicKeyFromPEM parses given PEM bytes to x509 PKIX, 261 | // casts to *ecdsa.PublicKey and returns ECDSAVerifier. 262 | func ParsePKIXPublicKeyFromPEM(bytes []byte) (PublicKey, error) { 263 | p, err := DecodePEMBytes(bytes, PemTypePublic) 264 | if err != nil { 265 | return nil, fmt.Errorf("decode pem: %v", err) 266 | } 267 | 268 | pkix, err := x509.ParsePKIXPublicKey(p) 269 | if err != nil { 270 | return nil, fmt.Errorf("parse pkix: %v", err) 271 | } 272 | 273 | pk, ok := pkix.(*ecdsa.PublicKey) 274 | if !ok { 275 | return nil, fmt.Errorf("invalid public key format") 276 | } 277 | 278 | return signature.ECDSAVerifier{ 279 | Key: pk, 280 | HashAlg: crypto.SHA3_512, 281 | }, nil 282 | } 283 | -------------------------------------------------------------------------------- /pkg/signed/extractors_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package signed 21 | 22 | import ( 23 | "encoding/pem" 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | "github.com/theupdateframework/go-tuf/encrypted" 28 | ) 29 | 30 | var ( 31 | prf = func(string, bool) bool { 32 | return true 33 | } 34 | ) 35 | 36 | func TestEncryptPEMsByTUF(t *testing.T) { 37 | assert := assert.New(t) 38 | 39 | got, err := GenerateShamirPEMsToMemAsArray(pf, 5, 3) 40 | assert.NoError(err) 41 | assert.NotNil(got) 42 | 43 | files, err := EncryptPEMsByTUF(pf, prf, got) 44 | assert.NoError(err) 45 | assert.NotNil(files) 46 | assert.Len(files, 5) 47 | 48 | for _, file := range files { 49 | p, _ := pem.Decode(file.Data) 50 | assert.NotNil(p) 51 | 52 | decodedBytes, err := encrypted.Decrypt(p.Bytes, []byte("test")) 53 | assert.NoError(err) 54 | assert.NotNil(decodedBytes) 55 | } 56 | } 57 | 58 | func TestParseECDSAPublicKeyFromPEM(t *testing.T) { 59 | assert := assert.New(t) 60 | 61 | got, err := GenerateTUFEncryptedKeys(pf) 62 | assert.NoError(err) 63 | assert.NotNil(got) 64 | 65 | pkix, err := ParsePKIXPublicKeyFromPEM(got.PublicBytes) 66 | assert.NoError(err) 67 | assert.NotNil(pkix) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/signed/generators.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package signed 21 | 22 | import ( 23 | "crypto/ecdsa" 24 | "crypto/elliptic" 25 | "crypto/rand" 26 | "crypto/x509" 27 | "encoding/pem" 28 | "fmt" 29 | 30 | "github.com/Dentrax/cocert/pkg/password" 31 | 32 | "github.com/hashicorp/vault/shamir" 33 | "github.com/theupdateframework/go-tuf/encrypted" 34 | ) 35 | 36 | func GenerateECDSAEllipticP521() (*ecdsa.PrivateKey, error) { 37 | return ecdsa.GenerateKey(elliptic.P521(), rand.Reader) 38 | } 39 | 40 | func GenerateTUFEncryptedKeys(pf PassFunc) (*Keys, error) { 41 | ellipticP521, err := GenerateECDSAEllipticP521() 42 | if err != nil { 43 | return nil, fmt.Errorf("failed to generate ECDSA from elliptic.P521(): %v", err) 44 | } 45 | 46 | pkcs8, err := x509.MarshalPKCS8PrivateKey(ellipticP521) 47 | if err != nil { 48 | return nil, fmt.Errorf("failed to marshal PKCS8 key: %v", err) 49 | } 50 | 51 | pkix, err := x509.MarshalPKIXPublicKey(&ellipticP521.PublicKey) 52 | if err != nil { 53 | return nil, fmt.Errorf("failed to marshal PKIX key: %v", err) 54 | } 55 | 56 | password, err := pf(true, password.CreateNewPasswordMsg, true) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | encBytes, err := encrypted.Encrypt(pkcs8, password) 62 | if err != nil { 63 | return nil, fmt.Errorf("failed to encrypt PKCS8: %v", err) 64 | } 65 | 66 | pkixPEMBytes := pem.EncodeToMemory(&pem.Block{ 67 | Type: string(PemTypePublic), 68 | Bytes: pkix, 69 | }) 70 | 71 | return &Keys{ 72 | PrivateBytesPlain: ellipticP521.D.Bytes(), 73 | PrivateBytes: encBytes, 74 | PublicBytes: pkixPEMBytes, 75 | }, nil 76 | } 77 | 78 | func GenerateShamirPEMsToMemAsArray(pf PassFunc, parts, threshold int) (*Keys, error) { 79 | keys, err := GenerateTUFEncryptedKeys(pf) 80 | if err != nil { 81 | return nil, fmt.Errorf("failed to generate TUF: %v", err) 82 | } 83 | 84 | bytes, err := shamir.Split(keys.PrivateBytes, parts, threshold) 85 | if err != nil { 86 | return nil, fmt.Errorf("shamir splitting error: %v", err) 87 | } 88 | 89 | keys.PrivatePEMBytes = bytes 90 | 91 | return keys, nil 92 | } 93 | 94 | func GenerateShamirPEMsToMemAsArrayFromCustomPrivateKey(path string, parts, threshold int) (*Keys, error) { 95 | bytes, err := ReadCustomPrivateKeyFileAndDecodePEMFromPath(path) 96 | if err != nil { 97 | return nil, fmt.Errorf("failed to decode pem: %v", err) 98 | } 99 | 100 | splitted, err := shamir.Split(bytes, parts, threshold) 101 | if err != nil { 102 | return nil, fmt.Errorf("shamir splitting error: %v", err) 103 | } 104 | 105 | return &Keys{ 106 | PrivateBytesPlain: nil, 107 | PrivatePEMBytes: splitted, 108 | PrivateBytes: nil, 109 | PublicBytes: nil, 110 | }, nil 111 | } 112 | -------------------------------------------------------------------------------- /pkg/signed/generators_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package signed 21 | 22 | import ( 23 | "crypto/ecdsa" 24 | "crypto/x509" 25 | "encoding/pem" 26 | "testing" 27 | 28 | "github.com/hashicorp/vault/shamir" 29 | "github.com/stretchr/testify/assert" 30 | "github.com/theupdateframework/go-tuf/encrypted" 31 | ) 32 | 33 | var ( 34 | pf = func(bool, string, bool) ([]byte, error) { 35 | return []byte("test"), nil 36 | } 37 | ) 38 | 39 | func TestGenerateECDSAEllipticP521(t *testing.T) { 40 | assert := assert.New(t) 41 | 42 | got, err := GenerateECDSAEllipticP521() 43 | assert.NoError(err) 44 | assert.NotNil(got) 45 | assert.NotEmpty(got.D.Bytes()) 46 | assert.NotEmpty(got.X.Bytes()) 47 | assert.NotEmpty(got.Y.Bytes()) 48 | } 49 | 50 | func TestGenerateTUFEncryptedKeys(t *testing.T) { 51 | assert := assert.New(t) 52 | 53 | got, err := GenerateTUFEncryptedKeys(pf) 54 | assert.NoError(err) 55 | assert.NotNil(got) 56 | 57 | p, pr := pem.Decode(got.PublicBytes) 58 | assert.NotNil(p) 59 | assert.Empty(pr) 60 | 61 | d, err := encrypted.Decrypt(got.PrivateBytes, []byte("test")) 62 | assert.NoError(err) 63 | assert.NotEmpty(d) 64 | 65 | pkix, err := x509.ParsePKIXPublicKey(p.Bytes) 66 | assert.NoError(err) 67 | assert.NotNil(pkix) 68 | 69 | pkcs8, err := x509.ParsePKCS8PrivateKey(d) 70 | assert.NoError(err) 71 | assert.NotNil(pkcs8) 72 | 73 | if pk, ok := pkcs8.(*ecdsa.PrivateKey); ok { 74 | assert.True(ok) 75 | assert.NotNil(pk) 76 | assert.Equal(got.PrivateBytesPlain, pk.D.Bytes()) 77 | } else { 78 | assert.Fail("could not parse PKIX to ecdsa.PrivateKey") 79 | } 80 | } 81 | 82 | func TestGenerateShamirPEMsToMemAsArray(t *testing.T) { 83 | assert := assert.New(t) 84 | 85 | got, err := GenerateShamirPEMsToMemAsArray(pf, 5, 2) 86 | assert.NoError(err) 87 | assert.NotNil(got) 88 | 89 | e1 := got.PrivatePEMBytes[2] 90 | e2 := got.PrivatePEMBytes[4] 91 | 92 | var s [][]byte 93 | s = append(s, e1) 94 | s = append(s, e2) 95 | 96 | c, err := shamir.Combine(s) 97 | assert.NoError(err) 98 | assert.NotNil(c) 99 | } 100 | -------------------------------------------------------------------------------- /pkg/signed/loaders.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package signed 21 | 22 | import ( 23 | "fmt" 24 | "io/ioutil" 25 | "os" 26 | "path/filepath" 27 | 28 | "github.com/hashicorp/vault/shamir" 29 | "github.com/sigstore/sigstore/pkg/signature" 30 | "github.com/theupdateframework/go-tuf/encrypted" 31 | ) 32 | 33 | type PublicKey interface { 34 | signature.Verifier 35 | signature.PublicKeyProvider 36 | } 37 | 38 | func LoadPublicKey(path string) (PublicKey, error) { 39 | bytes, err := ioutil.ReadFile(filepath.Clean(path)) 40 | if err != nil { 41 | return nil, fmt.Errorf("read public key: %v", err) 42 | } 43 | 44 | pk, err := ParsePKIXPublicKeyFromPEM(bytes) 45 | if err != nil { 46 | return nil, fmt.Errorf("parse pkix: %v", err) 47 | } 48 | 49 | return pk, nil 50 | } 51 | 52 | func LoadAndCombinePrivateKeysFromPaths(pf PassFunc, combineEncryptedFiles, combineUnEncryptedFiles []string) ([]byte, error) { 53 | fmt.Fprint(os.Stdout, "(Press Enter to continue without decrypt...)", "\n") 54 | 55 | parts, err := loadPrivateKeysFromPaths(pf, combineEncryptedFiles, combineUnEncryptedFiles) 56 | if err != nil { 57 | return nil, fmt.Errorf("load private keys error: %v", err) 58 | } 59 | 60 | c, err := shamir.Combine(parts) 61 | if err != nil { 62 | return nil, fmt.Errorf("shamir combining error: %v", err) 63 | } 64 | 65 | return c, nil 66 | } 67 | 68 | // loadPrivateKeysFromPaths load private keys from given paths 69 | // combineEncryptedFiles represents -f, which is we will show password prompt 70 | // combineUnEncryptedFiles represents -F, which is we will NOT show password prompt 71 | func loadPrivateKeysFromPaths(pf PassFunc, combineEncryptedFiles, combineUnEncryptedFiles []string) ([][]byte, error) { 72 | privateParts := make([][]byte, len(combineEncryptedFiles)+len(combineUnEncryptedFiles)) 73 | 74 | c := 0 75 | 76 | for i := 0; i < len(combineEncryptedFiles); i++ { 77 | k, err := readAndDecodePEMKeyFromPath(combineEncryptedFiles[i], pf) 78 | if err != nil { 79 | return nil, fmt.Errorf("read and decrypt pem file error: %v", err) 80 | } 81 | 82 | privateParts[c] = k 83 | c++ 84 | } 85 | 86 | for i := 0; i < len(combineUnEncryptedFiles); i++ { 87 | k, err := ReadFileAndDecodePEMFromPath(combineUnEncryptedFiles[i]) 88 | if err != nil { 89 | return nil, fmt.Errorf("read and decode pem file error: %v", err) 90 | } 91 | 92 | privateParts[c] = k 93 | c++ 94 | } 95 | 96 | return privateParts, nil 97 | } 98 | 99 | func readAndDecodePEMKeyFromPath(path string, pf PassFunc) ([]byte, error) { 100 | kb, err := ioutil.ReadFile(filepath.Clean(path)) 101 | if err != nil { 102 | return nil, fmt.Errorf("read file: %v", err) 103 | } 104 | 105 | p, err := DecodePEMFromFile(path, kb, pf, encrypted.Decrypt, false, true) 106 | if err != nil { 107 | return nil, fmt.Errorf("failed to decode PEM from file: %v", err) 108 | } 109 | 110 | return p, nil 111 | } 112 | -------------------------------------------------------------------------------- /pkg/signed/signer.go: -------------------------------------------------------------------------------- 1 | package signed 2 | 3 | import ( 4 | "context" 5 | "crypto" 6 | "crypto/ecdsa" 7 | "encoding/base64" 8 | "fmt" 9 | "os" 10 | 11 | "github.com/Dentrax/cocert/pkg/password" 12 | 13 | "github.com/sigstore/cosign/pkg/cosign/fulcio" 14 | "github.com/sigstore/sigstore/pkg/signature" 15 | _ "golang.org/x/crypto/sha3" //nolint:golint 16 | "golang.org/x/term" 17 | ) 18 | 19 | type Signer struct { 20 | signature.Signer 21 | 22 | PK *ecdsa.PrivateKey 23 | Cert string 24 | } 25 | 26 | func NewKeySigner(ctx context.Context, shamirFiles []string, privateKey string) (Signer, error) { 27 | s, err := DecideSignerType(ctx, shamirFiles, privateKey) 28 | if err != nil { 29 | return Signer{}, fmt.Errorf("create signer: %v", err) 30 | } 31 | return s, nil 32 | } 33 | 34 | func NewKeylessSigner(ctx context.Context, shamirFiles []string, privateKey string) (Signer, error) { 35 | s, err := DecideSignerType(ctx, shamirFiles, privateKey) 36 | if err != nil { 37 | return Signer{}, fmt.Errorf("create signer: %v", err) 38 | } 39 | 40 | flow := fulcio.FlowNormal 41 | if !term.IsTerminal(0) { 42 | fmt.Fprintln(os.Stderr, "Non-interactive mode detected, using device flow.") 43 | flow = fulcio.FlowDevice 44 | } 45 | 46 | cert, _, err := fulcio.GetCert(ctx, s.PK, flow) 47 | if err != nil { 48 | return Signer{}, fmt.Errorf("retrieving cert: %v", err) 49 | } 50 | 51 | return Signer{ 52 | Signer: s, 53 | Cert: cert, 54 | }, nil 55 | } 56 | 57 | func NewSignerFromShamir(ctx context.Context, files []string) (Signer, error) { 58 | s, err := LoadAndCombinePrivateKeysFromPaths(password.GetPass, files, nil) 59 | if err != nil { 60 | return Signer{}, fmt.Errorf("loading keys: %v", err) 61 | } 62 | 63 | return NewSignerFromBytes(s) 64 | } 65 | 66 | func NewSignerFromBytes(bytes []byte) (Signer, error) { 67 | pk, err := DecryptTUFEncryptedPrivateKey(bytes, password.GetPass) 68 | if err != nil { 69 | return Signer{}, fmt.Errorf("decrypt with master key: %v", err) 70 | } 71 | 72 | verifier := signature.NewECDSASignerVerifier(pk, crypto.SHA3_512) 73 | 74 | return Signer{ 75 | Signer: verifier, 76 | PK: pk, 77 | }, nil 78 | } 79 | 80 | func DecideSignerType(ctx context.Context, shamirFiles []string, privateKey string) (Signer, error) { 81 | switch { 82 | case privateKey != "": 83 | bytes, err := ReadFileAndDecodePEMFromPath(privateKey) 84 | if err != nil { 85 | return Signer{}, fmt.Errorf("read private key: %v", err) 86 | } 87 | s, err := NewSignerFromBytes(bytes) 88 | if err != nil { 89 | return Signer{}, fmt.Errorf("create signer from bytes: %v", err) 90 | } 91 | return s, nil 92 | case len(shamirFiles) >= 2: 93 | s, err := NewSignerFromShamir(ctx, shamirFiles) 94 | if err != nil { 95 | return Signer{}, fmt.Errorf("create signer: %v", err) 96 | } 97 | return s, nil 98 | default: 99 | return Signer{}, fmt.Errorf("does not provided any private key(s)") 100 | } 101 | } 102 | 103 | func CreateSigner(ctx context.Context, signer signature.Signer, payload []byte) (string, error) { 104 | sig, _, err := signer.Sign(ctx, payload) 105 | if err != nil { 106 | return "", fmt.Errorf("signing: %v", err) 107 | } 108 | 109 | return base64.StdEncoding.EncodeToString(sig), nil 110 | } 111 | -------------------------------------------------------------------------------- /pkg/signed/types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Furkan Türkal 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | package signed 21 | 22 | const ( 23 | PemTypePrivate PemType = "ENCRYPTED PRIVATE KEY" 24 | PemTypePublic PemType = "PUBLIC KEY" 25 | PemTypeCertificate PemType = "CERTIFICATE" 26 | ) 27 | 28 | type PemType string 29 | 30 | type PassFunc func(bool, string, bool) ([]byte, error) 31 | type PrompterFunc func(string, bool) bool 32 | 33 | type Keys struct { 34 | PrivateBytesPlain []byte 35 | PrivatePEMBytes [][]byte 36 | PrivateBytes []byte 37 | PublicBytes []byte 38 | } 39 | -------------------------------------------------------------------------------- /pkg/signed/verifier.go: -------------------------------------------------------------------------------- 1 | package signed 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "io/ioutil" 8 | 9 | "github.com/sigstore/sigstore/pkg/signature" 10 | ) 11 | 12 | func NewVerifier(publicKeyFile, publicCertFile string) (PublicKey, error) { 13 | 14 | getVerifier := func(pub, cert string) (PublicKey, error) { 15 | switch { 16 | case cert != "": 17 | bytes, err := ioutil.ReadFile(cert) 18 | if err != nil { 19 | return nil, fmt.Errorf("read cert file: %v", err) 20 | } 21 | 22 | pk, err := ParseECDSAPublicKeyFromPEM(bytes) 23 | if err != nil { 24 | return nil, fmt.Errorf("extract public key from ECDSA PEM: %v", err) 25 | } 26 | 27 | return pk, nil 28 | 29 | case pub != "": 30 | pk, err := LoadPublicKey(pub) 31 | if err != nil { 32 | return nil, fmt.Errorf("load public key: %v", err) 33 | } 34 | 35 | return pk, nil 36 | } 37 | 38 | return nil, fmt.Errorf("no pub or cert provided") 39 | } 40 | 41 | pk, err := getVerifier(publicKeyFile, publicCertFile) 42 | if err != nil { 43 | return signature.ECDSAVerifier{}, fmt.Errorf("could not get verifier: %v", err) 44 | } 45 | 46 | return pk, nil 47 | } 48 | 49 | func VerifyKey(ctx context.Context, verifier PublicKey, rawPayload []byte, base64Signature []byte) error { 50 | sig, err := base64.StdEncoding.DecodeString(string(base64Signature)) 51 | if err != nil { 52 | return fmt.Errorf("decode base64: %v", err) 53 | } 54 | 55 | return verifier.Verify(ctx, rawPayload, sig) 56 | } 57 | -------------------------------------------------------------------------------- /test/combine.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | set timeout -1 4 | spawn ./cocert combine -f cocert0.key -f cocert1.key -o combined.key 5 | 6 | expect "Enter your password for cocert0.key:" 7 | send -- "0\n" 8 | 9 | expect "Enter your password for cocert1.key:" 10 | send -- "1\n" 11 | 12 | expect "Enter your master key:" 13 | send -- "123\n" 14 | 15 | expect eof -------------------------------------------------------------------------------- /test/combine_splitted.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | set timeout -1 4 | spawn ./cocert combine -f cocert0.key -f cocert1.key -o combined_splitted.key 5 | 6 | expect "Enter your password for cocert0.key:" 7 | send -- "0\n" 8 | 9 | expect "Enter your password for cocert1.key:" 10 | send -- "1\n" 11 | 12 | expect "Enter your master key:" 13 | send -- "cosign\n" 14 | 15 | expect eof -------------------------------------------------------------------------------- /test/cosign.key: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED COSIGN PRIVATE KEY----- 2 | eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6 3 | OCwicCI6MX0sInNhbHQiOiJsVTREOWtkWHAvbjQ3WTRDWGVwNWxTQTAwNC9aTW5m 4 | bGJWY0V3YTZMZkxnPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94 5 | Iiwibm9uY2UiOiJrWFJwTkhrRnRmNzhPNUJzbmg0NUdDZWZUVFhjWmNNNyJ9LCJj 6 | aXBoZXJ0ZXh0IjoiSWlnQ0pJeVlZQjJHNy9rSWc3cHVncWJoQUFlb1dtSlYxM081 7 | bTFhNDVqcFQwVGNmbitMb2hBOXhRWnYwTWMxTjRUOUZvSythQ2hjNmtNUEJ2RHdh 8 | YVhaNWdHTXJ1NURRN1JwMGlMOWcvMDJhNlphR0xXV0h3QWxOdmwrUlpUNXI0V3I3 9 | T1FGTFIwdnhESzZnclVEcnVIWVFBL1lnTEFwdUJaaERWRjJtajE4QTUrOFJ3UXo4 10 | V2tRZDZ0d1J0bnNxZE1NRE5SMmMrYjN5RFE9PSJ9 11 | -----END ENCRYPTED COSIGN PRIVATE KEY----- 12 | -------------------------------------------------------------------------------- /test/e2e.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | set -o errexit; set -o nounset; set -o pipefail; 4 | 5 | setup_file() { 6 | export XDG_CACHE_HOME="$(mktemp -d)" 7 | export CMD="$XDG_CACHE_HOME/cocert" 8 | cp *.exp $XDG_CACHE_HOME 9 | cp *.key $XDG_CACHE_HOME 10 | run go build -o "$CMD" ../. 11 | } 12 | 13 | teardown_file() { 14 | rm -rf "$XDG_CACHE_HOME" 15 | } 16 | 17 | setup() { 18 | pushd $XDG_CACHE_HOME 19 | } 20 | 21 | teardown() { 22 | popd 23 | } 24 | 25 | @test "main: should run" { 26 | run ${CMD} 27 | echo "$output" 28 | [ "$status" -eq 0 ] 29 | } 30 | 31 | @test "generate: should success" { 32 | run ./generate.exp 33 | stat cocert.pub 34 | echo "$output" 35 | [ "$status" -eq 0 ] 36 | } 37 | 38 | @test "decrypt: should success" { 39 | run ${CMD} decrypt -f cocert0.key -k 0 -o cocert0.key.decrypted 40 | echo -n "1" | ${CMD} decrypt -f cocert1.key -o cocert1.key.decrypted 41 | echo -n "2" | ${CMD} decrypt -f cocert2.key -o cocert2.key.decrypted 42 | stat cocert0.key.decrypted 43 | stat cocert1.key.decrypted 44 | stat cocert2.key.decrypted 45 | echo "$output" 46 | [ "$status" -eq 0 ] 47 | } 48 | 49 | @test "encrypt: should success" { 50 | run ${CMD} encrypt -f cocert0.key.decrypted -k 0 -o cocert0.key.encrypted 51 | stat cocert0.key.encrypted 52 | [ "$status" -eq 0 ] 53 | } 54 | 55 | @test "combine: should success" { 56 | run ./combine.exp 57 | stat combined.key 58 | echo "$output" 59 | [ "$status" -eq 0 ] 60 | [[ $output = *"Combined"* ]] 61 | } 62 | 63 | @test "combine: should success for unencrypted" { 64 | run bash -c "echo -n "123" | ${CMD} combine -F cocert1.key.decrypted -F cocert2.key.decrypted -o private.key" 65 | echo "$output" 66 | stat private.key 67 | [ "$status" -eq 0 ] 68 | [[ $output = *"Combined"* ]] 69 | } 70 | 71 | @test "sign: should success combine" { 72 | run ./sign.exp 73 | stat combine.signature 74 | echo "$output" 75 | [ "$status" -eq 0 ] 76 | [[ "$output" = *"Signed:"* ]] 77 | } 78 | 79 | @test "sign: should success pk" { 80 | run ./sign_pk.exp 81 | stat combined.signature 82 | echo "$output" 83 | [ "$status" -eq 0 ] 84 | [[ "$output" = *"Signed:"* ]] 85 | } 86 | 87 | @test "verify: should success" { 88 | run ${CMD} verify -f cocert.pub -p "Foo Bar Baz" -s combine.signature 89 | echo "$output" 90 | [ "$status" -eq 0 ] 91 | [[ "$output" = *"Verified."* ]] 92 | } 93 | 94 | @test "split: should success for cosign key" { 95 | run ./split.exp 96 | echo "$output" 97 | [ "$status" -eq 0 ] 98 | } 99 | 100 | @test "combine: should success for splitted cosign key" { 101 | run ./combine_splitted.exp 102 | stat combined_splitted.key 103 | echo "$output" 104 | [ "$status" -eq 0 ] 105 | [[ $output = *"Combined"* ]] 106 | } -------------------------------------------------------------------------------- /test/generate.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | set timeout -1 4 | spawn ./cocert generate -p 3 -t 2 5 | 6 | expect "Create new password for private key: " 7 | send -- "123\n" 8 | 9 | expect "Confirm password:" 10 | send -- "123\n" 11 | 12 | expect "Do you want to encrypt each key using TUF?" 13 | send -- "y\r" 14 | 15 | expect "Create new password for cocert0.key key:" 16 | send -- "0\n" 17 | expect "Confirm password:" 18 | send -- "0\n" 19 | 20 | expect "Create new password for cocert1.key key:" 21 | send -- "1\n" 22 | expect "Confirm password:" 23 | send -- "1\n" 24 | 25 | expect "Create new password for cocert2.key key:" 26 | send -- "2\n" 27 | expect "Confirm password:" 28 | send -- "2\n" 29 | 30 | expect eof -------------------------------------------------------------------------------- /test/sign.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | set timeout -1 4 | spawn ./cocert sign -f cocert0.key -f cocert1.key -p "Foo Bar Baz" -O combine.signature 5 | 6 | expect "Enter your password for cocert0.key:" 7 | send -- "0\n" 8 | 9 | expect "Enter your password for cocert1.key:" 10 | send -- "1\n" 11 | 12 | expect "Enter your master key:" 13 | send -- "123\n" 14 | 15 | expect eof -------------------------------------------------------------------------------- /test/sign_pk.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | set timeout -1 4 | spawn ./cocert sign -F combined.key -p "Foo Bar Baz" -O combined.signature 5 | 6 | expect "Enter your master key:" 7 | send -- "123\n" 8 | 9 | expect eof -------------------------------------------------------------------------------- /test/split.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | set timeout -1 4 | spawn ./cocert split -f cosign.key -p 3 -t 2 5 | 6 | expect "Do you want to encrypt each key using TUF?" 7 | send -- "y\r" 8 | 9 | expect "Create new password for cocert0.key key:" 10 | send -- "0\n" 11 | expect "Confirm password:" 12 | send -- "0\n" 13 | 14 | expect "Create new password for cocert1.key key:" 15 | send -- "1\n" 16 | expect "Confirm password:" 17 | send -- "1\n" 18 | 19 | expect "Create new password for cocert2.key key:" 20 | send -- "2\n" 21 | expect "Confirm password:" 22 | send -- "2\n" 23 | 24 | expect eof --------------------------------------------------------------------------------