Skip to content

Commit

Permalink
Merge pull request #1092 from smallstep/fix-1637
Browse files Browse the repository at this point in the history
Add handling of `cnf` claim
  • Loading branch information
maraino authored Jul 24, 2024
2 parents 7e969c8 + 8a2d36e commit 797ae59
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 42 deletions.
2 changes: 1 addition & 1 deletion command/ca/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func signCertificateAction(ctx *cli.Context) error {
}

// certificate flow unifies online and offline flows on a single api
flow, err := cautils.NewCertificateFlow(ctx)
flow, err := cautils.NewCertificateFlow(ctx, cautils.WithCertificateRequest(csr))
if err != nil {
return err
}
Expand Down
55 changes: 54 additions & 1 deletion command/ca/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"

"github.com/pkg/errors"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/pki"
"github.com/smallstep/cli/flags"
Expand All @@ -12,6 +13,8 @@ import (
"github.com/urfave/cli"
"go.step.sm/cli-utils/command"
"go.step.sm/cli-utils/errs"
"go.step.sm/crypto/pemutil"
"golang.org/x/crypto/ssh"
)

func tokenCommand() cli.Command {
Expand All @@ -27,6 +30,7 @@ func tokenCommand() cli.Command {
[**--output-file**=<file>] [**--kms**=uri] [**--key**=<file>] [**--san**=<SAN>] [**--offline**]
[**--revoke**] [**--x5c-cert**=<file>] [**--x5c-key**=<file>] [**--x5c-insecure**]
[**--sshpop-cert**=<file>] [**--sshpop-key**=<file>]
[**--cnf**=<fingerprint>] [**--cnf-file**=<file>]
[**--ssh**] [**--host**] [**--principal**=<name>] [**--k8ssa-token-path**=<file>]
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>]`,
Description: `**step ca token** command generates a one-time token granting access to the
Expand Down Expand Up @@ -82,6 +86,18 @@ Get a new token that becomes valid in 30 minutes and expires 5 minutes after tha
$ step ca token --not-before 30m --not-after 35m internal.example.com
'''
Get a new token with a confirmation claim to enforce a given CSR fingerprint:
'''
$ step certificate fingerprint --format base64-url-raw internal.csr
PJLNhtQoBE1yGN_ZKzr4Y2U5pyqIGiyyszkoz2raDOw
$ step ca token --cnf PJLNhtQoBE1yGN_ZKzr4Y2U5pyqIGiyyszkoz2raDOw internal.smallstep.com
'''
Get a new token with a confirmation claim to enforce the use of a given CSR:
'''
step ca token --cnf-file internal.csr internal.smallstep.com
'''
Get a new token signed with the given private key, the public key must be
configured in the certificate authority:
'''
Expand Down Expand Up @@ -133,6 +149,11 @@ Get a new token for an SSH host certificate:
$ step ca token my-remote.hostname --ssh --host
'''
Get a new token with a confirmation claim to enforce the use of a given public key:
'''
step ca token --ssh --host --cnf-file internal.pub internal.smallstep.com
'''
Generate a renew token and use it in a renew after expiry request:
'''
$ TOKEN=$(step ca token --x5c-cert internal.crt --x5c-key internal.key --renew internal.example.com)
Expand Down Expand Up @@ -186,6 +207,8 @@ multiple principals.`,
flags.SSHPOPKey,
flags.NebulaCert,
flags.NebulaKey,
flags.Confirmation,
flags.ConfirmationFile,
cli.StringFlag{
Name: "key",
Usage: `The private key <file> used to sign the JWT. This is usually downloaded from
Expand Down Expand Up @@ -240,6 +263,9 @@ func tokenAction(ctx *cli.Context) error {
isSSH := ctx.Bool("ssh")
isHost := ctx.Bool("host")
principals := ctx.StringSlice("principal")
// confirmation claims
cnfFile := ctx.String("cnf-file")
cnf := ctx.String("cnf")

switch {
case isSSH && len(sans) > 0:
Expand All @@ -252,6 +278,8 @@ func tokenAction(ctx *cli.Context) error {
return errs.RequiredWithFlag(ctx, "host", "ssh")
case !isSSH && len(principals) > 0:
return errs.RequiredWithFlag(ctx, "principal", "ssh")
case cnfFile != "" && cnf != "":
return errs.IncompatibleFlagWithFlag(ctx, "cnf-file", "cnf")
}

// Default token type is always a 'Sign' token.
Expand Down Expand Up @@ -295,6 +323,31 @@ func tokenAction(ctx *cli.Context) error {
}
}

// Add options to create a confirmation claim if a CSR or SSH public key is
// passed.
var tokenOpts []cautils.Option
if cnfFile != "" {
in, err := utils.ReadFile(cnfFile)
if err != nil {
return err
}
if isSSH {
sshPub, _, _, _, err := ssh.ParseAuthorizedKey(in)
if err != nil {
return errors.Wrap(err, "error parsing ssh public key")
}
tokenOpts = append(tokenOpts, cautils.WithSSHPublicKey(sshPub))
} else {
csr, err := pemutil.ParseCertificateRequest(in)
if err != nil {
return errors.Wrap(err, "error parsing certificate request")
}
tokenOpts = append(tokenOpts, cautils.WithCertificateRequest(csr))
}
} else if cnf != "" {
tokenOpts = append(tokenOpts, cautils.WithConfirmationFingerprint(cnf))
}

// --san and --type revoke are incompatible. Revocation tokens do not support SANs.
if typ == cautils.RevokeType && len(sans) > 0 {
return errs.IncompatibleFlagWithFlag(ctx, "san", "revoke")
Expand Down Expand Up @@ -327,7 +380,7 @@ func tokenAction(ctx *cli.Context) error {
return err
}
} else {
token, err = cautils.NewTokenFlow(ctx, typ, subject, sans, caURL, root, notBefore, notAfter, certNotBefore, certNotAfter)
token, err = cautils.NewTokenFlow(ctx, typ, subject, sans, caURL, root, notBefore, notAfter, certNotBefore, certNotAfter, tokenOpts...)
if err != nil {
return err
}
Expand Down
18 changes: 16 additions & 2 deletions command/certificate/fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func fingerprintCommand() cli.Command {
[**--bundle**] [**--roots**=<root-bundle>] [**--servername**=<servername>]
[**--format**=<format>] [**--sha1**] [**--insecure**]`,
Description: `**step certificate fingerprint** reads a certificate and prints to STDOUT the
certificate SHA256 of the raw certificate.
certificate SHA256 of the raw certificate or certificate signing request.
If <crt-file> contains multiple certificates (i.e., it is a certificate
"bundle") the fingerprint of the first certificate in the bundle will be
Expand Down Expand Up @@ -55,6 +55,12 @@ Get the fingerprints for a remote certificate with its intermediate:
$ step certificate fingerprint --bundle https://smallstep.com
e2c4f12edfc1816cc610755d32e6f45d5678ba21ecda1693bb5b246e3c48c03d
25847d668eb4f04fdd40b12b6b0740c567da7d024308eb6c2c96fe41d9de218d
'''
Get the fingerprint for a CSR using base64-url encoding without padding:
'''
$ step certificate fingerprint --format base64-url-raw hello.csr
PJLNhtQoBE1yGN_ZKzr4Y2U5pyqIGiyyszkoz2raDOw
'''`,
Flags: []cli.Flag{
cli.StringFlag{
Expand Down Expand Up @@ -128,7 +134,15 @@ func fingerprintAction(ctx *cli.Context) error {
default:
certs, err = pemutil.ReadCertificateBundle(crtFile)
if err != nil {
return err
// Fallback to parse a CSR
csr, csrErr := pemutil.ReadCertificateRequest(crtFile)
if csrErr != nil {
return err
}
// We will only need the raw DER bytes to generate a fingerprint.
certs = []*x509.Certificate{
{Raw: csr.Raw},
}
}
}

Expand Down
70 changes: 37 additions & 33 deletions command/ssh/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,43 @@ func certificateAction(ctx *cli.Context) error {
}
}

flow, err := cautils.NewCertificateFlow(ctx)
var (
sshPub ssh.PublicKey
pub, priv interface{}
flowOptions []cautils.Option
)

if isSign {
// Use public key supplied as input.
in, err := utils.ReadFile(keyFile)
if err != nil {
return err
}

sshPub, _, _, _, err = ssh.ParseAuthorizedKey(in)
if err != nil {
return errors.Wrap(err, "error parsing ssh public key")
}
if sshPrivKeyFile != "" {
if priv, err = pemutil.Read(sshPrivKeyFile); err != nil {
return errors.Wrap(err, "error parsing private key")
}
}
flowOptions = append(flowOptions, cautils.WithSSHPublicKey(sshPub))
} else {
// Generate keypair
pub, priv, err = keyutil.GenerateKeyPair(kty, curve, size)
if err != nil {
return err
}

sshPub, err = ssh.NewPublicKey(pub)
if err != nil {
return errors.Wrap(err, "error creating public key")
}
}

flow, err := cautils.NewCertificateFlow(ctx, flowOptions...)
if err != nil {
return err
}
Expand Down Expand Up @@ -382,38 +418,6 @@ func certificateAction(ctx *cli.Context) error {
identityKey = key
}

var sshPub ssh.PublicKey
var pub, priv interface{}

if isSign {
// Use public key supplied as input.
in, err := utils.ReadFile(keyFile)
if err != nil {
return err
}

sshPub, _, _, _, err = ssh.ParseAuthorizedKey(in)
if err != nil {
return errors.Wrap(err, "error parsing ssh public key")
}
if sshPrivKeyFile != "" {
if priv, err = pemutil.Read(sshPrivKeyFile); err != nil {
return errors.Wrap(err, "error parsing private key")
}
}
} else {
// Generate keypair
pub, priv, err = keyutil.GenerateKeyPair(kty, curve, size)
if err != nil {
return err
}

sshPub, err = ssh.NewPublicKey(pub)
if err != nil {
return errors.Wrap(err, "error creating public key")
}
}

var sshAuPub ssh.PublicKey
var sshAuPubBytes []byte
var auPub, auPriv interface{}
Expand Down
13 changes: 13 additions & 0 deletions flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,19 @@ be stored in the 'sshpop' header.`,
be stored in the 'nebula' header.`,
}

// Confirmation is a cli.Flag used to add a confirmation claim in the token.
Confirmation = cli.StringFlag{
Name: "cnf",
Usage: `The <fingerprint> of the CSR to restrict this token for.`,
}

// ConfirmationFile is a cli.Flag used to add a confirmation claim in the
// tokens. It will add a confirmation kid with the fingerprint of the CSR.
ConfirmationFile = cli.StringFlag{
Name: "cnf-file",
Usage: `The CSR <file> to restrict this token for.`,
}

// Team is a cli.Flag used to pass the team ID.
Team = cli.StringFlag{
Name: "team",
Expand Down
36 changes: 36 additions & 0 deletions token/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package token

import (
"bytes"
"crypto"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/ed25519"
Expand All @@ -15,6 +16,7 @@ import (

"github.com/pkg/errors"
nebula "github.com/slackhq/nebula/cert"
"go.step.sm/crypto/fingerprint"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x25519"
Expand Down Expand Up @@ -84,6 +86,40 @@ func WithSSH(v interface{}) Options {
})
}

// WithConfirmationFingerprint returns an Options function that sets the cnf
// claim with the given CSR fingerprint.
func WithConfirmationFingerprint(fp string) Options {
return func(c *Claims) error {
c.Set(ConfirmationClaim, map[string]string{
"x5rt#S256": fp,
})
return nil
}
}

// WithFingerprint returns an Options function that the cnf claims with
// "x5rt#S256" representing the fingerprint of the CSR
func WithFingerprint(v any) Options {
return func(c *Claims) error {
var data []byte
switch vv := v.(type) {
case *x509.CertificateRequest:
data = vv.Raw
default:
return fmt.Errorf("unsupported fingerprint for %T", v)
}

kid, err := fingerprint.New(data, crypto.SHA256, fingerprint.Base64RawURLFingerprint)
if err != nil {
return err
}
c.Set(ConfirmationClaim, map[string]string{
"x5rt#S256": kid,
})
return nil
}
}

// WithValidity validates boundary inputs and sets the 'nbf' (NotBefore) and
// 'exp' (expiration) options.
func WithValidity(notBefore, expiration time.Time) Options {
Expand Down
23 changes: 23 additions & 0 deletions token/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x25519"
"golang.org/x/crypto/ssh"
)

func TestOptions(t *testing.T) {
Expand All @@ -35,6 +37,11 @@ func TestOptions(t *testing.T) {
p256ECDHSigner, err := p256Signer.ECDH()
require.NoError(t, err)

testCSR, err := pemutil.ReadCertificateRequest("testdata/test.csr")
require.NoError(t, err)

testSSH := mustReadSSHPublicKey(t, "testdata/ssh-key.pub")

wrongNebulaContentsFilename := "testdata/ca.crt"

emptyFile, err := os.CreateTemp(tempDir, "empty-file")
Expand Down Expand Up @@ -79,6 +86,10 @@ func TestOptions(t *testing.T) {
{"WithNebulaCurve25519Cert empty file fail", WithNebulaCert(emptyFile.Name(), nil), empty, true},
{"WithNebulaCurve25519Cert invalid content fail", WithNebulaCert(c25519CertFilename, nil), empty, true},
{"WithNebulaCurve25519Cert mismatching key fail", WithNebulaCert(c25519CertFilename, p256Signer), empty, true},
{"WithConfirmationFingerprint ok", WithConfirmationFingerprint("my-kid"), &Claims{ExtraClaims: map[string]any{"cnf": map[string]string{"kid": "my-kid"}}}, false},
{"WithFingerprint csr ok", WithFingerprint(testCSR), &Claims{ExtraClaims: map[string]any{"cnf": map[string]string{"kid": "ak6j6CwuZbd_mOQ-pNOUwhpmtSN0mY0xrLvaQL4J5l8"}}}, false},
{"WithFingerprint ssh ok", WithFingerprint(testSSH), &Claims{ExtraClaims: map[string]any{"cnf": map[string]string{"kid": "hpTQOoB7fIRxTp-FhXCIm94mGBv7_dzr_5SxLn1Pnwk"}}}, false},
{"WithFingerprint fail", WithFingerprint("unexpected type"), empty, true},
}

for _, tt := range tests {
Expand All @@ -96,6 +107,18 @@ func TestOptions(t *testing.T) {
}
}

func mustReadSSHPublicKey(t *testing.T, filename string) ssh.PublicKey {
t.Helper()

b, err := os.ReadFile(filename)
require.NoError(t, err)

pub, _, _, _, err := ssh.ParseAuthorizedKey(b)
require.NoError(t, err)

return pub
}

func serializeAndWriteNebulaCert(t *testing.T, tempDir string, cert *nebula.NebulaCertificate) (string, []byte) {
file, err := os.CreateTemp(tempDir, "nebula-test-cert-*")
require.NoError(t, err)
Expand Down
1 change: 1 addition & 0 deletions token/testdata/ssh-key.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF14RP3HJkO1yoZHjo9t/4bJgyJGiSPxhm6FApa3VtG1 [email protected]
8 changes: 8 additions & 0 deletions token/testdata/test.csr
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBBDCBqwIBADAbMRkwFwYDVQQDDBB0ZXN0QGV4YW1wbGUuY29tMFkwEwYHKoZI
zj0CAQYIKoZIzj0DAQcDQgAEPj0tlICeGPiz361yM+AGlZmDK+N/cT0SVloozOQH
1ljdNbookliEX8eRnFnelZRaql1KhrVOXhfwBmd/eGhti6AuMCwGCSqGSIb3DQEJ
DjEfMB0wGwYDVR0RBBQwEoEQdGVzdEBleGFtcGxlLmNvbTAKBggqhkjOPQQDAgNI
ADBFAiEA4WuukEVIFJQHNqlZVsWtsWsSVLNRCxBBJfH7/+txNw4CIGyK3eo5MDvR
DepPHVRF16/b+iW/4HgAgIC90+5Q4IrL
-----END CERTIFICATE REQUEST-----
Loading

0 comments on commit 797ae59

Please sign in to comment.