From 34440005caf3be5e57d1459c9a6cb08c7a6ac8ea Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 6 Feb 2023 09:58:33 -0800 Subject: [PATCH 1/3] Add support for creating attestation object Attestation objects can be used to resolve ACME device-attest-01 challenges. --- cmd/attest.go | 141 ++++++++++++++++++++++++++++++++++++++++++++++++-- go.mod | 2 + go.sum | 4 ++ 3 files changed, 144 insertions(+), 3 deletions(-) diff --git a/cmd/attest.go b/cmd/attest.go index 9cf7b82..024d72c 100644 --- a/cmd/attest.go +++ b/cmd/attest.go @@ -14,11 +14,22 @@ package cmd import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" "encoding/pem" "errors" "fmt" + "io/ioutil" "os" + "github.com/fxamacker/cbor/v2" "github.com/spf13/cobra" "go.step.sm/crypto/kms" "go.step.sm/crypto/kms/apiv1" @@ -31,17 +42,30 @@ import ( var attestCmd = &cobra.Command{ Use: "attest ", Short: "create an attestation certificate", - Long: `This command, if the KMS supports it, it prints an attestation certificate or an endorsement key. + Long: `Print an attestation certificate, an endorsement key, or if the "--format" flag +is set, an attestation object. Currently this command is only supported on +YubiKeys. -Currently this command is only supported on YubiKeys.`, +An attestation object can be used to resolve an ACME device-attest-01 challenge. +To pass this challenge the client needs to show prove of possession of a private +key by signing the ACME key authorization, the format is defined by RFC 8555 as +a string that concatenates the token for the challenge with a key fingerprint +separated by a "." character: + + keyAuthorization = token || '.' || base64url(Thumbprint(accountKey))`, Example: ` # Get the attestation certificate from a YubiKey: - step-kms-plugin attest yubikey:slot-id=9c`, + step-kms-plugin attest yubikey:slot-id=9c + + # Create an attestation object used in an ACME device-attest-01 flow: + echo -n . | step-kms-plugin attest --format step yubikey:slot-id=9c`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return showErrUsage(cmd) } flags := cmd.Flags() + format := flagutil.MustString(flags, "format") + in := flagutil.MustString(flags, "in") kuri := flagutil.MustString(flags, "kms") if kuri == "" { kuri = args[0] @@ -68,6 +92,23 @@ Currently this command is only supported on YubiKeys.`, } switch { + case format != "": + data, err := getAttestationData(in) + if err != nil { + return err + } + signer, err := km.CreateSigner(&apiv1.CreateSignerRequest{ + SigningKey: args[0], + }) + if err != nil { + return fmt.Errorf("failed to get a signer: %w", err) + } + var certs []*x509.Certificate + if resp.Certificate != nil { + certs = append([]*x509.Certificate{}, resp.Certificate) + certs = append(certs, resp.CertificateChain...) + } + return printAttestationObject(format, certs, signer, data) case resp.Certificate != nil: if err := pem.Encode(os.Stdout, &pem.Block{ Type: "CERTIFICATE", @@ -96,7 +137,101 @@ Currently this command is only supported on YubiKeys.`, }, } +type attestationObject struct { + Format string `json:"fmt"` + AttStatement map[string]interface{} `json:"attStmt,omitempty"` +} + +func getAttestationData(in string) ([]byte, error) { + if in != "" { + return os.ReadFile(in) + } + fi, err := os.Stdin.Stat() + if err != nil { + return nil, err + } + if (fi.Mode() & os.ModeCharDevice) == 0 { + return ioutil.ReadAll(os.Stdin) + } else { + fmt.Println("Type data to sign and press Ctrl+D to finish:") + return ioutil.ReadAll(os.Stdin) + } +} + +func printAttestationObject(format string, certs []*x509.Certificate, signer crypto.Signer, data []byte) error { + var alg int64 + var digest []byte + var opts crypto.SignerOpts + switch k := signer.Public().(type) { + case *ecdsa.PublicKey: + if k.Curve != elliptic.P256() { + return fmt.Errorf("unsupported elliptic curve %s", k.Curve) + } + alg = -7 // ES256 + opts = crypto.SHA256 + sum := sha256.Sum256([]byte(data)) + digest = sum[:] + case *rsa.PublicKey: + // TODO(mariano): support for PS256 (-37) + alg = -257 // RS256 + opts = crypto.SHA256 + sum := sha256.Sum256([]byte(data)) + digest = sum[:] + case ed25519.PublicKey: + alg = -8 // EdDSA + opts = crypto.Hash(0) + digest = []byte(data) + default: + return fmt.Errorf("unsupported public key type %T", k) + } + + // Sign proves possession of private key. Per recommendation at + // https://w3c.github.io/webauthn/#sctn-signature-attestation-types, we use + // CBOR to encode the signature. + sig, err := signer.Sign(rand.Reader, digest, opts) + if err != nil { + return fmt.Errorf("failed to sign key authorization: %w", err) + } + sig, err = cbor.Marshal(sig) + if err != nil { + return fmt.Errorf("failed marshaling signature: %w", err) + } + + stmt := map[string]interface{}{ + "alg": alg, + "sig": sig, + } + + if len(certs) > 0 { + x5c := make([][]byte, len(certs)) + for i, c := range certs { + x5c[i] = c.Raw + } + stmt["x5c"] = x5c + } + + obj := attestationObject{ + Format: format, + AttStatement: stmt, + } + + b, err := cbor.Marshal(obj) + if err != nil { + return fmt.Errorf("failed marshaling attestation object: %w", err) + } + + fmt.Println(base64.RawURLEncoding.EncodeToString(b)) + return nil +} + func init() { rootCmd.AddCommand(attestCmd) attestCmd.SilenceUsage = true + + flags := attestCmd.Flags() + flags.SortFlags = false + + format := flagutil.LowerValue("format", []string{"", "step", "packed"}, "") + flags.Var(format, "format", "The `format` to print the attestation.\nOptions are step or packed") + flags.String("in", "", "The `file` to sign with an attestation format.") } diff --git a/go.mod b/go.mod index fcbd473..b61ee54 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/smallstep/step-kms-plugin go 1.18 require ( + github.com/fxamacker/cbor/v2 v2.4.0 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 go.step.sm/crypto v0.23.2 @@ -53,6 +54,7 @@ require ( github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/thales-e-security/pool v0.0.2 // indirect + github.com/x448/float16 v0.8.4 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/net v0.5.0 // indirect golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect diff --git a/go.sum b/go.sum index 2ecfaf0..19ea561 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= +github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/go-piv/piv-go v1.10.0 h1:P1Y1VjBI5DnXW0+YkKmTuh5opWnMIrKriUaIOblee9Q= github.com/go-piv/piv-go v1.10.0/go.mod h1:NZ2zmjVkfFaL/CF8cVQ/pXdXtuj110zEKGdJM6fJZZM= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= @@ -153,6 +155,8 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= From b7db7c8d6ad5b9f49db409b10ec3ac35029e6383 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 6 Feb 2023 15:13:49 -0800 Subject: [PATCH 2/3] Clarify attest command help --- cmd/attest.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/attest.go b/cmd/attest.go index 024d72c..07a7fdd 100644 --- a/cmd/attest.go +++ b/cmd/attest.go @@ -47,10 +47,10 @@ is set, an attestation object. Currently this command is only supported on YubiKeys. An attestation object can be used to resolve an ACME device-attest-01 challenge. -To pass this challenge the client needs to show prove of possession of a private -key by signing the ACME key authorization, the format is defined by RFC 8555 as -a string that concatenates the token for the challenge with a key fingerprint -separated by a "." character: +To pass this challenge, the client needs proof of possession of a private key by +signing the ACME key authorization. The format is defined in RFC 8555 as a +string that concatenates the challenge token for the challenge with the ACME +account key fingerprint separated by a "." character: keyAuthorization = token || '.' || base64url(Thumbprint(accountKey))`, Example: ` # Get the attestation certificate from a YubiKey: From dea64d4e7d7b60c15fd7ffbdba34a61868ac8584 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 6 Feb 2023 15:20:37 -0800 Subject: [PATCH 3/3] Fix linter errors --- cmd/attest.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cmd/attest.go b/cmd/attest.go index 07a7fdd..4b6664b 100644 --- a/cmd/attest.go +++ b/cmd/attest.go @@ -26,7 +26,7 @@ import ( "encoding/pem" "errors" "fmt" - "io/ioutil" + "io" "os" "github.com/fxamacker/cbor/v2" @@ -151,11 +151,10 @@ func getAttestationData(in string) ([]byte, error) { return nil, err } if (fi.Mode() & os.ModeCharDevice) == 0 { - return ioutil.ReadAll(os.Stdin) - } else { - fmt.Println("Type data to sign and press Ctrl+D to finish:") - return ioutil.ReadAll(os.Stdin) + return io.ReadAll(os.Stdin) } + fmt.Println("Type data to sign and press Ctrl+D to finish:") + return io.ReadAll(os.Stdin) } func printAttestationObject(format string, certs []*x509.Certificate, signer crypto.Signer, data []byte) error { @@ -169,18 +168,18 @@ func printAttestationObject(format string, certs []*x509.Certificate, signer cry } alg = -7 // ES256 opts = crypto.SHA256 - sum := sha256.Sum256([]byte(data)) + sum := sha256.Sum256(data) digest = sum[:] case *rsa.PublicKey: // TODO(mariano): support for PS256 (-37) alg = -257 // RS256 opts = crypto.SHA256 - sum := sha256.Sum256([]byte(data)) + sum := sha256.Sum256(data) digest = sum[:] case ed25519.PublicKey: alg = -8 // EdDSA opts = crypto.Hash(0) - digest = []byte(data) + digest = data default: return fmt.Errorf("unsupported public key type %T", k) }