Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TPMKMS support #71

Merged
merged 18 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 83 additions & 46 deletions cmd/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ var attestCmd = &cobra.Command{
Use: "attest <uri>",
Short: "create an attestation certificate",
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.
is set, an attestation object. Currently this command is only supported with
YubiKeys and the TPM KMS.

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
Expand All @@ -57,18 +57,36 @@ account key fingerprint separated by a "." character:
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`,
echo -n <token>.<fingerprint> | step-kms-plugin attest --format step yubikey:slot-id=9c

# Get the attestation certificate belonging to an Attestion Key, using the default TPM KMS:
step-kms-plugin attest 'tpmkms:name=my-ak;ak=true'

# Get the attestation certificate for an attested key, using the default TPM KMS:
step-kms-plugin attest tpmkms:name=my-attested-key

# Get the attestation certificate chain for an attested key, using the default TPM KMS:
step-kms-plugin attest --bundle tpmkms:name=my-attested-key

# Create an attestation statement for an attested key, using the default TPM KMS:
step-kms-plugin attest --format tpm tpmkms:name=my-attested-key

# Create an attestation statement for an attested key, using the default TPM KMS,
enrolling with a Smallstep Attestation CA if no AK certificate is available (yet):
step-kms-plugin attest --format tpm 'tpmkms:name=my-attested-key;attestation-ca-url=https://my.attestation.ca/url;attestation-ca-root=/path/to/trusted/roots.pem'`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return showErrUsage(cmd)
}

name := args[0]
flags := cmd.Flags()
format := flagutil.MustString(flags, "format")
bundle := flagutil.MustBool(flags, "bundle")
in := flagutil.MustString(flags, "in")
kuri := flagutil.MustString(flags, "kms")
kuri := ensureSchemePrefix(flagutil.MustString(flags, "kms"))
if kuri == "" {
kuri = args[0]
kuri = name
}

km, err := kms.New(cmd.Context(), apiv1.Options{
Expand All @@ -85,46 +103,49 @@ account key fingerprint separated by a "." character:
}

resp, err := attester.CreateAttestation(&apiv1.CreateAttestationRequest{
Name: args[0],
Name: name,
})
if err != nil {
return fmt.Errorf("failed to attest: %w", err)
}

switch {
case format != "":
data, err := getAttestationData(in)
if err != nil {
return err
var data []byte
var signer crypto.Signer
if format != "tpm" { // the tpm format doesn't require data to be signed
data, err = getAttestationData(in)
if err != nil {
return err
}
}
signer, err := km.CreateSigner(&apiv1.CreateSignerRequest{
SigningKey: args[0],
})
if err != nil {
if signer, err = km.CreateSigner(&apiv1.CreateSignerRequest{
SigningKey: name,
}); 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",
Bytes: resp.Certificate.Raw,
}); err != nil {
return fmt.Errorf("failed to encode certificate: %w", err)
switch {
case len(resp.CertificateChain) > 0:
certs = resp.CertificateChain
case resp.Certificate != nil:
certs = []*x509.Certificate{resp.Certificate}
}
for _, c := range resp.CertificateChain {
if err := pem.Encode(os.Stdout, &pem.Block{
Type: "CERTIFICATE",
Bytes: c.Raw,
}); err != nil {
return fmt.Errorf("failed to encode certificate chain: %w", err)
return printAttestationObject(format, certs, signer, data, resp.CertificationParameters)
case len(resp.CertificateChain) > 0:
switch {
case bundle:
for _, c := range resp.CertificateChain {
if err := outputCert(c); err != nil {
return err
}
}
default:
return outputCert(resp.CertificateChain[0])
}
return nil
case resp.Certificate != nil:
return outputCert(resp.Certificate)
case resp.PublicKey != nil:
block, err := pemutil.Serialize(resp.PublicKey)
if err != nil {
Expand Down Expand Up @@ -157,7 +178,7 @@ func getAttestationData(in string) ([]byte, error) {
return io.ReadAll(os.Stdin)
}

func printAttestationObject(format string, certs []*x509.Certificate, signer crypto.Signer, data []byte) error {
func printAttestationObject(format string, certs []*x509.Certificate, signer crypto.Signer, data []byte, params *apiv1.CertificationParameters) error {
var alg int64
var digest []byte
var opts crypto.SignerOpts
Expand All @@ -184,21 +205,36 @@ func printAttestationObject(format string, certs []*x509.Certificate, signer cry
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,
}

switch format {
case "tpm":
// TPM key attestation is performed at key creation time. The key is attested by
// an Attestation Key (AK). The result of attesting a key can be recorded, so that
// the certification facts can be used at a later time to verify the key was created
// by a specific TPM.
if params == nil {
return errors.New("TPM key attestation requires CertificationParameters to be set")
}
stmt["ver"] = "2.0"
stmt["sig"] = params.CreateSignature // signature over the (empty) data is ignored for the tpm format
stmt["certInfo"] = params.CreateAttestation
stmt["pubArea"] = params.Public
default:
// 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["sig"] = sig
}

if len(certs) > 0 {
Expand Down Expand Up @@ -230,7 +266,8 @@ func init() {
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")
format := flagutil.LowerValue("format", []string{"", "step", "packed", "tpm"}, "")
flags.Var(format, "format", "The `format` to print the attestation.\nOptions are step, packed or tpm")
flags.Bool("bundle", false, "Print all certificates in the chain")
maraino marked this conversation as resolved.
Show resolved Hide resolved
flags.String("in", "", "The `file` to sign with an attestation format.")
}
112 changes: 86 additions & 26 deletions cmd/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
package cmd

import (
"encoding/pem"
"crypto/x509"
"fmt"
"io/fs"

Expand All @@ -30,38 +30,75 @@ import (
var certificateCmd = &cobra.Command{
Use: "certificate <uri>",
Short: "print or import a certificate in a KMS",
Long: `This command, if the KMS supports it, it prints or imports a certificate in a KMS.`,
Long: `This command, if the KMS supports it, prints or imports a certificate in a KMS.`,
Example: ` # Import a certificate to a PKCS #11 module:
step-kms-plugin certificate --import cert.pem \
--kms 'pkcs11:module-path=/path/to/libsofthsm2.so;token=softhsm?pin-value=pass' \
'pkcs11:id=2000;object=my-cert'

# Print a previously store certificate:
# Print a previously stored certificate:
step-kms-plugin certificate \
--kms 'pkcs11:module-path=/path/to/libsofthsm2.so;token=softhsm?pin-value=pass' \
'pkcs11:id=2000;object=my-cert'`,
'pkcs11:id=2000;object=my-cert'

# Print a previously stored certificate for an Attestation Key (AK), using the default TPM KMS:
step-kms-plugin certificate 'tpmkms:name=my-ak;ak=true'

# Print a previously stored certificate, using the default TPM KMS:
step-kms-plugin certificate tpmkms:name=my-key

# Print a previously stored certificate chain, using the default TPM KMS:
step-kms-plugin certificate --bundle tpmkms:name=my-key`,
maraino marked this conversation as resolved.
Show resolved Hide resolved
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return showErrUsage(cmd)
}

name := args[0]
flags := cmd.Flags()
certFile := flagutil.MustString(flags, "import")
bundle := flagutil.MustBool(flags, "bundle")

kuri := flagutil.MustString(flags, "kms")
kuri := ensureSchemePrefix(flagutil.MustString(flags, "kms"))
if kuri == "" {
kuri = args[0]
kuri = name
}

// Read a certificate using the CertFS.
if certFile == "" {
if bundle {
km, err := kms.New(cmd.Context(), apiv1.Options{
URI: kuri,
})
if err != nil {
return fmt.Errorf("failed to load key manager: %w", err)
}
defer km.Close()
if cm, ok := km.(apiv1.CertificateChainManager); ok {
certs, err := cm.LoadCertificateChain(&apiv1.LoadCertificateChainRequest{
Name: name,
})
if err != nil {
return err
}
for _, c := range certs {
outputCert(c)
}
return nil
}
return fmt.Errorf("--bundle is not compatible with %q", kuri)
}

// TODO(hs): support reading a certificate chain / bundle instead of
// just single certificate in the CertFS instead? Would require supporting
// serializing multiple things to PEM, e.g. a certificate chain.
fsys, err := kms.CertFS(cmd.Context(), kuri)
if err != nil {
return err
}
defer fsys.Close()

b, err := fs.ReadFile(fsys, args[0])
b, err := fs.ReadFile(fsys, name)
if err != nil {
return err
}
Expand All @@ -71,7 +108,8 @@ var certificateCmd = &cobra.Command{
}

// Import and read certificate using the key manager to avoid opening the kms twice.
cert, err := pemutil.ReadCertificate(certFile)
var cert *x509.Certificate
certs, err := pemutil.ReadCertificateBundle(certFile)
if err != nil {
return err
}
Expand All @@ -84,26 +122,47 @@ var certificateCmd = &cobra.Command{
}
defer km.Close()

cm, ok := km.(apiv1.CertificateManager)
if !ok {
return fmt.Errorf("%s does not implement a CertificateManager", kuri)
}
if err := cm.StoreCertificate(&apiv1.StoreCertificateRequest{
Name: args[0],
Certificate: cert,
}); err != nil {
return err
switch cm := km.(type) {
case apiv1.CertificateChainManager:
if err := cm.StoreCertificateChain(&apiv1.StoreCertificateChainRequest{
Name: name,
CertificateChain: certs,
}); err != nil {
return err
}
certs, err = cm.LoadCertificateChain(&apiv1.LoadCertificateChainRequest{
Name: name,
})
if err != nil {
return err
}
cert = certs[0]
case apiv1.CertificateManager:
if err := cm.StoreCertificate(&apiv1.StoreCertificateRequest{
Name: name,
Certificate: cert,
}); err != nil {
return err
}
cert, err = cm.LoadCertificate(&apiv1.LoadCertificateRequest{
Name: name,
})
if err != nil {
return err
}
default:
return fmt.Errorf("%q does not implement a CertificateManager or CertificateChainManager", kuri)
}
cert, err = cm.LoadCertificate(&apiv1.LoadCertificateRequest{
Name: args[0],
})
if err != nil {
return err

switch {
case bundle:
for _, c := range certs {
outputCert(c)
}
default:
outputCert(cert)
}
fmt.Print(string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
})))

return nil
},
}
Expand All @@ -116,4 +175,5 @@ func init() {
flags.SortFlags = false

flags.String("import", "", "The certificate `file` to import")
flags.Bool("bundle", false, "Print all certificates in the chain/bundle")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would print the bundled certificate by default. A different option can be added to show only the leaf. Probably something cleaner that --bundled=false

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left this as is, to be consistent with our other tooling.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

step-kms-plugin certificate is mainly used to load a certificate from a KMS when step ca certificate --x5c-cert is used. The --x5c-cert flag expects a bundled certificate, that is what you will use with files. The flag --x5c-chain was added to support a bundled certificate in those KMSs that only support a single certificate. There is some inconsistency because not all KMSs support the same things, but I don't think we want to add --x5c-chain unless it is necessary.

We can keep the flag here, for "consistency" with step certificate inspect, but we will need to change the cli to add the --bundle option. This will cause some incompatibility depending if you are using the last version of the plugin or not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
Loading