Skip to content

Commit

Permalink
Merge pull request #36 from smallstep/attestation-object
Browse files Browse the repository at this point in the history
Add support for creating attestation object
  • Loading branch information
maraino committed Feb 7, 2023
2 parents a75a70f + dea64d4 commit df45304
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 3 deletions.
140 changes: 137 additions & 3 deletions cmd/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
"os"

"github.com/fxamacker/cbor/v2"
"github.com/spf13/cobra"
"go.step.sm/crypto/kms"
"go.step.sm/crypto/kms/apiv1"
Expand All @@ -31,17 +42,30 @@ import (
var attestCmd = &cobra.Command{
Use: "attest <uri>",
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 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:
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 <token>.<fingerprint> | 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]
Expand All @@ -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",
Expand Down Expand Up @@ -96,7 +137,100 @@ 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 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 {
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(data)
digest = sum[:]
case *rsa.PublicKey:
// TODO(mariano): support for PS256 (-37)
alg = -257 // RS256
opts = crypto.SHA256
sum := sha256.Sum256(data)
digest = sum[:]
case ed25519.PublicKey:
alg = -8 // EdDSA
opts = crypto.Hash(0)
digest = 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.")
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down

0 comments on commit df45304

Please sign in to comment.