From 472b4ddfb0fd0a9c04885655141c4814619880ee Mon Sep 17 00:00:00 2001 From: Alkorin Date: Sun, 9 Mar 2025 16:56:17 +0100 Subject: [PATCH] Split binary into lib & cmd --- .github/workflows/release.yml | 16 ++--- .github/workflows/tests.yml | 17 +++++ .goreleaser.yml | 7 +- cmd/yubico-piv-checker/main.go | 31 +++++++++ .../yubico-piv-checker/version.go | 0 go.mod | 11 ++-- go.sum | 27 +++----- main.go => lib/checker/checker.go | 65 +++++-------------- main_test.go => lib/checker/checker_test.go | 30 ++++++--- consts.go => lib/checker/consts.go | 19 +++++- types.go => lib/types/types.go | 26 ++++---- .../types/yubicopinpolicy_enumer.go | 2 +- .../types/yubicotouchpolicy_enumer.go | 2 +- 13 files changed, 141 insertions(+), 112 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 cmd/yubico-piv-checker/main.go rename version.go => cmd/yubico-piv-checker/version.go (100%) rename main.go => lib/checker/checker.go (51%) rename main_test.go => lib/checker/checker_test.go (79%) rename consts.go => lib/checker/consts.go (83%) rename types.go => lib/types/types.go (62%) rename yubicopinpolicy_enumer.go => lib/types/yubicopinpolicy_enumer.go (99%) rename yubicotouchpolicy_enumer.go => lib/types/yubicotouchpolicy_enumer.go (99%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d5fecd1..90e68e1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,22 +12,18 @@ jobs: goreleaser: runs-on: ubuntu-latest steps: - - - name: Checkout + - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v4 + - name: Setup Go + uses: actions/setup-go@v5 with: - go-version: 1.21 - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v5 + go-version: '1.23.x' + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: latest - args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..00a707f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,17 @@ +name: Go +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23.x' + - name: Build + run: go build -v ./cmd/yubico-piv-checker + - name: Test + run: go test -v ./... \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml index 0754ec5..d4386a5 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,6 +1,4 @@ -before: - hooks: - - go mod download +version: 2 builds: - env: - CGO_ENABLED=0 @@ -23,10 +21,9 @@ builds: - "7" gomips: - hardfloat + main: ./cmd/yubico-piv-checker checksum: name_template: 'checksums.txt' -snapshot: - name_template: "{{ .Tag }}-next" changelog: sort: asc nfpms: diff --git a/cmd/yubico-piv-checker/main.go b/cmd/yubico-piv-checker/main.go new file mode 100644 index 0000000..d51d0a4 --- /dev/null +++ b/cmd/yubico-piv-checker/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + + "github.com/ovh/yubico-piv-checker/lib/checker" +) + +func init() { + log.SetOutput(os.Stderr) +} + +func main() { + if len(os.Args) != 4 { + fmt.Fprintf(os.Stderr, "Usage: %s ssh-key attestation key-certificate\n", os.Args[0]) + os.Exit(-1) + } + + r, err := checker.VerifySSHKey(os.Args[1], os.Args[2], os.Args[3]) + if err != nil { + log.Fatal(err) + } + + err = json.NewEncoder(os.Stdout).Encode(r) + if err != nil { + log.Fatal(err) + } +} diff --git a/version.go b/cmd/yubico-piv-checker/version.go similarity index 100% rename from version.go rename to cmd/yubico-piv-checker/version.go diff --git a/go.mod b/go.mod index bdaa4bf..bfe1b1b 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,13 @@ module github.com/ovh/yubico-piv-checker -go 1.21 +go 1.23.0 require ( - github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.6.1 - golang.org/x/crypto v0.18.0 + github.com/maxatome/go-testdeep v1.14.0 + golang.org/x/crypto v0.36.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.16.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + golang.org/x/sys v0.31.0 // indirect ) diff --git a/go.sum b/go.sum index 1169c15..4d7f1c6 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,10 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI= +github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= diff --git a/main.go b/lib/checker/checker.go similarity index 51% rename from main.go rename to lib/checker/checker.go index 4372903..74e8a4d 100644 --- a/main.go +++ b/lib/checker/checker.go @@ -1,73 +1,60 @@ -package main +package checker import ( "bytes" "crypto/x509" "encoding/asn1" - "encoding/json" "encoding/pem" "fmt" - "log" - "os" - "golang.org/x/crypto/ssh" + "github.com/ovh/yubico-piv-checker/lib/types" - "github.com/pkg/errors" + "golang.org/x/crypto/ssh" ) -func init() { - log.SetOutput(os.Stderr) -} - -func ParseCertificate(cert string) (*x509.Certificate, error) { +func parseCertificate(cert string) (*x509.Certificate, error) { block, _ := pem.Decode([]byte(cert)) if block == nil || block.Type != "CERTIFICATE" { - return nil, errors.New("Invalid PEM type") + return nil, fmt.Errorf("invalid PEM type") } return x509.ParseCertificate(block.Bytes) } -func VerifySSHKey(sshKey string, attestation string, keyCertificate string) (*Result, error) { +func VerifySSHKey(sshKey string, attestation string, keyCertificate string) (*types.Result, error) { sshPubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey)) if err != nil { - return nil, errors.Wrapf(err, "Failed to parse SSH Key %q", sshKey) + return nil, fmt.Errorf("failed to parse SSH Key %q: %w", sshKey, err) } // Parse attestation and check associated public key - att, err := ParseCertificate(attestation) + att, err := parseCertificate(attestation) if err != nil { - return nil, errors.Wrap(err, "Failed to parse attestation") + return nil, fmt.Errorf("failed to parse attestation: %w", err) } attPubKey, err := ssh.NewPublicKey(att.PublicKey) if err != nil { - return nil, errors.Wrap(err, "Failed to compute SSH Key from attestation") + return nil, fmt.Errorf("failed to compute SSH Key from attestation: %w", err) } if !bytes.Equal(sshPubKey.Marshal(), attPubKey.Marshal()) { - return nil, errors.New("SSH Key doesn't match attestation") + return nil, fmt.Errorf("SSH Key doesn't match attestation") } // Parse key certificate and verify attestation signature - keyCert, err := ParseCertificate(keyCertificate) + keyCert, err := parseCertificate(keyCertificate) if err != nil { - return nil, errors.Wrap(err, "Failed to parse Key Certificate") + return nil, fmt.Errorf("failed to parse Key Certificate: %w", err) } err = keyCert.CheckSignature(att.SignatureAlgorithm, att.RawTBSCertificate, att.Signature) if err != nil { - return nil, errors.Wrap(err, "Invalid attestation signature") - } - - // Parse YubicoCA and verify keyCertificate signature - yubiCert, err := ParseCertificate(yubicoCertificate) - if err != nil { - return nil, errors.Wrap(err, "Failed to parse Yubico Certificate") + return nil, fmt.Errorf("invalid attestation signature: %w", err) } err = yubiCert.CheckSignature(keyCert.SignatureAlgorithm, keyCert.RawTBSCertificate, keyCert.Signature) if err != nil { - return nil, errors.Wrap(err, "Invalid Key Certificate signature") + return nil, fmt.Errorf("invalid Key Certificate signature: %w", err) } - var r Result + var r types.Result r.SSHKey.FingerprintMD5 = ssh.FingerprintLegacyMD5(sshPubKey) r.SSHKey.FingerprintSHA = ssh.FingerprintSHA256(sshPubKey) @@ -81,26 +68,10 @@ func VerifySSHKey(sshKey string, attestation string, keyCertificate string) (*Re } else if e.Id.Equal(oidExtensionYubikeyFirmware) && len(e.Value) == 3 { r.Yubikey.FirmwareVersion = fmt.Sprintf("%d.%d.%d", e.Value[0], e.Value[1], e.Value[2]) } else if e.Id.Equal(oidExtensionYubikeyPolicy) && len(e.Value) == 2 { - r.Yubikey.PinPolicy = YubicoPinPolicy(e.Value[0]) - r.Yubikey.TouchPolicy = YubicoTouchPolicy(e.Value[1]) + r.Yubikey.PinPolicy = types.YubicoPinPolicy(e.Value[0]) + r.Yubikey.TouchPolicy = types.YubicoTouchPolicy(e.Value[1]) } } return &r, nil } - -func main() { - if len(os.Args) != 4 { - fmt.Fprintf(os.Stderr, "Usage: %s ssh-key attestation key-certificate\n", os.Args[0]) - os.Exit(-1) - } - - r, err := VerifySSHKey(os.Args[1], os.Args[2], os.Args[3]) - if err != nil { - log.Fatal(err) - } - err = json.NewEncoder(os.Stdout).Encode(r) - if err != nil { - log.Fatal(err) - } -} diff --git a/main_test.go b/lib/checker/checker_test.go similarity index 79% rename from main_test.go rename to lib/checker/checker_test.go index fad4032..81aa5cf 100644 --- a/main_test.go +++ b/lib/checker/checker_test.go @@ -1,9 +1,11 @@ -package main +package checker_test import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/maxatome/go-testdeep/td" + "github.com/ovh/yubico-piv-checker/lib/checker" + "github.com/ovh/yubico-piv-checker/lib/types" ) var sshKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQClBrnz47s5ER1vnhBSaKIYddvDBty9LFoLOJ3/EmJahzMex80vZA61QO+vRAjM64gwDHgtmoSjiwCAq20J7EZgqJDOuxgX5zLG7rA6xxooQEvVMkmKlHkIeCnBlOwhtr5YjQ4hk0DboLK+955c7kiqW7dJkzHVnzyYG0ILQiSlrY+cCEa/UceGv74fgMQe71B8UC32N27IxN/gssqgHSvgMiQ8nMNQJW2h0mIT3/pKceu+gt4qscZCYYq9Qoz6tPIDZA7KaBZb0Y7kSAenEwjsTQvy5/iE8ELPRBZtmHdW/R78bljX/UZ5sEN5lw9MRHz2zFhFPxdcfpnnQopFH0QJ` @@ -48,13 +50,21 @@ LL62+racaCSKom8Ty1yBgNiZmcho8+buAfU= -----END CERTIFICATE-----` func TestCheck(t *testing.T) { - result, err := VerifySSHKey(sshKey, attestation, keyCertificate) - assert.NoError(t, err) + result, err := checker.VerifySSHKey(sshKey, attestation, keyCertificate) + td.Require(t).CmpNoError(err) - assert.Equal(t, "46:00:b0:eb:d1:fd:b7:86:ea:da:09:7a:49:dd:e3:56", result.SSHKey.FingerprintMD5) - assert.Equal(t, "SHA256:V0yDye/t5QVSC6nAnQ8MsgYUS/bZZKQmB0cYk6+UgqI", result.SSHKey.FingerprintSHA) - assert.Equal(t, 5970478, result.Yubikey.SerialNumber) - assert.Equal(t, "4.3.5", result.Yubikey.FirmwareVersion) - assert.Equal(t, PinPolicyAlways, result.Yubikey.PinPolicy) - assert.Equal(t, TouchPolicyNever, result.Yubikey.TouchPolicy) + td.Cmp(t, result, td.Struct( + &types.Result{ + SSHKey: types.SSHKey{ + FingerprintMD5: "46:00:b0:eb:d1:fd:b7:86:ea:da:09:7a:49:dd:e3:56", + FingerprintSHA: "SHA256:V0yDye/t5QVSC6nAnQ8MsgYUS/bZZKQmB0cYk6+UgqI", + }, + Yubikey: types.Yubikey{ + SerialNumber: 5970478, + FirmwareVersion: "4.3.5", + PinPolicy: types.PinPolicyAlways, + TouchPolicy: types.TouchPolicyNever, + }, + }, + )) } diff --git a/consts.go b/lib/checker/consts.go similarity index 83% rename from consts.go rename to lib/checker/consts.go index d533d3a..9a80520 100644 --- a/consts.go +++ b/lib/checker/consts.go @@ -1,4 +1,9 @@ -package main +package checker + +import ( + "crypto/x509" + "fmt" +) // See https://developers.yubico.com/PIV/Introduction/PIV_attestation.html var oidExtensionYubikeyFirmware = []int{1, 3, 6, 1, 4, 1, 41482, 3, 3} @@ -25,3 +30,15 @@ bW5yWvyS9zNXaqGaUmP3U9/b6DlHdDogMLu3VLpBB9bm5bjaKWWJYgWltCVgUbFq Fqyi4+JE014cSgR57Jcu3dZiehB6UtAPgad9L5cNvua/IWRmm+ANy3O2LH++Pyl8 SREzU8onbBsjMg9QDiSf5oJLKvd/Ren+zGY7 -----END CERTIFICATE-----` + +var yubiCert *x509.Certificate + +func init() { + // Parse YubicoCA and verify keyCertificate signature + cert, err := parseCertificate(yubicoCertificate) + if err != nil { + panic(fmt.Errorf("failed to parse Yubico Certificate: %w", err)) + } + + yubiCert = cert +} diff --git a/types.go b/lib/types/types.go similarity index 62% rename from types.go rename to lib/types/types.go index 4093971..2376576 100644 --- a/types.go +++ b/lib/types/types.go @@ -1,4 +1,4 @@ -package main +package types //go:generate enumer -type YubicoPinPolicy -json -text -trimprefix PinPolicy //go:generate enumer -type YubicoTouchPolicy -json -text -trimprefix TouchPolicy @@ -19,15 +19,19 @@ const ( TouchPolicyCached15s ) +type SSHKey struct { + FingerprintMD5 string + FingerprintSHA string +} + +type Yubikey struct { + SerialNumber int + FirmwareVersion string + PinPolicy YubicoPinPolicy + TouchPolicy YubicoTouchPolicy +} + type Result struct { - SSHKey struct { - FingerprintMD5 string - FingerprintSHA string - } - Yubikey struct { - SerialNumber int - FirmwareVersion string - PinPolicy YubicoPinPolicy - TouchPolicy YubicoTouchPolicy - } + SSHKey SSHKey + Yubikey Yubikey } diff --git a/yubicopinpolicy_enumer.go b/lib/types/yubicopinpolicy_enumer.go similarity index 99% rename from yubicopinpolicy_enumer.go rename to lib/types/yubicopinpolicy_enumer.go index 579e5a0..37150e7 100644 --- a/yubicopinpolicy_enumer.go +++ b/lib/types/yubicopinpolicy_enumer.go @@ -1,6 +1,6 @@ // Code generated by "enumer -type YubicoPinPolicy -json -text -trimprefix PinPolicy"; DO NOT EDIT. -package main +package types import ( "encoding/json" diff --git a/yubicotouchpolicy_enumer.go b/lib/types/yubicotouchpolicy_enumer.go similarity index 99% rename from yubicotouchpolicy_enumer.go rename to lib/types/yubicotouchpolicy_enumer.go index 9c54517..6292af4 100644 --- a/yubicotouchpolicy_enumer.go +++ b/lib/types/yubicotouchpolicy_enumer.go @@ -1,6 +1,6 @@ // Code generated by "enumer -type YubicoTouchPolicy -json -text -trimprefix TouchPolicy"; DO NOT EDIT. -package main +package types import ( "encoding/json"