diff --git a/command/ssh/ssh.go b/command/ssh/ssh.go index 3fcadb47c..d15f5389d 100644 --- a/command/ssh/ssh.go +++ b/command/ssh/ssh.go @@ -95,6 +95,7 @@ $ ssh internal.example.com rekeyCommand(), renewCommand(), revokeCommand(), + verifyCommand(), }, } diff --git a/command/ssh/verify.go b/command/ssh/verify.go new file mode 100644 index 000000000..ead01c65b --- /dev/null +++ b/command/ssh/verify.go @@ -0,0 +1,104 @@ +package ssh + +import ( + "bytes" + "encoding/base64" + "fmt" + + "github.com/pkg/errors" + "github.com/urfave/cli" + "golang.org/x/crypto/ssh" + "golang.org/x/exp/maps" + + "go.step.sm/cli-utils/command" + + "github.com/smallstep/cli/utils" +) + +func verifyCommand() cli.Command { + return cli.Command{ + Name: "verify", + Action: command.ActionFunc(verifyAction), + Usage: "verify an ssh certificate", + UsageText: `**step ssh verify** `, + Description: `**step ssh verify** command ... +format. + +`, + } +} + +func verifyAction(ctx *cli.Context) error { + + // TODO: validation of args; allow caFile to be interpreted as directory + var ( + certFile = ctx.Args().Get(0) + caFile = ctx.Args().Get(1) + ) + + certBytes, err := utils.ReadFile(certFile) + if err != nil { + return err + } + + pub, _, _, _, err := ssh.ParseAuthorizedKey(certBytes) + if err != nil { + // Attempt to parse the key without the type. + certBytes = bytes.TrimSpace(certBytes) + keyBytes := make([]byte, base64.StdEncoding.DecodedLen(len(certBytes))) + n, err := base64.StdEncoding.Decode(keyBytes, certBytes) + if err != nil { + return errors.Wrap(err, "error parsing ssh certificate") + } + if pub, err = ssh.ParsePublicKey(keyBytes[:n]); err != nil { + return errors.Wrap(err, "error parsing ssh certificate") + } + } + cert, ok := pub.(*ssh.Certificate) + if !ok { + return errors.Errorf("error decoding ssh certificate: %T is not an *ssh.Certificate", pub) + } + + // TODO: add additional config? This could include a revocation check. + checker := &ssh.CertChecker{ + SupportedCriticalOptions: maps.Keys(cert.CriticalOptions), // allow any option in the certificate + } + + var principal string + if len(cert.ValidPrincipals) > 0 { + principal = cert.ValidPrincipals[0] + } + + // check critical options, principal, validity and signature + if err := checker.CheckCert(principal, cert); err != nil { + return fmt.Errorf("error verifying ssh certificate: %w", err) + } + + caBytes, err := utils.ReadFile(caFile) + if err != nil { + return err + } + + caPub, _, _, _, err := ssh.ParseAuthorizedKey(caBytes) + if err != nil { + // Attempt to parse the key without the type. + certBytes = bytes.TrimSpace(caBytes) + keyBytes := make([]byte, base64.StdEncoding.DecodedLen(len(certBytes))) + n, err := base64.StdEncoding.Decode(keyBytes, certBytes) + if err != nil { + return errors.Wrap(err, "error parsing ssh CA certificate") + } + if caPub, err = ssh.ParsePublicKey(keyBytes[:n]); err != nil { + return errors.Wrap(err, "error parsing ssh CA certificate") + } + } + + // check the certificate was signed by the SSH CA provided + caFP := ssh.FingerprintSHA256(caPub) + certSignerFP := ssh.FingerprintSHA256(cert.SignatureKey) + if certSignerFP != caFP { + return fmt.Errorf("ssh certificate signed by %q does not equal ssh CA %q", certSignerFP, caFP) + } + + return nil +} diff --git a/go.mod b/go.mod index 897af0d27..d1efe5766 100644 --- a/go.mod +++ b/go.mod @@ -121,10 +121,11 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.etcd.io/bbolt v1.3.6 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 // indirect + golang.org/x/mod v0.6.0 // indirect golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect golang.org/x/text v0.6.0 // indirect - golang.org/x/tools v0.1.12 // indirect + golang.org/x/tools v0.2.0 // indirect google.golang.org/api v0.106.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect diff --git a/go.sum b/go.sum index 09fd85878..0f903d95d 100644 --- a/go.sum +++ b/go.sum @@ -647,6 +647,8 @@ golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4 golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 h1:fiNkyhJPUvxbRPbCqY/D9qdjmPzfHcpK3P4bM4gioSY= +golang.org/x/exp v0.0.0-20230118134722-a68e582fa157/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -656,6 +658,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -766,6 +770,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=