├── .gitignore ├── BUG-BOUNTY.md ├── nix ├── shell.nix ├── default.nix └── README.md ├── go.mod ├── pkg └── pivit │ ├── reset_test.go │ ├── print.go │ ├── print_test.go │ ├── reset.go │ ├── import.go │ ├── generate_test.go │ ├── sign_test.go │ ├── verify.go │ ├── verify_test.go │ ├── utils.go │ ├── pivit.go │ ├── sign.go │ ├── import_test.go │ ├── utils_test.go │ ├── status.go │ ├── generate.go │ └── fake_pivit.go ├── flake.lock ├── .github └── workflows │ ├── nix.yml │ ├── build.yml │ └── release.yml ├── Makefile ├── LICENSE.md ├── flake.nix ├── CONTRIBUTING.md ├── go.sum ├── README.md └── cmd └── pivit └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | /pivit 2 | build 3 | .DS_Store 4 | result 5 | *~ 6 | /cover.out 7 | -------------------------------------------------------------------------------- /BUG-BOUNTY.md: -------------------------------------------------------------------------------- 1 | Serious about security 2 | ====================== 3 | 4 | Square recognizes the important contributions the security research community 5 | can make. We therefore encourage reporting security issues with the code 6 | contained in this repository. 7 | 8 | If you believe you have discovered a security vulnerability, please follow the 9 | guidelines at https://bugcrowd.com/squareopensource -------------------------------------------------------------------------------- /nix/shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | with pkgs; 3 | let 4 | # A cgo dependency (go-piv) needs either pcsclite (Linux) or PCSC (macOS) 5 | pcsc = lib.optional stdenv.isLinux (lib.getDev pcsclite) 6 | ++ lib.optional stdenv.isDarwin (darwin.apple_sdk.frameworks.PCSC); 7 | in mkShell { 8 | buildInputs = [ 9 | file 10 | gnumake 11 | go 12 | pcsc 13 | ] ++ lib.optional stdenv.isLinux pkg-config; 14 | } 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cashapp/pivit 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d 7 | github.com/github/smimesign v0.2.0 8 | github.com/go-piv/piv-go/v2 v2.1.0 9 | github.com/manifoldco/promptui v0.9.0 10 | github.com/pborman/getopt/v2 v2.1.0 11 | github.com/pkg/errors v0.9.1 12 | github.com/stretchr/testify v1.3.0 13 | golang.org/x/crypto v0.17.0 14 | ) 15 | 16 | require ( 17 | github.com/chzyer/readline v1.5.1 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | golang.org/x/sys v0.15.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /pkg/pivit/reset_test.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-piv/piv-go/v2/piv" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestReset(t *testing.T) { 11 | yk, err := testYubikey() 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | err = ResetYubikey(yk, &ResetOpts{ 17 | Pin: "87654321", 18 | }) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | assert.NotEqual(t, piv.DefaultPUK, yk.puk) 24 | assert.NotEqual(t, piv.DefaultManagementKey, yk.managementKey) 25 | assert.Equal(t, yk.pin, "87654321") 26 | assert.Equal(t, &yk.managementKey, yk.metadata.ManagementKey) 27 | } 28 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1742751704, 6 | "narHash": "sha256-rBfc+H1dDBUQ2mgVITMGBPI1PGuCznf9rcWX/XIULyE=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "f0946fa5f1fb876a9dc2e1850d9d3a4e3f914092", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "id": "nixpkgs", 14 | "ref": "nixos-24.11", 15 | "type": "indirect" 16 | } 17 | }, 18 | "root": { 19 | "inputs": { 20 | "nixpkgs": "nixpkgs" 21 | } 22 | } 23 | }, 24 | "root": "root", 25 | "version": 7 26 | } 27 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | with pkgs; 3 | let 4 | # A cgo dependency (go-piv) needs either pcsclite (Linux) or PCSC (macOS) 5 | pcsc = lib.optional stdenv.isLinux (lib.getDev pcsclite) 6 | ++ lib.optional stdenv.isDarwin (darwin.apple_sdk.frameworks.PCSC); 7 | in buildGoModule rec { 8 | pname = "pivit"; 9 | version = "0.6.0"; 10 | 11 | src = ./..; 12 | 13 | # This needs to be updated whenever go.sum changes 14 | vendorHash = "sha256-Lr2TFHaTc85ZV+9BzLKejorCvqDdCRgQb5LZeMkWmHY="; 15 | 16 | buildInputs = pcsc; 17 | 18 | nativeBuildInputs = lib.optionals stdenv.isLinux [ pkg-config ]; 19 | 20 | meta = with lib; { 21 | description = "Utility for git signing using YubiKey PIV certificates"; 22 | homepage = "https://github.com/cashapp/pivit"; 23 | license = licenses.mit; 24 | maintainers = with maintainers; [ ddz yoavamit ]; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /pkg/pivit/print.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "encoding/pem" 5 | 6 | "github.com/go-piv/piv-go/v2/piv" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type CertificateOpts struct { 12 | // Slot to get certificate from 13 | Slot piv.Slot 14 | } 15 | 16 | type CertificateOutput struct { 17 | Fingerprint string 18 | CertificatePem string 19 | } 20 | 21 | // Certificate exports the certificate. 22 | func Certificate(yk Pivit, opts *CertificateOpts) (*CertificateOutput, error) { 23 | cert, err := yk.Certificate(opts.Slot) 24 | if err != nil { 25 | return nil, errors.Wrap(err, "get PIV certificate") 26 | } 27 | 28 | certBytes := pem.EncodeToMemory(&pem.Block{ 29 | Type: "CERTIFICATE", 30 | Bytes: cert.Raw, 31 | }) 32 | fingerprint := CertHexFingerprint(cert) 33 | return &CertificateOutput{ 34 | Fingerprint: fingerprint, 35 | CertificatePem: string(certBytes), 36 | }, nil 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/nix.yml: -------------------------------------------------------------------------------- 1 | name: "Nix Checks" 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | nix-build: 9 | name: "Build with Nix" 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macos-latest] 14 | steps: 15 | - name: Checkout repo 16 | uses: actions/checkout@v4 17 | - name: Install Nix 18 | uses: DeterminateSystems/nix-installer-action@main 19 | - name: Check Nix flake inputs 20 | uses: DeterminateSystems/flake-checker-action@v4 21 | - name: Check Nix flake 22 | run: nix flake check 23 | - name: Check `nix build` 24 | run: nix build 25 | - name: Check `nix develop` 26 | run: nix develop --command true 27 | - name: Check `nix-build` 28 | run: nix-build nix/ 29 | - name: Check `nix-shell` 30 | run: nix-shell --command true nix/ 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: pivit 3 | 4 | # 5 | # pivit: build pivit binary locally for development 6 | # 7 | .PHONY: pivit 8 | pivit: 9 | CGO_ENABLED=1 go build ./cmd/pivit 10 | 11 | # 12 | # install: install pivit to $GOPATH/bin 13 | # 14 | .PHONY: install 15 | install: 16 | go install ./cmd/pivit 17 | 18 | # 19 | # release: build a possibly cross-compiled and compressed release binary 20 | # 21 | .PHONY: release 22 | GOOS ?= $(shell go env GOOS) 23 | GOARCH ?= $(shell go env GOARCH) 24 | EXE = pivit-$(GOOS)-$(GOARCH) 25 | release: pivit 26 | (\ 27 | set -e ;\ 28 | cp -f pivit $(EXE) ;\ 29 | gzip -f -9 $(EXE) ;\ 30 | ) 31 | 32 | # 33 | # test: run tests 34 | # 35 | .PHONY: test 36 | test: pivit 37 | (\ 38 | set -e ;\ 39 | go test -coverprofile=cover.out ./pkg/... ;\ 40 | file pivit ;\ 41 | ./pivit --help 2> /dev/null ;\ 42 | ) 43 | 44 | # 45 | # clean: remove locally built artifacts 46 | # 47 | .PHONY: clean 48 | clean: 49 | rm -rf ./pivit* ./cover.out 50 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 Square, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /pkg/pivit/print_test.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-piv/piv-go/v2/piv" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCertificate_errEmptyYubikey(t *testing.T) { 11 | yk, err := testYubikey() 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | opts := &CertificateOpts{ 17 | Slot: piv.SlotCardAuthentication, 18 | } 19 | _, err = Certificate(yk, opts) 20 | assert.Error(t, err, "key not found") 21 | } 22 | 23 | func TestPrintCertificate_success(t *testing.T) { 24 | yk, err := testYubikey() 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | _, err = yk.GenerateKey(piv.DefaultManagementKey, piv.SlotCardAuthentication, piv.Key{}) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | cert, err := yk.Attest(piv.SlotCardAuthentication) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | err = yk.SetCertificate(piv.DefaultManagementKey, piv.SlotCardAuthentication, cert) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | opts := &CertificateOpts{ 45 | Slot: piv.SlotCardAuthentication, 46 | } 47 | output, err := Certificate(yk, opts) 48 | assert.NoError(t, err) 49 | assert.NotEmpty(t, output.Fingerprint) 50 | assert.NotNil(t, output.CertificatePem) 51 | } 52 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Sign and verify data using x509 certificates on Yubikeys"; 3 | 4 | # Use latest stable nixpkgs repository 5 | inputs.nixpkgs.url = "nixpkgs/nixos-24.11"; 6 | 7 | outputs = { self, nixpkgs }: 8 | let 9 | supportedSystems = [ 10 | "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" 11 | ]; 12 | 13 | # Helper function to generate an attrset 14 | # '{ x86_64-linux = f "x86_64-linux"; ... }'. 15 | forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 16 | 17 | # Nixpkgs instantiated for all supported system types. 18 | nixpkgsFor = forAllSystems ( 19 | system: import nixpkgs { inherit system; } 20 | ); 21 | 22 | in 23 | { 24 | devShells = forAllSystems (system: 25 | let 26 | pkgs = nixpkgsFor.${system}; 27 | in 28 | { 29 | # Use mkShell derivation from shell.nix 30 | default = import ./nix/shell.nix { inherit pkgs; }; 31 | }); 32 | 33 | packages = forAllSystems (system: 34 | let 35 | pkgs = nixpkgsFor.${system}; 36 | in 37 | { 38 | # Use buildGoModule derivation from default.nix 39 | default = import ./nix/default.nix { inherit pkgs; }; 40 | }); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "Build Checks" 2 | on: 3 | push: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-pivit: 10 | name: "Build Pivit" 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | go-version: [1.23.x] 15 | os: [ubuntu-latest, macos-latest] 16 | steps: 17 | - name: "Install golang" 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | - if: ${{ matrix.os == 'ubuntu-latest' }} 22 | name: "Install dependencies" 23 | run: | 24 | sudo apt update 25 | sudo apt install -y libpcsclite-dev 26 | - name: "Checkout code" 27 | uses: actions/checkout@v4 28 | - name: "Build and test" 29 | run: | 30 | set -euxo pipefail 31 | cd ${{ github.workspace }} 32 | make test 33 | 34 | if ${{ matrix.os == 'macos-latest' }}; then 35 | # Cross compile for darwin-arm64 36 | # Because arm64 binary does not run on an x86_64 runner, we don't 37 | # test it 38 | make pivit GOARCH=arm64 39 | fi 40 | - name: Upload results to Codecov 41 | uses: codecov/codecov-action@v4 42 | with: 43 | token: ${{ secrets.CODECOV_TOKEN }} 44 | 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | If you would like to contribute code to Pivit you can do so through GitHub by 5 | forking the repository and sending a pull request. 6 | 7 | When submitting code, please make every effort to follow existing conventions 8 | and style in order to keep the code as readable as possible. Please also make 9 | sure your code compiles by running `make`. 10 | 11 | Before your code can be accepted into the project you must also sign the 12 | [Individual Contributor License Agreement (CLA)][1]. 13 | 14 | Verified commits and releases 15 | ============================= 16 | 17 | Pivit is a command line tool that deals with security, and as such, we require that 18 | every code change in it will be [signed as well][2]. 19 | 20 | These signatures help us attest that code changes were made by real people, and provide 21 | and additional layer of security. 22 | 23 | In addition to only allowing verified commits, we also require signing every Pivit release tag 24 | with key that matches one of the allowed signers listed in `config/allowed_release_signers`. 25 | Release tags are verified during the release workflow in `.github/workflows/release.yaml`. 26 | 27 | [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 28 | [2]: https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits 29 | -------------------------------------------------------------------------------- /pkg/pivit/reset.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "math/big" 7 | 8 | "github.com/go-piv/piv-go/v2/piv" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type ResetOpts struct { 13 | // Pin new PIN to set for performing certain operations on the Yubikey 14 | Pin string 15 | } 16 | 17 | // ResetYubikey resets the yubikey, sets a new pin, and sets a random PIN unblock key 18 | func ResetYubikey(yk Pivit, opts *ResetOpts) error { 19 | if err := yk.Reset(); err != nil { 20 | return errors.Wrap(err, "reset PIV applet") 21 | } 22 | 23 | if err := yk.SetPIN(piv.DefaultPIN, opts.Pin); err != nil { 24 | return errors.Wrap(err, "failed to change pin") 25 | } 26 | 27 | newManagementKey, err := RandomManagementKey() 28 | if err != nil { 29 | return errors.Wrap(err, "failed to generate random management key") 30 | } 31 | 32 | if err = yk.SetManagementKey(piv.DefaultManagementKey, *newManagementKey); err != nil { 33 | return errors.Wrap(err, "set new management key") 34 | } 35 | if err = yk.SetMetadata(*newManagementKey, &piv.Metadata{ 36 | ManagementKey: newManagementKey, 37 | }); err != nil { 38 | return errors.Wrap(err, "failed to store new management key") 39 | } 40 | 41 | randomPuk, err := rand.Int(rand.Reader, big.NewInt(100_000_000)) 42 | if err != nil { 43 | return errors.Wrap(err, "create new random puk") 44 | } 45 | 46 | if err = yk.SetPUK(piv.DefaultPUK, fmt.Sprintf("%08d", randomPuk)); err != nil { 47 | return errors.Wrap(err, "set new puk") 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' 5 | name: "Release Deployable" 6 | jobs: 7 | verify-release-tag: 8 | name: "Verify release tag signature" 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: "Verify release tag" 12 | uses: cashapp/check-signature-action@v0.2.0 13 | id: check-tag-sig 14 | env: 15 | GH_TOKEN: ${{ github.token }} 16 | with: 17 | allowed-release-signers: yoavamit,mightyguava 18 | 19 | release-pivit: 20 | name: "Release Pivit" 21 | needs: verify-release-tag 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | matrix: 25 | go-version: [1.19.x] 26 | os: [ubuntu-latest, macos-latest] 27 | steps: 28 | - name: "Install golang" 29 | uses: actions/setup-go@v5 30 | with: 31 | go-version: ${{ matrix.go-version }} 32 | - if: ${{ matrix.os == 'ubuntu-latest' }} 33 | name: "Install dependencies" 34 | run: | 35 | sudo apt update 36 | sudo apt install -y libpcsclite-dev 37 | - if: ${{ matrix.os == 'macos-latest' }} 38 | name: "Install dependencies (MacOS)" 39 | run: | 40 | brew install openssh 41 | - name: "Checkout code" 42 | uses: actions/checkout@v4 43 | - name: "Build release" 44 | run: | 45 | set -euxo pipefail 46 | cd ${{ github.workspace }} 47 | make release GOARCH=amd64 48 | if ${{ matrix.os == 'macos-latest' }}; then 49 | make release GOARCH=arm64 50 | fi 51 | - name: "Release versioned" 52 | uses: ncipollo/release-action@v1 53 | with: 54 | allowUpdates: true 55 | artifacts: "pivit-*" 56 | token: ${{ secrets.GITHUB_TOKEN }} 57 | -------------------------------------------------------------------------------- /pkg/pivit/import.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/ed25519" 6 | "crypto/rsa" 7 | "crypto/x509" 8 | "encoding/pem" 9 | 10 | "github.com/go-piv/piv-go/v2/piv" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // ImportOpts specifies the parameters required when importing a certificate to the yubikey 15 | type ImportOpts struct { 16 | // CertificateBytes PEM encoded x509 certificate to import to the Yubikey 17 | CertificateBytes []byte 18 | // StopAfterFirst if false and CertificateBytes contains more data after the first PEM block, then return an error 19 | StopAfterFirst bool 20 | // Slot to store the certificate in 21 | Slot piv.Slot 22 | // Pin to access the Yubikey 23 | Pin string 24 | } 25 | 26 | // ImportCertificate stores a certificate file in a yubikey PIV slot 27 | func ImportCertificate(yk Pivit, opts *ImportOpts) error { 28 | block, rest := pem.Decode(opts.CertificateBytes) 29 | if (!opts.StopAfterFirst && len(rest) > 0) || block == nil { 30 | return errors.New("failed to parse certificate") 31 | } 32 | 33 | cert, err := x509.ParseCertificate(block.Bytes) 34 | if err != nil { 35 | return errors.Wrap(err, "parse certificate") 36 | } 37 | 38 | // the presence of a certificate indicates that the slot contains a private key 39 | // we don't want to import a certificate for a slot that doesn't contain a private key 40 | existingCertificate, err := yk.Certificate(opts.Slot) 41 | if err != nil { 42 | return errors.Wrap(err, "failed to get certificate") 43 | } 44 | if existingCertificate == nil { 45 | return errors.New("certificate not found") 46 | } 47 | 48 | publicKeyMatches := false 49 | switch pub := existingCertificate.PublicKey.(type) { 50 | case *rsa.PublicKey: 51 | publicKeyMatches = pub.Equal(cert.PublicKey) 52 | case *ecdsa.PublicKey: 53 | publicKeyMatches = pub.Equal(cert.PublicKey) 54 | case *ed25519.PublicKey: 55 | publicKeyMatches = pub.Equal(cert.PublicKey) 56 | } 57 | if !publicKeyMatches { 58 | return errors.New("imported certificate doesn't match the existing key") 59 | } 60 | 61 | managementKey, err := GetOrSetManagementKey(yk, opts.Pin) 62 | if err != nil { 63 | return errors.Wrap(err, "failed to use management key") 64 | } 65 | 66 | err = yk.SetCertificate(*managementKey, opts.Slot, cert) 67 | if err != nil { 68 | return errors.Wrap(err, "set certificate") 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/pivit/generate_test.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-piv/piv-go/v2/piv" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGenerateCertificate(t *testing.T) { 11 | yk, err := testYubikey() 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | patchPivVerify(yk) 17 | defer unpatchPinVerify() 18 | 19 | testCases := []struct { 20 | description string 21 | selfSign bool 22 | generateCsr bool 23 | assumeYes bool 24 | slot piv.Slot 25 | input *promptReader 26 | 27 | expectError bool 28 | expectNilResult bool 29 | shouldGenerateKey bool 30 | shouldGenerateCsr bool 31 | }{ 32 | { 33 | description: "self-signing fails when not confirmed", 34 | selfSign: true, 35 | slot: piv.Slot{}, 36 | input: &promptReader{pin: "n\n"}, 37 | expectNilResult: true, 38 | }, 39 | { 40 | description: "self-signed succeeds with assume yes", 41 | selfSign: true, 42 | assumeYes: true, 43 | slot: piv.SlotCardAuthentication, 44 | shouldGenerateKey: true, 45 | }, 46 | { 47 | description: "self-signed succeeds with confirmation prompt", 48 | selfSign: true, 49 | slot: piv.SlotCardAuthentication, 50 | input: &promptReader{pin: "y\n"}, 51 | shouldGenerateKey: true, 52 | }, 53 | { 54 | description: "generates certificate signing request", 55 | generateCsr: true, 56 | slot: piv.SlotCardAuthentication, 57 | shouldGenerateKey: true, 58 | shouldGenerateCsr: true, 59 | }, 60 | { 61 | description: "fails when both self-sign and generate csr flags are true", 62 | selfSign: true, 63 | generateCsr: true, 64 | slot: piv.SlotCardAuthentication, 65 | expectError: true, 66 | }, 67 | } 68 | for _, test := range testCases { 69 | t.Run(test.description, func(t *testing.T) { 70 | defer func() { 71 | _ = yk.Reset() 72 | }() 73 | opts := &GenerateCertificateOpts{ 74 | Algorithm: piv.AlgorithmEC384, 75 | SelfSign: test.selfSign, 76 | GenerateCsr: test.generateCsr, 77 | AssumeYes: test.assumeYes, 78 | PINPolicy: piv.PINPolicyNever, 79 | TouchPolicy: piv.TouchPolicyAlways, 80 | Slot: piv.SlotCardAuthentication, 81 | Prompt: test.input, 82 | Pin: piv.DefaultPIN, 83 | } 84 | result, err := GenerateCertificate(yk, opts) 85 | if test.expectError { 86 | assert.Error(t, err) 87 | assert.Nil(t, result) 88 | } else { 89 | assert.NoError(t, err) 90 | if test.expectNilResult { 91 | assert.Nil(t, result) 92 | } else { 93 | assert.NotEmpty(t, result.AttestationCertificate) 94 | if test.shouldGenerateKey { 95 | assert.NotEmpty(t, result.Certificate) 96 | assert.NotEmpty(t, yk.slots[test.slot].cert) 97 | } else { 98 | assert.Empty(t, result.Certificate) 99 | assert.Empty(t, yk.slots[test.slot].cert) 100 | } 101 | if test.shouldGenerateCsr { 102 | assert.NotEmpty(t, result.CertificateSigningRequest) 103 | } else { 104 | assert.Empty(t, result.CertificateSigningRequest) 105 | } 106 | } 107 | } 108 | }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pkg/pivit/sign_test.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "bytes" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "os" 8 | "testing" 9 | 10 | "github.com/go-piv/piv-go/v2/piv" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestSign(t *testing.T) { 15 | yk, err := testYubikey() 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | patchPivVerify(yk) 21 | defer unpatchPinVerify() 22 | 23 | genOpts := &GenerateCertificateOpts{ 24 | Algorithm: piv.AlgorithmEC384, 25 | SelfSign: true, 26 | GenerateCsr: false, 27 | AssumeYes: true, 28 | PINPolicy: piv.PINPolicyNever, 29 | TouchPolicy: piv.TouchPolicyAlways, 30 | Slot: piv.SlotCardAuthentication, 31 | Pin: piv.DefaultPIN, 32 | } 33 | result, err := GenerateCertificate(yk, genOpts) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | pemCert, _ := pem.Decode(result.Certificate) 39 | cert, err := x509.ParseCertificate(pemCert.Bytes) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | fingerprint := CertHexFingerprint(cert) 44 | 45 | testCases := []struct { 46 | description string 47 | armor bool 48 | detach bool 49 | userId string 50 | timestampUrl string 51 | slot piv.Slot 52 | expectError bool 53 | skipCI bool 54 | }{ 55 | { 56 | description: "no certificate found", 57 | slot: piv.SlotSignature, 58 | expectError: true, 59 | }, 60 | { 61 | description: "bad fingerprint", 62 | slot: piv.SlotCardAuthentication, 63 | expectError: true, 64 | }, 65 | { 66 | description: "default options", 67 | userId: fingerprint, 68 | slot: piv.SlotCardAuthentication, 69 | }, 70 | { 71 | description: "with armor", 72 | armor: true, 73 | userId: fingerprint, 74 | slot: piv.SlotCardAuthentication, 75 | }, 76 | { 77 | description: "detached signature", 78 | detach: true, 79 | userId: fingerprint, 80 | slot: piv.SlotCardAuthentication, 81 | }, 82 | { 83 | description: "with timestamp server", 84 | userId: fingerprint, 85 | timestampUrl: "http://timestamp.digicert.com", 86 | slot: piv.SlotCardAuthentication, 87 | skipCI: true, 88 | }, 89 | { 90 | description: "bad timestamp server", 91 | userId: fingerprint, 92 | timestampUrl: "http://127.0.0.1", 93 | slot: piv.SlotCardAuthentication, 94 | expectError: true, 95 | }, 96 | } 97 | for _, test := range testCases { 98 | t.Run(test.description, func(t *testing.T) { 99 | if test.skipCI { 100 | skipCI(t) 101 | } 102 | 103 | signOpts := &SignOpts{ 104 | StatusFd: 0, 105 | Detach: test.detach, 106 | Armor: test.armor, 107 | UserId: test.userId, 108 | TimestampAuthority: test.timestampUrl, 109 | Message: &bytes.Buffer{}, 110 | Slot: test.slot, 111 | Prompt: nil, 112 | } 113 | sig, err := Sign(yk, signOpts) 114 | if test.expectError { 115 | assert.Error(t, err) 116 | assert.Nil(t, sig) 117 | } else { 118 | assert.NoError(t, err) 119 | assert.NotEmpty(t, sig) 120 | if test.armor { 121 | block, _ := pem.Decode(sig) 122 | assert.Equal(t, "SIGNED MESSAGE", block.Type) 123 | } 124 | } 125 | }) 126 | } 127 | } 128 | 129 | // skipCI skips a test if we can determine the environment we're running in is a NixOS sandbox 130 | // it's used to skip tests that use networking for example 131 | func skipCI(t *testing.T) { 132 | if os.Getenv("NIX_ENFORCE_PURITY") != "" { 133 | t.Skip("Skipping test in CI environment") 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= 2 | github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s= 3 | github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= 4 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 5 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 6 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 7 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 8 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 9 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 10 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 11 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 12 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/github/smimesign v0.2.0 h1:Hho4YcX5N1I9XNqhq0fNx0Sts8MhLonHd+HRXVGNjvk= 17 | github.com/github/smimesign v0.2.0/go.mod h1:iZiiwNT4HbtGRVqCQu7uJPEZCuEE5sfSSttcnePkDl4= 18 | github.com/go-piv/piv-go/v2 v2.1.0 h1:e2sqz91TY1q2LKlZGvfAuryV/2vpy7UeKs0Gwu/7AHs= 19 | github.com/go-piv/piv-go/v2 v2.1.0/go.mod h1:4l8gMOksz1w2USRwdHmhgiwVh+p+Rf8hfb/zKdZcmQM= 20 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 21 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 22 | github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= 23 | github.com/pborman/getopt/v2 v2.1.0 h1:eNfR+r+dWLdWmV8g5OlpyrTYHkhVNxHBdN2cCrJmOEA= 24 | github.com/pborman/getopt/v2 v2.1.0/go.mod h1:4NtW75ny4eBw9fO1bhtNdYTlZKYX5/tBLtsOpwKIKd0= 25 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 26 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 27 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 31 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 32 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 33 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 34 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 35 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 36 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 37 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 38 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 39 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 40 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 43 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 44 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 45 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 46 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 47 | -------------------------------------------------------------------------------- /pkg/pivit/verify.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "bytes" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "fmt" 8 | "io" 9 | "os" 10 | 11 | "github.com/go-piv/piv-go/v2/piv" 12 | 13 | "github.com/certifi/gocertifi" 14 | cms "github.com/github/smimesign/ietf-cms" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | // VerifyOpts specifies the parameters required when verifying signatures 19 | type VerifyOpts struct { 20 | // Signature to verify 21 | Signature io.Reader 22 | // Message associated with the signature. 23 | // This option is only used when verifying a detached signature 24 | Message io.Reader 25 | // Slot containing certificate to verify with 26 | Slot piv.Slot 27 | } 28 | 29 | // VerifySignature verifies digital signatures. 30 | // If the given signature is detached, then read the message associated with the signature form VerifyOpts.Message 31 | func VerifySignature(yk Pivit, opts *VerifyOpts) error { 32 | EmitNewSign() 33 | 34 | buf := new(bytes.Buffer) 35 | if _, err := io.Copy(buf, opts.Signature); err != nil { 36 | return errors.Wrap(err, "read signature") 37 | } 38 | 39 | var ber []byte 40 | if blk, _ := pem.Decode(buf.Bytes()); blk != nil { 41 | if blk.Type != signedMessagePemHeader { 42 | return errors.New(fmt.Sprintf("unexpected PEM header: \"%s\"", blk.Type)) 43 | } 44 | ber = blk.Bytes 45 | } else { 46 | ber = buf.Bytes() 47 | } 48 | 49 | sd, err := cms.ParseSignedData(ber) 50 | if err != nil { 51 | return errors.Wrap(err, "parse signature") 52 | } 53 | 54 | if sd.IsDetached() { 55 | if opts.Message == nil { 56 | return errors.New("expected detached signature, but message wasn't provided") 57 | } 58 | return verifyDetached(yk, sd, opts.Message, opts.Slot) 59 | } 60 | 61 | if opts.Message != nil { 62 | return errors.New("expected to verify attached signature, but still got a message reader") 63 | } 64 | return verifyAttached(yk, sd, opts.Slot) 65 | } 66 | 67 | func verifyAttached(yk Pivit, sd *cms.SignedData, slot piv.Slot) error { 68 | chains, err := sd.Verify(verifyOpts(yk, slot)) 69 | if err != nil { 70 | if len(chains) > 0 { 71 | EmitBadSig(chains) 72 | } else { 73 | // TODO: We're omitting a bunch of arguments here. 74 | EmitErrSig() 75 | } 76 | 77 | return errors.Wrap(err, "verify signature") 78 | } 79 | 80 | var ( 81 | cert = chains[0][0][0] 82 | fpr = CertHexFingerprint(cert) 83 | subj = cert.Subject.String() 84 | ) 85 | 86 | _, _ = fmt.Fprintf(os.Stderr, "pivit: Signature made using certificate ID 0x%s\n", fpr) 87 | EmitGoodSig(chains) 88 | 89 | // TODO: Maybe split up signature checking and certificate checking so we can 90 | // output something more meaningful. 91 | _, _ = fmt.Fprintf(os.Stderr, "pivit: Good signature from \"%s\"\n", subj) 92 | EmitTrustFully() 93 | 94 | return nil 95 | } 96 | 97 | func verifyDetached(yk Pivit, sd *cms.SignedData, data io.Reader, slot piv.Slot) error { 98 | buf := new(bytes.Buffer) 99 | if _, err := io.Copy(buf, data); err != nil { 100 | return errors.Wrap(err, "read message file") 101 | } 102 | 103 | chains, err := sd.VerifyDetached(buf.Bytes(), verifyOpts(yk, slot)) 104 | if err != nil { 105 | if len(chains) > 0 { 106 | EmitBadSig(chains) 107 | } else { 108 | // TODO: We're omitting a bunch of arguments here. 109 | EmitErrSig() 110 | } 111 | 112 | return errors.Wrap(err, "failed to verify signature") 113 | } 114 | 115 | var ( 116 | cert = chains[0][0][0] 117 | fpr = CertHexFingerprint(cert) 118 | subj = cert.Subject.String() 119 | ) 120 | 121 | _, _ = fmt.Fprintf(os.Stderr, "pivit: Signature made using certificate ID 0x%s\n", fpr) 122 | EmitGoodSig(chains) 123 | 124 | // TODO: Maybe split up signature checking and certificate checking so we can 125 | // output something more meaningful. 126 | _, _ = fmt.Fprintf(os.Stderr, "pivit: Good signature from \"%s\"\n", subj) 127 | EmitTrustFully() 128 | 129 | return nil 130 | } 131 | 132 | func verifyOpts(yk Pivit, slot piv.Slot) x509.VerifyOptions { 133 | roots, err := x509.SystemCertPool() 134 | if err != nil { 135 | // SystemCertPool isn't implemented for Windows. fall back to mozilla trust store 136 | roots, err = gocertifi.CACerts() 137 | if err != nil { 138 | // fall back to an empty store 139 | // verification will likely fail 140 | roots = x509.NewCertPool() 141 | } 142 | } 143 | 144 | cert, err := yk.Certificate(slot) 145 | if err == nil { 146 | roots.AddCert(cert) 147 | } 148 | 149 | return x509.VerifyOptions{ 150 | Roots: roots, 151 | // TODO: we might want to limit signature verification to only certificates that have the right key usage extension 152 | KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /pkg/pivit/verify_test.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "bytes" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "io" 8 | "testing" 9 | 10 | "github.com/go-piv/piv-go/v2/piv" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestVerifySignature(t *testing.T) { 15 | yk, err := testYubikey() 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | patchPivVerify(yk) 20 | defer unpatchPinVerify() 21 | 22 | result, err := GenerateCertificate(yk, &GenerateCertificateOpts{ 23 | Algorithm: piv.AlgorithmEC384, 24 | SelfSign: false, 25 | GenerateCsr: false, 26 | AssumeYes: true, 27 | PINPolicy: piv.PINPolicyNever, 28 | TouchPolicy: piv.TouchPolicyAlways, 29 | Slot: piv.SlotCardAuthentication, 30 | Pin: piv.DefaultPIN, 31 | }) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | pemCert, _ := pem.Decode(result.Certificate) 37 | cert, err := x509.ParseCertificate(pemCert.Bytes) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | fingerprint := CertHexFingerprint(cert) 42 | 43 | testCases := []struct { 44 | name string 45 | detach bool 46 | armor bool 47 | message io.Reader 48 | }{ 49 | { 50 | name: "detached, not armored", 51 | detach: true, 52 | armor: false, 53 | message: &bytes.Buffer{}, 54 | }, 55 | { 56 | name: "attached, not armored", 57 | detach: false, 58 | armor: false, 59 | message: &bytes.Buffer{}, 60 | }, 61 | { 62 | name: "detached and armored", 63 | detach: true, 64 | armor: true, 65 | message: &bytes.Buffer{}, 66 | }, 67 | { 68 | name: "attached and armored", 69 | detach: false, 70 | armor: true, 71 | message: &bytes.Buffer{}, 72 | }, 73 | } 74 | 75 | for _, test := range testCases { 76 | t.Run(test.name, func(t *testing.T) { 77 | sig, err := Sign(yk, &SignOpts{ 78 | StatusFd: 0, 79 | Detach: test.detach, 80 | Armor: test.armor, 81 | UserId: fingerprint, 82 | TimestampAuthority: "", 83 | Message: test.message, 84 | Slot: piv.SlotCardAuthentication, 85 | }) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | 90 | var message io.Reader 91 | if test.detach { 92 | message = test.message 93 | } else { 94 | message = nil 95 | } 96 | err = VerifySignature(yk, &VerifyOpts{ 97 | Signature: bytes.NewReader(sig), 98 | Message: message, 99 | Slot: piv.SlotCardAuthentication, 100 | }) 101 | assert.NoError(t, err) 102 | }) 103 | } 104 | 105 | t.Run("attached with message parameter", func(t *testing.T) { 106 | sig, err := Sign(yk, &SignOpts{ 107 | StatusFd: 0, 108 | Detach: false, 109 | Armor: false, 110 | UserId: fingerprint, 111 | TimestampAuthority: "", 112 | Message: &bytes.Buffer{}, 113 | Slot: piv.SlotCardAuthentication, 114 | }) 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | err = VerifySignature(yk, &VerifyOpts{ 119 | Signature: bytes.NewReader(sig), 120 | Message: &bytes.Buffer{}, 121 | Slot: piv.SlotCardAuthentication, 122 | }) 123 | assert.Error(t, err) 124 | }) 125 | 126 | t.Run("unexpected PEM header", func(t *testing.T) { 127 | sig, err := Sign(yk, &SignOpts{ 128 | StatusFd: 0, 129 | Detach: false, 130 | Armor: true, 131 | UserId: fingerprint, 132 | TimestampAuthority: "", 133 | Message: &bytes.Buffer{}, 134 | Slot: piv.SlotCardAuthentication, 135 | }) 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | block, _ := pem.Decode(sig) 140 | sig = pem.EncodeToMemory(&pem.Block{ 141 | Type: "UNEXPECTED", 142 | Bytes: block.Bytes, 143 | }) 144 | err = VerifySignature(yk, &VerifyOpts{ 145 | Signature: bytes.NewReader(sig), 146 | Message: &bytes.Buffer{}, 147 | Slot: piv.SlotCardAuthentication, 148 | }) 149 | assert.Error(t, err) 150 | }) 151 | 152 | t.Run("bad detach signature", func(t *testing.T) { 153 | sig, err := Sign(yk, &SignOpts{ 154 | StatusFd: 0, 155 | Detach: true, 156 | Armor: false, 157 | UserId: fingerprint, 158 | TimestampAuthority: "", 159 | Message: &bytes.Buffer{}, 160 | Slot: piv.SlotCardAuthentication, 161 | }) 162 | if err != nil { 163 | t.Fatal(err) 164 | } 165 | err = VerifySignature(yk, &VerifyOpts{ 166 | Signature: bytes.NewReader(sig), 167 | Message: bytes.NewReader([]byte("not the same signed message")), 168 | Slot: piv.SlotCardAuthentication, 169 | }) 170 | assert.Error(t, err) 171 | }) 172 | } 173 | -------------------------------------------------------------------------------- /pkg/pivit/utils.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/sha1" 7 | "crypto/x509" 8 | "encoding/hex" 9 | "fmt" 10 | "io" 11 | "strings" 12 | 13 | "github.com/go-piv/piv-go/v2/piv" 14 | "github.com/manifoldco/promptui" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | // CertHexFingerprint returns the SHA1 checksum a certificate's raw bytes 19 | func CertHexFingerprint(certificate *x509.Certificate) string { 20 | fpr := sha1.Sum(certificate.Raw) 21 | fingerprintString := hex.EncodeToString(fpr[:]) 22 | return fingerprintString 23 | } 24 | 25 | // GetPin prompts the user for a PIN 26 | func GetPin(reader io.Reader) (string, error) { 27 | validatePin := func(input string) error { 28 | if len(input) < 6 || len(input) > 8 { 29 | return fmt.Errorf("PIN must be 6-8 digits long") 30 | } 31 | return nil 32 | } 33 | 34 | // promptui.Prompt requires io.ReadCloser for some reason, but it's much nicer to pass io.Reader 35 | // if what the user gave us doesn't implement io.ReadCloser, we'll wrap it for them 36 | readCloser, ok := reader.(io.ReadCloser) 37 | if !ok { 38 | readCloser = readeCloser(reader) 39 | } 40 | prompt := promptui.Prompt{ 41 | Label: "PIN", 42 | Validate: validatePin, 43 | Mask: '*', 44 | Stdin: readCloser, 45 | } 46 | 47 | newPin, err := prompt.Run() 48 | if err != nil { 49 | return "", err 50 | } 51 | return newPin, nil 52 | } 53 | 54 | func confirm(label string, stdin io.ReadCloser) (bool, error) { 55 | prompt := promptui.Prompt{ 56 | Label: label, 57 | IsConfirm: true, 58 | Stdin: stdin, 59 | } 60 | result, err := prompt.Run() 61 | if errors.Is(err, promptui.ErrAbort) { 62 | return false, nil 63 | } 64 | return strings.ToLower(result) == "y", err 65 | } 66 | 67 | // GetSlot returns a piv.Slot. Defaults to 9e 68 | func GetSlot(slot string) piv.Slot { 69 | switch slot { 70 | case piv.SlotCardAuthentication.String(): 71 | return piv.SlotCardAuthentication 72 | case piv.SlotAuthentication.String(): 73 | return piv.SlotAuthentication 74 | case piv.SlotSignature.String(): 75 | return piv.SlotSignature 76 | case piv.SlotKeyManagement.String(): 77 | return piv.SlotKeyManagement 78 | default: 79 | return piv.SlotCardAuthentication 80 | } 81 | } 82 | 83 | // RandomManagementKey returns a *[]byte slice filled with random byte values 84 | func RandomManagementKey() (*[]byte, error) { 85 | mk := make([]byte, 24) 86 | if _, err := rand.Reader.Read(mk); err != nil { 87 | return nil, err 88 | } 89 | return &mk, nil 90 | } 91 | 92 | // deriveManagementKey returns the first 24 bytes of the SHA256 checksum of the given pin 93 | // NOTE: this function has an error in it and SHOULD NOT BE USED. 94 | // It'll return the same checksum every time it's being called. 95 | // Its only use case should be in the GetOrSetManagementKey function for legacy reasons. 96 | func deriveManagementKey(pin string) *[]byte { 97 | hash := crypto.SHA256.New() 98 | checksum := hash.Sum([]byte(pin)) 99 | var mk []byte 100 | copy(mk[:], checksum[:24]) 101 | return &mk 102 | } 103 | 104 | // GetOrSetManagementKey returns the management key from the PIV metadata section. 105 | // If it's not found, it derives the management key from the PIN, and will then: 106 | // 1. create a new random management key 107 | // 2. set it as the new management key 108 | // 3. store it in the PIV metadata section 109 | // 4. return the newly set management key 110 | func GetOrSetManagementKey(yk Pivit, pin string) (*[]byte, error) { 111 | var newManagementKey *[]byte 112 | metadata, err := yk.Metadata(pin) 113 | if err != nil { 114 | return nil, err 115 | } 116 | if metadata.ManagementKey == nil { 117 | oldManagementKey := deriveManagementKey(pin) 118 | randomManagementKey, err := RandomManagementKey() 119 | if err != nil { 120 | return nil, errors.Wrap(err, "failed to generate random management key") 121 | } 122 | 123 | if err = yk.SetManagementKey(*oldManagementKey, *randomManagementKey); err != nil { 124 | return nil, errors.Wrap(err, "set new management key") 125 | } 126 | if err = yk.SetMetadata(*randomManagementKey, &piv.Metadata{ 127 | ManagementKey: randomManagementKey, 128 | }); err != nil { 129 | return nil, errors.Wrap(err, "failed to store new management key") 130 | } 131 | 132 | newManagementKey = randomManagementKey 133 | } else { 134 | newManagementKey = metadata.ManagementKey 135 | } 136 | return newManagementKey, nil 137 | } 138 | 139 | func readeCloser(reader io.Reader) io.ReadCloser { 140 | readCloser, ok := reader.(io.ReadCloser) 141 | if ok { 142 | return readCloser 143 | } 144 | return ReaderWrapper{reader} 145 | } 146 | 147 | type ReaderWrapper struct { 148 | io.Reader 149 | } 150 | 151 | func (r ReaderWrapper) Close() error { 152 | return nil 153 | } 154 | 155 | var _ io.ReadCloser = (*ReaderWrapper)(nil) 156 | -------------------------------------------------------------------------------- /pkg/pivit/pivit.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | "fmt" 7 | "io" 8 | "strconv" 9 | 10 | "github.com/go-piv/piv-go/v2/piv" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type Pivit interface { 15 | Close() error 16 | AttestationCertificate() (*x509.Certificate, error) 17 | Attest(slot piv.Slot) (*x509.Certificate, error) 18 | PrivateKey(slot piv.Slot, publicKey crypto.PublicKey, auth piv.KeyAuth) (crypto.PrivateKey, error) 19 | Certificate(slot piv.Slot) (*x509.Certificate, error) 20 | SetManagementKey(oldKey, newKey []byte) error 21 | Metadata(pin string) (*piv.Metadata, error) 22 | SetMetadata(managementKey []byte, metadata *piv.Metadata) error 23 | Reset() error 24 | SetPIN(oldPIN, newPIN string) error 25 | SetPUK(oldPUK, newPUK string) error 26 | SetCertificate(managementKey []byte, slot piv.Slot, certificate *x509.Certificate) error 27 | GenerateKey(managementKey []byte, slot piv.Slot, key piv.Key) (crypto.PublicKey, error) 28 | Version() piv.Version 29 | } 30 | 31 | var _ Pivit = (*piv.YubiKey)(nil) 32 | 33 | 34 | // YubikeyHandle returns a handle to the connected piv.YubiKey. 35 | // It errors unless there is exactly one YubiKey connected. 36 | func YubikeyHandle() (*piv.YubiKey, error) { 37 | return YubikeyHandleWithSerial("") 38 | } 39 | 40 | // YubikeyHandleWithSerial returns a handle to a piv.YubiKey. 41 | // If serial is empty, returns the YubiKey found only if exactly one card is present. 42 | // If serial is provided, returns the YubiKey with the matching serial number. 43 | func YubikeyHandleWithSerial(serial string) (*piv.YubiKey, error) { 44 | cards, err := piv.Cards() 45 | if err != nil { 46 | return nil, errors.Wrap(err, "enumerate smart cards") 47 | } 48 | 49 | if len(cards) == 0 { 50 | return nil, errors.New("no smart card found") 51 | } 52 | 53 | if serial == "" { 54 | if len(cards) > 1 { 55 | return nil, errors.New("multiple smart cards found but no serial specified") 56 | } 57 | yk, err := piv.Open(cards[0]) 58 | return yk, err 59 | } 60 | 61 | expectedSerial, err := strconv.ParseUint(serial, 10, 32) 62 | if err != nil { 63 | return nil, fmt.Errorf("invalid serial number format: %v", err) 64 | } 65 | 66 | for _, card := range cards { 67 | yk, err := piv.Open(card) 68 | if err != nil { 69 | continue 70 | } 71 | 72 | cardSerial, err := yk.Serial() 73 | if err != nil { 74 | yk.Close() 75 | continue 76 | } 77 | 78 | if uint64(cardSerial) == expectedSerial { 79 | return yk, nil 80 | } 81 | yk.Close() 82 | } 83 | 84 | return nil, fmt.Errorf("no smart card found with serial number %s", serial) 85 | } 86 | 87 | // signer implements crypto.Signer using a yubikey 88 | type signer struct { 89 | yk Pivit 90 | s piv.Slot 91 | stdin io.Reader 92 | } 93 | 94 | var _ crypto.Signer = (*signer)(nil) 95 | 96 | // NewYubikeySigner returns a signer 97 | func NewYubikeySigner(yk Pivit, s piv.Slot, stdin io.Reader) crypto.Signer { 98 | return signer{ 99 | yk: yk, 100 | s: s, 101 | stdin: stdin, 102 | } 103 | } 104 | 105 | // Sign implements crypto.Signer 106 | func (y signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { 107 | auth := piv.KeyAuth{ 108 | PINPrompt: func() (string, error) { 109 | pin, err := GetPin(y.stdin) 110 | if err != nil { 111 | return "", errors.Wrap(err, "get pin") 112 | } 113 | return pin, nil 114 | }, 115 | } 116 | 117 | // yubikeys with version 4.3.0 and lower must have the PINPolicy parameter specified 118 | // for newer versions, it's automatically inferred from the attestation certificate 119 | version := y.yk.Version() 120 | if version.Major < 4 || (version.Major == 4 && version.Minor < 3) { 121 | pinPolicy, err := y.getPINPolicy() 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | auth.PINPolicy = *pinPolicy 127 | } 128 | 129 | private, err := y.yk.PrivateKey(y.s, y.Public(), auth) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | switch priv := private.(type) { 135 | case crypto.Signer: 136 | return priv.Sign(rand, digest, opts) 137 | default: 138 | return nil, fmt.Errorf("invalid key type") 139 | } 140 | } 141 | 142 | // Public implements crypto.Signer 143 | func (y signer) Public() crypto.PublicKey { 144 | 145 | cert, err := y.yk.Certificate(y.s) 146 | if err != nil { 147 | return nil 148 | } 149 | 150 | return cert.PublicKey 151 | } 152 | 153 | func (y signer) getPINPolicy() (*piv.PINPolicy, error) { 154 | attestationCert, err := y.yk.AttestationCertificate() 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | keyCert, err := y.yk.Certificate(y.s) 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | attestation, err := piv.Verify(attestationCert, keyCert) 165 | if err != nil { 166 | return nil, err 167 | } 168 | return &attestation.PINPolicy, nil 169 | } 170 | 171 | var verify = piv.Verify 172 | -------------------------------------------------------------------------------- /pkg/pivit/sign.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "bytes" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "io" 8 | "strings" 9 | 10 | "github.com/go-piv/piv-go/v2/piv" 11 | 12 | cms "github.com/github/smimesign/ietf-cms" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | // SignOpts specifies the parameters required when signing data 17 | type SignOpts struct { 18 | // StatusFd file descriptor to write the "status protocol" to. For more details see the [status] package 19 | StatusFd int 20 | // Detach excludes the content being signed from the signature 21 | Detach bool 22 | // Armor encodes the signature in a PEM block 23 | Armor bool 24 | // UserId identifies the user of the certificate. Can be either an email address or the certificate's fingerprint 25 | UserId string 26 | // TimestampAuthority adds a timestamp to the signature from the given URL. See RFC3161 for more details 27 | TimestampAuthority string 28 | // Message to sign 29 | Message io.Reader 30 | // Slot containing key for signing 31 | Slot piv.Slot 32 | // Prompt where to get the pin from (only used if piv.PINPolicy requires it) 33 | Prompt io.ReadCloser 34 | } 35 | 36 | const signedMessagePemHeader = "SIGNED MESSAGE" 37 | 38 | // Sign creates a digital signature from the given data in SignOpts.Message 39 | func Sign(yk Pivit, opts *SignOpts) ([]byte, error) { 40 | cert, err := yk.Certificate(opts.Slot) 41 | if err != nil { 42 | return nil, errors.Wrap(err, "get identity certificate") 43 | } 44 | 45 | if err = certificateContainsUserId(cert, opts.UserId); err != nil { 46 | return nil, errors.Wrap(err, "no suitable certificate found") 47 | } 48 | 49 | SetupStatus(opts.StatusFd) 50 | dataBuf := new(bytes.Buffer) 51 | if _, err = io.Copy(dataBuf, opts.Message); err != nil { 52 | return nil, errors.Wrap(err, "read message to sign") 53 | } 54 | 55 | sd, err := cms.NewSignedData(dataBuf.Bytes()) 56 | if err != nil { 57 | return nil, errors.Wrap(err, "create signed data") 58 | } 59 | 60 | yubikeySigner := NewYubikeySigner(yk, opts.Slot, opts.Prompt) 61 | if err = sd.Sign([]*x509.Certificate{cert}, yubikeySigner); err != nil { 62 | return nil, errors.Wrap(err, "sign message") 63 | } 64 | // Git is looking for "\n[GNUPG:] SIG_CREATED ", meaning we need to print a 65 | // line before SIG_CREATED. BEGIN_SIGNING seems appropriate. GPG emits this, 66 | // though GPGSM does not. 67 | EmitBeginSigning() 68 | if opts.Detach { 69 | sd.Detached() 70 | } 71 | 72 | if len(opts.TimestampAuthority) > 0 { 73 | if err = sd.AddTimestamps(opts.TimestampAuthority); err != nil { 74 | return nil, errors.Wrap(err, "add timestamp to signature") 75 | } 76 | } 77 | 78 | chain := []*x509.Certificate{cert} 79 | if err = sd.SetCertificates(chain); err != nil { 80 | return nil, errors.Wrap(err, "set certificates in signature") 81 | } 82 | 83 | der, err := sd.ToDER() 84 | if err != nil { 85 | return nil, errors.Wrap(err, "serialize signature") 86 | } 87 | 88 | EmitSigCreated(cert, opts.Detach) 89 | if opts.Armor { 90 | buf := &bytes.Buffer{} 91 | err = pem.Encode(buf, &pem.Block{ 92 | Type: signedMessagePemHeader, 93 | Bytes: der, 94 | }) 95 | if err != nil { 96 | return nil, errors.New("write signature") 97 | } 98 | return buf.Bytes(), nil 99 | } else { 100 | return der, nil 101 | } 102 | } 103 | 104 | func certificateContainsUserId(cert *x509.Certificate, userId string) error { 105 | email, err := normalizeEmail(userId) 106 | if err != nil { 107 | fingerprint := normalizeFingerprint(userId) 108 | if !strings.EqualFold(CertHexFingerprint(cert), fingerprint) { 109 | return errors.Errorf("no certificate found with fingerprint %s", fingerprint) 110 | } 111 | } else { 112 | if !certificateContainsEmail(cert, email) { 113 | return errors.Errorf("no certificate found with email %s", email) 114 | } 115 | } 116 | 117 | return nil 118 | } 119 | 120 | // normalizeEmail extracts the email address portion from the user ID string 121 | // or an error if the user ID string doesn't contain a valid email address. 122 | // 123 | // The user ID string is expected to be either: 124 | // - An email address 125 | // - A string containing a name, comment and email address, like "Full Name (comment) " 126 | // - A hex fingerprint 127 | func normalizeEmail(userId string) (string, error) { 128 | emailStartIndex := strings.Index(userId, "<") 129 | if emailStartIndex != -1 { 130 | emailEndIndex := strings.Index(userId, ">") 131 | return userId[emailStartIndex:emailEndIndex], nil 132 | } 133 | 134 | if strings.ContainsRune(userId, '@') { 135 | return userId, nil 136 | } 137 | 138 | return "", errors.New("user id doesn't contain email address") 139 | } 140 | 141 | func normalizeFingerprint(userId string) string { 142 | return strings.TrimPrefix(userId, "0x") 143 | } 144 | 145 | func certificateContainsEmail(certificate *x509.Certificate, email string) bool { 146 | for _, sanEmail := range certificate.EmailAddresses { 147 | if sanEmail == email { 148 | return true 149 | } 150 | } 151 | 152 | return false 153 | } 154 | -------------------------------------------------------------------------------- /pkg/pivit/import_test.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/pem" 9 | "net" 10 | "net/url" 11 | "testing" 12 | "time" 13 | 14 | "github.com/go-piv/piv-go/v2/piv" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestImportCertificate(t *testing.T) { 19 | yk, err := testYubikey() 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | patchPivVerify(yk) 24 | defer unpatchPinVerify() 25 | 26 | generateResults, err := GenerateCertificate(yk, &GenerateCertificateOpts{ 27 | Algorithm: piv.AlgorithmEC384, 28 | SelfSign: true, 29 | GenerateCsr: false, 30 | AssumeYes: true, 31 | PINPolicy: piv.PINPolicyNever, 32 | TouchPolicy: piv.TouchPolicyAlways, 33 | Slot: piv.SlotCardAuthentication, 34 | Pin: piv.DefaultPIN, 35 | }) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | testCases := []struct { 41 | name string 42 | certBytes []byte 43 | stopAfterFirst bool 44 | slot piv.Slot 45 | pin string 46 | expectErr bool 47 | }{ 48 | { 49 | name: "bad pin", 50 | certBytes: generateResults.Certificate, 51 | stopAfterFirst: true, 52 | slot: piv.SlotCardAuthentication, 53 | pin: "654321", 54 | expectErr: true, 55 | }, 56 | { 57 | name: "bad certificate bytes", 58 | certBytes: createBadPEM(generateResults.Certificate), 59 | stopAfterFirst: true, 60 | slot: piv.SlotCardAuthentication, 61 | pin: piv.DefaultPIN, 62 | expectErr: true, 63 | }, 64 | { 65 | name: "bad x509 certificate", 66 | certBytes: createBadX509Certificate(generateResults.Certificate), 67 | stopAfterFirst: true, 68 | slot: piv.SlotCardAuthentication, 69 | pin: piv.DefaultPIN, 70 | expectErr: true, 71 | }, 72 | { 73 | name: "empty slot", 74 | certBytes: generateResults.Certificate, 75 | stopAfterFirst: true, 76 | slot: piv.SlotSignature, 77 | pin: piv.DefaultPIN, 78 | expectErr: true, 79 | }, 80 | { 81 | name: "contains more data after cert", 82 | certBytes: append(generateResults.Certificate, []byte("\nsome more data here")...), 83 | stopAfterFirst: false, 84 | slot: piv.SlotCardAuthentication, 85 | pin: piv.DefaultPIN, 86 | expectErr: true, 87 | }, 88 | { 89 | name: "contains more data after cert but stopAfterFirst is true", 90 | certBytes: append(generateResults.Certificate, []byte("\nsome more data here")...), 91 | stopAfterFirst: true, 92 | slot: piv.SlotCardAuthentication, 93 | pin: piv.DefaultPIN, 94 | expectErr: false, 95 | }, 96 | { 97 | name: "cert and key don't match", 98 | certBytes: createRandomCertificate(t), 99 | stopAfterFirst: true, 100 | slot: piv.SlotCardAuthentication, 101 | pin: piv.DefaultPIN, 102 | expectErr: true, 103 | }, 104 | } 105 | for _, test := range testCases { 106 | t.Run(test.name, func(t *testing.T) { 107 | actual := ImportCertificate(yk, &ImportOpts{ 108 | CertificateBytes: test.certBytes, 109 | StopAfterFirst: test.stopAfterFirst, 110 | Slot: test.slot, 111 | Pin: test.pin, 112 | }) 113 | if test.expectErr { 114 | assert.Error(t, actual) 115 | } else { 116 | assert.NoError(t, actual) 117 | } 118 | }) 119 | } 120 | } 121 | 122 | func createBadPEM(cert []byte) []byte { 123 | return cert[1:] 124 | } 125 | 126 | func createBadX509Certificate(cert []byte) []byte { 127 | block, _ := pem.Decode(cert) 128 | return pem.EncodeToMemory(&pem.Block{ 129 | Type: block.Type, 130 | Bytes: block.Bytes[1:], 131 | }) 132 | } 133 | 134 | func createRandomCertificate(t *testing.T) []byte { 135 | priv, err := rsa.GenerateKey(rand.Reader, 2048) 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | subject := pkix.Name{ 140 | Organization: []string{"Fake Yubico."}, 141 | OrganizationalUnit: []string{}, 142 | SerialNumber: "12376172635", 143 | CommonName: "Fake Pivit", 144 | } 145 | extKeyUsage := []x509.ExtKeyUsage{ 146 | x509.ExtKeyUsageClientAuth, 147 | x509.ExtKeyUsageCodeSigning, 148 | } 149 | serial, err := randomSerial() 150 | if err != nil { 151 | t.Fatal(err) 152 | } 153 | template := &x509.Certificate{ 154 | Subject: subject, 155 | SerialNumber: serial, 156 | DNSNames: []string{}, 157 | EmailAddresses: []string{}, 158 | IPAddresses: []net.IP{}, 159 | URIs: []*url.URL{}, 160 | KeyUsage: x509.KeyUsageDigitalSignature, 161 | ExtKeyUsage: extKeyUsage, 162 | ExtraExtensions: []pkix.Extension{}, 163 | NotBefore: time.Now(), 164 | NotAfter: time.Now().AddDate(0, 0, 1), 165 | } 166 | certBytes, err := x509.CreateCertificate(rand.Reader, template, template, priv.Public(), priv) 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | return pem.EncodeToMemory(&pem.Block{ 171 | Type: "CERTIFICATE", 172 | Bytes: certBytes, 173 | }) 174 | } 175 | -------------------------------------------------------------------------------- /nix/README.md: -------------------------------------------------------------------------------- 1 | # Building Pivit with Nix 2 | 3 | ## Nix Flakes (Recommended) 4 | 5 | Nix Flakes are recommended for deterministic reproducibility (input 6 | revisions are pinned in flake.lock). The following nix commands can 7 | all be run from the top-level of this repository or by specifying the 8 | URL-like name of the github repo (`github:cashapp/pivit`). 9 | 10 | ### Building with Nix 11 | 12 | ``` 13 | % nix build 14 | [...] 15 | % ./result/bin/pivit 16 | specify --help, --sign, --verify, --import, --generate, --reset or --print 17 | ``` 18 | 19 | ### Shell with `pivit` in the path 20 | 21 | ``` 22 | % nix shell 23 | [...] 24 | % which pivit 25 | /nix/store/pqpl0r6vvwln590v9zlm63ym0jqc6s62-pivit-0.6.0/bin/pivit 26 | ``` 27 | 28 | ### Development environment shell 29 | 30 | ``` 31 | % nix develop 32 | [...] 33 | (nix:nix-shell-env) $ make 34 | CGO_ENABLED=1 go build ./cmd/pivit 35 | [...] 36 | ``` 37 | 38 | ### Install package to profile 39 | 40 | ``` 41 | % nix profile install 42 | [...] 43 | ``` 44 | 45 | ### Nix (stable) 46 | 47 | For compatibility with the stable release of Nix without flakes 48 | support, Nix expressions are provided to use `nix-shell` and `nix-build`. 49 | 50 | ### Development Environment using `nix-shell` 51 | 52 | [`shell.nix`](shell.nix) can be used with `nix-shell` to enter into a 53 | development environment that includes all of the dependencies needed 54 | to build and test `pivit`: 55 | 56 | ``` 57 | % nix-shell 58 | these 103 paths will be fetched (174.05 MiB download, 1109.32 MiB unpacked): 59 | /nix/store/zcri58i63hrkm4iz3k5isxj52yx76qck-Platforms 60 | /nix/store/rvr43gjzdpbcz9jvq0kxdcmr3ijnq0zg-SDKs 61 | /nix/store/by8f4kgi39hxihikxy3g5rq9lnjxl1ji-Toolchains 62 | [...] 63 | 64 | [nix-shell:~/src/github.com/cashapp/pivit]$ make 65 | CGO_ENABLED=1 go build ./cmd/pivit 66 | go: downloading github.com/github/smimesign v0.2.0 67 | go: downloading github.com/pkg/errors v0.9.1 68 | go: downloading github.com/pborman/getopt/v2 v2.1.0 69 | go: downloading github.com/go-piv/piv-go v1.11.0 70 | go: downloading github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d 71 | go: downloading golang.org/x/crypto v0.17.0 72 | go: downloading github.com/manifoldco/promptui v0.9.0 73 | go: downloading github.com/chzyer/readline v1.5.1 74 | 75 | [nix-shell:~/src/github.com/cashapp/pivit]$ 76 | ``` 77 | 78 | ### Building with `nix-build` 79 | 80 | [`default.nix`](default.nix) can be used to build pivit with 81 | `nix-build`: 82 | 83 | ``` 84 | % nix-build 85 | this derivation will be built: 86 | /nix/store/v53x1i0bh9ldrd6jrv6m9q0rh73vbpah-pivit-0.6.0.drv 87 | building '/nix/store/v53x1i0bh9ldrd6jrv6m9q0rh73vbpah-pivit-0.6.0.drv'... 88 | Running phase: unpackPhase 89 | unpacking source archive /nix/store/327m9aw6w4nji2gz9jcmw79p0rqchn0s-pivit 90 | source root is pivit 91 | Running phase: patchPhase 92 | Running phase: updateAutotoolsGnuConfigScriptsPhase 93 | Running phase: configurePhase 94 | Running phase: buildPhase 95 | Building subPackage ./cmd/pivit 96 | Building subPackage ./cmd/pivit/status 97 | Building subPackage ./cmd/pivit/utils 98 | Building subPackage ./cmd/pivit/yubikey 99 | Running phase: checkPhase 100 | Running phase: installPhase 101 | Running phase: fixupPhase 102 | checking for references to /private/tmp/nix-build-pivit-0.6.0.drv-0/ in /nix/store/wfbdk46ywh5vza557jxdsxxcmbxfk5lg-pivit-0.6.0... 103 | patching script interpreter paths in /nix/store/wfbdk46ywh5vza557jxdsxxcmbxfk5lg-pivit-0.6.0 104 | stripping (with command strip and flags -S) in /nix/store/wfbdk46ywh5vza557jxdsxxcmbxfk5lg-pivit-0.6.0/bin 105 | /nix/store/wfbdk46ywh5vza557jxdsxxcmbxfk5lg-pivit-0.6.0 106 | ``` 107 | 108 | If the build was successful, it will create a symlink `result` that points to 109 | the output path in the nix store. You can run `pivit` from its `bin/` 110 | directory: 111 | 112 | ``` 113 | % ./result/bin/pivit 114 | specify --help, --sign, --verify, --import, --generate, --reset or --print 115 | ``` 116 | 117 | ## Maintenance 118 | 119 | ### Updating `vendorHash` 120 | 121 | When the contents of `go.sum` change, the nix build will also fail due to a 122 | hash mismatch: 123 | 124 | ``` 125 | error: hash mismatch in fixed-output derivation '/nix/store/alrrfv23kdgm99bfn1vqp2vr5n522xga-pivit-0.6.0-go-modules.drv': 126 | specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= 127 | got: sha256-S4Su9y10SjimxGAm/3k3tgritZ44ZB2N4CdwQEcGvQc= 128 | error: 1 dependencies of derivation '/nix/store/cmvdxq4v7lwmcgpqpwjwnc3dngprqilj-pivit-0.6.0.drv' failed to build 129 | ``` 130 | 131 | When this occurs, the current go modules derivation hash (shown for "got") 132 | needs to be set as `vendorHash` in [`default.nix`](default.nix). 133 | 134 | ### Updating nixpkgs flake input 135 | 136 | A little while after a new stable NixOS/nixpkgs is released at the end 137 | of November and April each year, it is a good time to update the 138 | stable nixpkgs input URL in [`flake.nix`](../flake.nix): 139 | 140 | ``` 141 | # Use a stable nixpkgs repository 142 | - inputs.nixpkgs.url = "nixpkgs/nixos-23.04"; 143 | + inputs.nixpkgs.url = "nixpkgs/nixos-23.11"; 144 | 145 | outputs = { self, nixpkgs }: 146 | ``` 147 | 148 | In addition, the pinned revisions of the flake inputs in 149 | [`flake.lock`](../flake.lock) should also be updated using 150 | `nix flake update`: 151 | 152 | ``` 153 | % nix flake update 154 | warning: Git tree '/home/ddz/src/github.com/cashapp/pivit' is dirty 155 | warning: updating lock file '/home/ddz/src/github.com/cashapp/pivit/flake.lock': 156 | • Updated input 'nixpkgs': 157 | 'github:NixOS/nixpkgs/6723fa4e4f1a30d42a633bef5eb01caeb281adc3' (2024-01-08) 158 | → 'github:NixOS/nixpkgs/3dc440faeee9e889fe2d1b4d25ad0f430d449356' (2024-01-10) 159 | warning: Git tree '/Users/io/src/github.com/cashapp/pivit' is dirty 160 | ``` 161 | -------------------------------------------------------------------------------- /pkg/pivit/utils_test.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/pem" 6 | "testing" 7 | 8 | "github.com/go-piv/piv-go/v2/piv" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestCertHexFingerprint(t *testing.T) { 13 | // A cert from google.com, fetched on 2024-07-18. 14 | pemCert := []byte(`-----BEGIN CERTIFICATE----- 15 | MIIN4zCCDMugAwIBAgIRAMmhwkjdy7M4CpXgoB3Cf3gwDQYJKoZIhvcNAQELBQAw 16 | OzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFUdvb2dsZSBUcnVzdCBTZXJ2aWNlczEM 17 | MAoGA1UEAxMDV1IyMB4XDTI0MDYyNDA2MzU0NFoXDTI0MDkxNjA2MzU0M1owFzEV 18 | MBMGA1UEAwwMKi5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE 19 | Aauze9K4ufZ06E6KUt96AwULUYHeawlHYonV89CZjZZaQovHYFKfo8ctn9Vl1dmU 20 | MgoxLStODjnsE0RFLbRJn6OCC88wggvLMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE 21 | DDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRifK0FZVbQdFKQ 22 | d6eMwsnEyN2rCDAfBgNVHSMEGDAWgBTeGx7teRXUPjckwyG77DQ5bUKyMDBYBggr 23 | BgEFBQcBAQRMMEowIQYIKwYBBQUHMAGGFWh0dHA6Ly9vLnBraS5nb29nL3dyMjAl 24 | BggrBgEFBQcwAoYZaHR0cDovL2kucGtpLmdvb2cvd3IyLmNydDCCCaUGA1UdEQSC 25 | CZwwggmYggwqLmdvb2dsZS5jb22CFiouYXBwZW5naW5lLmdvb2dsZS5jb22CCSou 26 | YmRuLmRldoIVKi5vcmlnaW4tdGVzdC5iZG4uZGV2ghIqLmNsb3VkLmdvb2dsZS5j 27 | b22CGCouY3Jvd2Rzb3VyY2UuZ29vZ2xlLmNvbYIYKi5kYXRhY29tcHV0ZS5nb29n 28 | bGUuY29tggsqLmdvb2dsZS5jYYILKi5nb29nbGUuY2yCDiouZ29vZ2xlLmNvLmlu 29 | gg4qLmdvb2dsZS5jby5qcIIOKi5nb29nbGUuY28udWuCDyouZ29vZ2xlLmNvbS5h 30 | coIPKi5nb29nbGUuY29tLmF1gg8qLmdvb2dsZS5jb20uYnKCDyouZ29vZ2xlLmNv 31 | bS5jb4IPKi5nb29nbGUuY29tLm14gg8qLmdvb2dsZS5jb20udHKCDyouZ29vZ2xl 32 | LmNvbS52boILKi5nb29nbGUuZGWCCyouZ29vZ2xlLmVzggsqLmdvb2dsZS5mcoIL 33 | Ki5nb29nbGUuaHWCCyouZ29vZ2xlLml0ggsqLmdvb2dsZS5ubIILKi5nb29nbGUu 34 | cGyCCyouZ29vZ2xlLnB0gg8qLmdvb2dsZWFwaXMuY26CESouZ29vZ2xldmlkZW8u 35 | Y29tggwqLmdzdGF0aWMuY26CECouZ3N0YXRpYy1jbi5jb22CD2dvb2dsZWNuYXBw 36 | cy5jboIRKi5nb29nbGVjbmFwcHMuY26CEWdvb2dsZWFwcHMtY24uY29tghMqLmdv 37 | b2dsZWFwcHMtY24uY29tggxna2VjbmFwcHMuY26CDiouZ2tlY25hcHBzLmNughJn 38 | b29nbGVkb3dubG9hZHMuY26CFCouZ29vZ2xlZG93bmxvYWRzLmNughByZWNhcHRj 39 | aGEubmV0LmNughIqLnJlY2FwdGNoYS5uZXQuY26CEHJlY2FwdGNoYS1jbi5uZXSC 40 | EioucmVjYXB0Y2hhLWNuLm5ldIILd2lkZXZpbmUuY26CDSoud2lkZXZpbmUuY26C 41 | EWFtcHByb2plY3Qub3JnLmNughMqLmFtcHByb2plY3Qub3JnLmNughFhbXBwcm9q 42 | ZWN0Lm5ldC5jboITKi5hbXBwcm9qZWN0Lm5ldC5jboIXZ29vZ2xlLWFuYWx5dGlj 43 | cy1jbi5jb22CGSouZ29vZ2xlLWFuYWx5dGljcy1jbi5jb22CF2dvb2dsZWFkc2Vy 44 | dmljZXMtY24uY29tghkqLmdvb2dsZWFkc2VydmljZXMtY24uY29tghFnb29nbGV2 45 | YWRzLWNuLmNvbYITKi5nb29nbGV2YWRzLWNuLmNvbYIRZ29vZ2xlYXBpcy1jbi5j 46 | b22CEyouZ29vZ2xlYXBpcy1jbi5jb22CFWdvb2dsZW9wdGltaXplLWNuLmNvbYIX 47 | Ki5nb29nbGVvcHRpbWl6ZS1jbi5jb22CEmRvdWJsZWNsaWNrLWNuLm5ldIIUKi5k 48 | b3VibGVjbGljay1jbi5uZXSCGCouZmxzLmRvdWJsZWNsaWNrLWNuLm5ldIIWKi5n 49 | LmRvdWJsZWNsaWNrLWNuLm5ldIIOZG91YmxlY2xpY2suY26CECouZG91YmxlY2xp 50 | Y2suY26CFCouZmxzLmRvdWJsZWNsaWNrLmNughIqLmcuZG91YmxlY2xpY2suY26C 51 | EWRhcnRzZWFyY2gtY24ubmV0ghMqLmRhcnRzZWFyY2gtY24ubmV0gh1nb29nbGV0 52 | cmF2ZWxhZHNlcnZpY2VzLWNuLmNvbYIfKi5nb29nbGV0cmF2ZWxhZHNlcnZpY2Vz 53 | LWNuLmNvbYIYZ29vZ2xldGFnc2VydmljZXMtY24uY29tghoqLmdvb2dsZXRhZ3Nl 54 | cnZpY2VzLWNuLmNvbYIXZ29vZ2xldGFnbWFuYWdlci1jbi5jb22CGSouZ29vZ2xl 55 | dGFnbWFuYWdlci1jbi5jb22CGGdvb2dsZXN5bmRpY2F0aW9uLWNuLmNvbYIaKi5n 56 | b29nbGVzeW5kaWNhdGlvbi1jbi5jb22CJCouc2FmZWZyYW1lLmdvb2dsZXN5bmRp 57 | Y2F0aW9uLWNuLmNvbYIWYXBwLW1lYXN1cmVtZW50LWNuLmNvbYIYKi5hcHAtbWVh 58 | c3VyZW1lbnQtY24uY29tggtndnQxLWNuLmNvbYINKi5ndnQxLWNuLmNvbYILZ3Z0 59 | Mi1jbi5jb22CDSouZ3Z0Mi1jbi5jb22CCzJtZG4tY24ubmV0gg0qLjJtZG4tY24u 60 | bmV0ghRnb29nbGVmbGlnaHRzLWNuLm5ldIIWKi5nb29nbGVmbGlnaHRzLWNuLm5l 61 | dIIMYWRtb2ItY24uY29tgg4qLmFkbW9iLWNuLmNvbYIUZ29vZ2xlc2FuZGJveC1j 62 | bi5jb22CFiouZ29vZ2xlc2FuZGJveC1jbi5jb22CHiouc2FmZW51cC5nb29nbGVz 63 | YW5kYm94LWNuLmNvbYINKi5nc3RhdGljLmNvbYIUKi5tZXRyaWMuZ3N0YXRpYy5j 64 | b22CCiouZ3Z0MS5jb22CESouZ2NwY2RuLmd2dDEuY29tggoqLmd2dDIuY29tgg4q 65 | LmdjcC5ndnQyLmNvbYIQKi51cmwuZ29vZ2xlLmNvbYIWKi55b3V0dWJlLW5vY29v 66 | a2llLmNvbYILKi55dGltZy5jb22CC2FuZHJvaWQuY29tgg0qLmFuZHJvaWQuY29t 67 | ghMqLmZsYXNoLmFuZHJvaWQuY29tggRnLmNuggYqLmcuY26CBGcuY2+CBiouZy5j 68 | b4IGZ29vLmdsggp3d3cuZ29vLmdsghRnb29nbGUtYW5hbHl0aWNzLmNvbYIWKi5n 69 | b29nbGUtYW5hbHl0aWNzLmNvbYIKZ29vZ2xlLmNvbYISZ29vZ2xlY29tbWVyY2Uu 70 | Y29tghQqLmdvb2dsZWNvbW1lcmNlLmNvbYIIZ2dwaHQuY26CCiouZ2dwaHQuY26C 71 | CnVyY2hpbi5jb22CDCoudXJjaGluLmNvbYIIeW91dHUuYmWCC3lvdXR1YmUuY29t 72 | gg0qLnlvdXR1YmUuY29tghR5b3V0dWJlZWR1Y2F0aW9uLmNvbYIWKi55b3V0dWJl 73 | ZWR1Y2F0aW9uLmNvbYIPeW91dHViZWtpZHMuY29tghEqLnlvdXR1YmVraWRzLmNv 74 | bYIFeXQuYmWCByoueXQuYmWCGmFuZHJvaWQuY2xpZW50cy5nb29nbGUuY29tghMq 75 | LmFuZHJvaWQuZ29vZ2xlLmNughIqLmNocm9tZS5nb29nbGUuY26CFiouZGV2ZWxv 76 | cGVycy5nb29nbGUuY24wEwYDVR0gBAwwCjAIBgZngQwBAgEwNgYDVR0fBC8wLTAr 77 | oCmgJ4YlaHR0cDovL2MucGtpLmdvb2cvd3IyLzlVVmJOMHc1RTZZLmNybDCCAQQG 78 | CisGAQQB1nkCBAIEgfUEgfIA8AB2AO7N0GTV2xrOxVy3nbTNE6Iyh0Z8vOzew1FI 79 | WUZxH7WbAAABkEksH+YAAAQDAEcwRQIgb3GDZEJp5gGKD+G8h0kuRIpZR3GoUYBx 80 | SwTpDs90yikCIQDEasgknC+8Arx3FivNoNTDah99IhCo+hDxgqDZgeUz4wB2ANq2 81 | v2s/tbYin5vCu1xr6HCRcWy7UYSFNL2kPTBI1/urAAABkEksIBMAAAQDAEcwRQIg 82 | JGg70VEbhuxYm5Q9xwBo95meGfXFvqkSTYkwiCWf9OICIQDuxWZXr6wepFX9AalY 83 | 5MwwUrvBJh5vlFQA/xsadcdv3zANBgkqhkiG9w0BAQsFAAOCAQEACujgHU4+xJm3 84 | o5uguuS4VKASvMdViP4p2RYrM9teOyXemQKgB1AnFcVbabGrST3iNR3dxChWcux1 85 | F65f/hvsehMKeWujcBgrAismJYi1YBpHJqbmWwniBrlcj3gCuoLRcGompoEocM0h 86 | fUgYmXTk6ISic0T4cFOcogE1uqP5cBWIBfPVjmVQcrsbr7TCNKGwBUaQM0fPpe4e 87 | y1Cf3dJYC0plgcSRycTs+7bzBvL4v1PsJeRnN30fL6inKc6pGrJQjbhKta4wJWaI 88 | DklnGaJUyq5Mp98Gam51m9i4616VmkODloxROWIeHUZbQ8XwWBsaRMf7rdu/RsSo 89 | 5FxOxhBVdA== 90 | -----END CERTIFICATE----- 91 | `) 92 | block, _ := pem.Decode(pemCert) 93 | cert, err := x509.ParseCertificate(block.Bytes) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | expectedFingerprint := "0b280e1bfffcc81bafd74e50f3ee7559bbd54624" 98 | fingerprint := CertHexFingerprint(cert) 99 | assert.Equal(t, expectedFingerprint, fingerprint) 100 | } 101 | 102 | func TestGetSlot(t *testing.T) { 103 | testCases := []struct { 104 | name string 105 | slot piv.Slot 106 | }{ 107 | {piv.SlotCardAuthentication.String(), piv.SlotCardAuthentication}, 108 | {piv.SlotSignature.String(), piv.SlotSignature}, 109 | {piv.SlotKeyManagement.String(), piv.SlotKeyManagement}, 110 | {piv.SlotAuthentication.String(), piv.SlotAuthentication}, 111 | {"anything else", piv.SlotCardAuthentication}, 112 | } 113 | for _, testCase := range testCases { 114 | t.Run(testCase.name, func(t *testing.T) { 115 | slot := GetSlot(testCase.name) 116 | assert.Equal(t, slot, testCase.slot) 117 | }) 118 | } 119 | } 120 | 121 | func TestRandomManagementKey(t *testing.T) { 122 | key1, err := RandomManagementKey() 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | assert.Len(t, *key1, 24) 127 | 128 | key2, err := RandomManagementKey() 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | assert.NotEqual(t, key1, key2) 133 | } 134 | 135 | func TestGetOrSetManagementKey(t *testing.T) { 136 | yk, err := testYubikey() 137 | if err != nil { 138 | t.Fatal(err) 139 | } 140 | 141 | badManagementKey := deriveManagementKey("123456") 142 | err = yk.SetManagementKey(piv.DefaultManagementKey, *badManagementKey) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | 147 | newManagementKey, err := GetOrSetManagementKey(yk, "123456") 148 | assert.NoError(t, err) 149 | assert.NotEqual(t, badManagementKey, newManagementKey) 150 | } 151 | -------------------------------------------------------------------------------- /pkg/pivit/status.go: -------------------------------------------------------------------------------- 1 | package pivit 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | "fmt" 7 | "os" 8 | "sync" 9 | "time" 10 | 11 | "golang.org/x/crypto/openpgp/packet" 12 | "golang.org/x/crypto/openpgp/s2k" 13 | ) 14 | 15 | // Large parts of this file were taken from https://github.com/github/smimesign/blob/v0.1.0/status.go 16 | 17 | // This file implements gnupg's "status protocol". When the --status-fd argument 18 | // is passed, gpg will output machine-readable status updates to that fd. 19 | // Details on the "protocol" can be found at https://git.io/vFFKC 20 | 21 | type status string 22 | 23 | const ( 24 | // BEGIN_SIGNING 25 | // Mark the start of the actual signing process. This may be used as an 26 | // indication that all requested secret keys are ready for use. 27 | sBeginSigning status = "BEGIN_SIGNING" 28 | 29 | // SIG_CREATED 30 | // A signature has been created using these parameters. 31 | // Values for type are: 32 | // - D :: detached 33 | // - C :: cleartext 34 | // - S :: standard 35 | // (only the first character should be checked) 36 | // 37 | // are 2 hex digits with the OpenPGP signature class. 38 | // 39 | // Note, that TIMESTAMP may either be a number of seconds since Epoch 40 | // or an ISO 8601 string which can be detected by the presence of the 41 | // letter 'T'. 42 | sSigCreated status = "SIG_CREATED" 43 | 44 | // NEWSIG [] 45 | // Is issued right before a signature verification starts. This is 46 | // useful to define a context for parsing ERROR status messages. 47 | // arguments are currently defined. If signers_uid is given and is 48 | // not "-" this is the percent escape value of the OpenPGP Signer's 49 | // User ID signature sub-packet. 50 | sNewSig status = "NEWSIG" 51 | 52 | // GOODSIG 53 | // The signature with the key_id is good. For each signature only one 54 | // of the codes GOODSIG, BADSIG, EXPSIG, EXPKEYSIG, REVKEYSIG or 55 | // ERRSIG will be emitted. In the past they were used as a marker 56 | // for a new signature; new code should use the NEWSIG status 57 | // instead. The username is the primary one encoded in UTF-8 and %XX 58 | // escaped. The fingerprint may be used instead of the long key_id if 59 | // it is available. This is the case with CMS and might eventually 60 | // also be available for OpenPGP. 61 | sGoodSig status = "GOODSIG" 62 | 63 | // BADSIG 64 | // The signature with the key_id has not been verified okay. The username is 65 | // the primary one encoded in UTF-8 and %XX escaped. The fingerprint may be 66 | // used instead of the long key_id if it is available. This is the case with 67 | // CMS and might eventually also be available for OpenPGP. 68 | sBadSig status = "BADSIG" 69 | 70 | // ERRSIG