├── .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 |
15 |
16 |
17 |
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 | 
29 |
30 | [Asciinema](https://asciinema.org/a/411543)
31 |
32 | # High Level Architecture
33 |
34 | 
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 | 
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
--------------------------------------------------------------------------------