Repository: smallstep/cli
Pull request #1092 (2023-12-29 01:16:23)

Add handling of `cnf` claim
 Allow to add confirmation claims to tokens

This commit allows passing confirmation claims to tokens to tie the
tokens with a provided CSR or SSH public key.

Fixes smallstep/certificates#1637

Related PR:
- [ ] https://github.com/smallstep/certificates/pull/1660



Patch:
diff --git a/command/ca/sign.go b/command/ca/sign.go
index b108f536..a99b71de 100644
--- a/command/ca/sign.go
+++ b/command/ca/sign.go
@@ -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
 	}
diff --git a/command/ca/token.go b/command/ca/token.go
index 84d6ffb6..1df9aec1 100644
--- a/command/ca/token.go
+++ b/command/ca/token.go
@@ -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"
@@ -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 {
@@ -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
@@ -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:
 '''
@@ -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)
@@ -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
@@ -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:
@@ -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.
@@ -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")
@@ -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
 		}
diff --git a/command/certificate/fingerprint.go b/command/certificate/fingerprint.go
index f8cb63f5..491777de 100644
--- a/command/certificate/fingerprint.go
+++ b/command/certificate/fingerprint.go
@@ -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
@@ -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{
@@ -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},
+			}
 		}
 	}
 
diff --git a/command/ssh/certificate.go b/command/ssh/certificate.go
index 27563a18..973779e7 100644
--- a/command/ssh/certificate.go
+++ b/command/ssh/certificate.go
@@ -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
 	}
@@ -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{}
diff --git a/flags/flags.go b/flags/flags.go
index bb591ba3..a344d0a8 100644
--- a/flags/flags.go
+++ b/flags/flags.go
@@ -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",
diff --git a/token/options.go b/token/options.go
index 3643dffd..6bdd0ff4 100644
--- a/token/options.go
+++ b/token/options.go
@@ -2,6 +2,7 @@ package token
 
 import (
 	"bytes"
+	"crypto"
 	"crypto/ecdh"
 	"crypto/ecdsa"
 	"crypto/ed25519"
@@ -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"
@@ -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 {
diff --git a/token/token.go b/token/token.go
index b8b225d9..770f939b 100644
--- a/token/token.go
+++ b/token/token.go
@@ -32,6 +32,10 @@ const SANSClaim = "sans"
 // StepClaim is the property name for a JWT claim the stores the custom information in the certificate.
 const StepClaim = "step"
 
+// ConfirmationClaim is the property name for a JWT claim that stores a JSON
+// object used as Proof-Of-Possession.
+const ConfirmationClaim = "cnf"
+
 // Token interface which all token types should attempt to implement.
 type Token interface {
 	SignedString(sigAlg string, priv interface{}) (string, error)
diff --git a/utils/cautils/certificate_flow.go b/utils/cautils/certificate_flow.go
index f3973bf0..9b4f5000 100644
--- a/utils/cautils/certificate_flow.go
+++ b/utils/cautils/certificate_flow.go
@@ -27,6 +27,7 @@ import (
 	"go.step.sm/crypto/keyutil"
 	"go.step.sm/crypto/pemutil"
 	"go.step.sm/crypto/x509util"
+	"golang.org/x/crypto/ssh"
 )
 
 // CertificateFlow manages the flow to retrieve a new certificate.
@@ -35,16 +36,66 @@ type CertificateFlow struct {
 	offline   bool
 }
 
+type flowContext struct {
+	DisableCustomSANs       bool
+	SSHPublicKey            ssh.PublicKey
+	CertificateRequest      *x509.CertificateRequest
+	ConfirmationFingerprint string
+}
+
 // sharedContext is used to share information between commands.
-var sharedContext = struct {
-	DisableCustomSANs bool
-}{}
+var sharedContext flowContext
+
+type funcFlowOption struct {
+	f func(fo *flowContext)
+}
+
+func (ffo *funcFlowOption) apply(fo *flowContext) {
+	ffo.f(fo)
+}
+
+func newFuncFlowOption(f func(fo *flowContext)) *funcFlowOption {
+	return &funcFlowOption{
+		f: f,
+	}
+}
+
+type Option interface {
+	apply(fo *flowContext)
+}
+
+// WithSSHPublicKey sets the SSH public key used in the request.
+func WithSSHPublicKey(key ssh.PublicKey) Option {
+	return newFuncFlowOption(func(fo *flowContext) {
+		fo.SSHPublicKey = key
+	})
+}
+
+// WithCertificateRequest sets the X509 certificate request used in the request.
+func WithCertificateRequest(cr *x509.CertificateRequest) Option {
+	return newFuncFlowOption(func(fo *flowContext) {
+		fo.CertificateRequest = cr
+	})
+}
+
+// WithConfirmationFingerprint sets the confirmation fingerprint used in the
+// request.
+func WithConfirmationFingerprint(fp string) Option {
+	return newFuncFlowOption(func(fo *flowContext) {
+		fo.ConfirmationFingerprint = fp
+	})
+}
 
 // NewCertificateFlow initializes a cli flow to get a new certificate.
-func NewCertificateFlow(ctx *cli.Context) (*CertificateFlow, error) {
+func NewCertificateFlow(ctx *cli.Context, opts ...Option) (*CertificateFlow, error) {
 	var err error
 	var offlineClient *OfflineCA
 
+	// Add options to the shared context
+	for _, opt := range opts {
+		opt.apply(&sharedContext)
+	}
+
 	offline := ctx.Bool("offline")
 	if offline {
 		caConfig := ctx.String("ca-config")
diff --git a/utils/cautils/token_flow.go b/utils/cautils/token_flow.go
index 8d6a1dd9..185d9153 100644
--- a/utils/cautils/token_flow.go
+++ b/utils/cautils/token_flow.go
@@ -85,7 +85,12 @@ func (e *ACMETokenError) Error() string {
 }
 
 // NewTokenFlow implements the common flow used to generate a token
-func NewTokenFlow(ctx *cli.Context, tokType int, subject string, sans []string, caURL, root string, notBefore, notAfter time.Time, certNotBefore, certNotAfter provisioner.TimeDuration) (string, error) {
+func NewTokenFlow(ctx *cli.Context, tokType int, subject string, sans []string, caURL, root string, notBefore, notAfter time.Time, certNotBefore, certNotAfter provisioner.TimeDuration, opts ...Option) (string, error) {
+	// Apply options to shared context
+	for _, opt := range opts {
+		opt.apply(&sharedContext)
+	}
+
 	// Get audience from ca-url
 	audience, err := parseAudience(ctx, tokType)
 	if err != nil {
diff --git a/utils/cautils/token_generator.go b/utils/cautils/token_generator.go
index 479d8fcc..71d1b385 100644
--- a/utils/cautils/token_generator.go
+++ b/utils/cautils/token_generator.go
@@ -98,6 +98,14 @@ func (t *TokenGenerator) SignToken(sub string, sans []string, opts ...token.Opti
 		sans = []string{sub}
 	}
 	opts = append(opts, token.WithSANS(sans))
+
+	// Tie certificate request to the token used in the JWK and X5C provisioners
+	if sharedContext.CertificateRequest != nil {
+		opts = append(opts, token.WithFingerprint(sharedContext.CertificateRequest))
+	} else if sharedContext.ConfirmationFingerprint != "" {
+		opts = append(opts, token.WithConfirmationFingerprint(sharedContext.ConfirmationFingerprint))
+	}
+
 	return t.Token(sub, opts...)
 }
 
@@ -115,6 +123,7 @@ func (t *TokenGenerator) SignSSHToken(sub, certType string, principals []string,
 		ValidAfter:  notBefore,
 		ValidBefore: notAfter,
 	})}, opts...)
+
 	return t.Token(sub, opts...)
 }
 


Test patch:
diff --git a/token/options_test.go b/token/options_test.go
index 8c3303d1..f25ab144 100644
--- a/token/options_test.go
+++ b/token/options_test.go
@@ -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) {
@@ -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")
@@ -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 {
@@ -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)
diff --git a/token/testdata/ssh-key.pub b/token/testdata/ssh-key.pub
new file mode 100644
index 00000000..17ec459b
--- /dev/null
+++ b/token/testdata/ssh-key.pub
@@ -0,0 +1,1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF14RP3HJkO1yoZHjo9t/4bJgyJGiSPxhm6FApa3VtG1 mariano@overlook.local
diff --git a/token/testdata/test.csr b/token/testdata/test.csr
new file mode 100644
index 00000000..95cf8e75
--- /dev/null
+++ b/token/testdata/test.csr
@@ -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-----