From e489631e87f4cf92ab0ac6c6601c6998b3595f01 Mon Sep 17 00:00:00 2001 From: Steven Clark Date: Fri, 1 Nov 2024 12:57:52 -0400 Subject: [PATCH] Transit: Allow ENT only arguments for sign/verify. Add docs for new Ed25519 signature types (#28821) --- builtin/logical/transit/path_sign_verify.go | 884 +++++++++--------- .../logical/transit/path_sign_verify_ce.go | 61 ++ sdk/helper/keysutil/policy.go | 31 +- website/content/api-docs/secret/transit.mdx | 22 +- .../content/docs/secrets/transit/index.mdx | 4 +- 5 files changed, 562 insertions(+), 440 deletions(-) create mode 100644 builtin/logical/transit/path_sign_verify_ce.go diff --git a/builtin/logical/transit/path_sign_verify.go b/builtin/logical/transit/path_sign_verify.go index 2043c8724e..bcadf56f4b 100644 --- a/builtin/logical/transit/path_sign_verify.go +++ b/builtin/logical/transit/path_sign_verify.go @@ -7,7 +7,6 @@ import ( "context" "crypto/rsa" "encoding/base64" - "errors" "fmt" "strconv" "strings" @@ -20,6 +19,41 @@ import ( "github.com/mitchellh/mapstructure" ) +// policySignArgs are the arguments required to pass through to the SDK policy.SignWithOptions +type policySignArgs struct { + keyVersion int + keyContext []byte + input []byte + options keysutil.SigningOptions +} + +type policyVerifyArgs struct { + keyContext []byte + input []byte + options keysutil.SigningOptions + sig string +} + +type commonSignVerifyApiArgs struct { + keyName string + hashAlgorithm keysutil.HashType + marshaling keysutil.MarshalingType + prehashed bool + sigAlgorithm string + saltLength int +} + +// signApiArgs represents the input arguments that apply to all members of the batch +type signApiArgs struct { + commonSignVerifyApiArgs + keyVersion int +} + +// verifyApiArgs represents the input arguments that apply to all members of the batch +type verifyApiArgs struct { + commonSignVerifyApiArgs +} + // BatchRequestSignItem represents a request item for batch processing. // A map type allows us to distinguish between empty and missing values. type batchRequestSignItem map[string]string @@ -86,95 +120,7 @@ func (b *backend) pathSign() *framework.Path { OperationSuffix: "|with-algorithm", }, - Fields: map[string]*framework.FieldSchema{ - "name": { - Type: framework.TypeString, - Description: "The key to use", - }, - - "input": { - Type: framework.TypeString, - Description: "The base64-encoded input data", - }, - - "context": { - Type: framework.TypeString, - Description: `Base64 encoded context for key derivation. Required if key -derivation is enabled; currently only available with ed25519 keys.`, - }, - - "hash_algorithm": { - Type: framework.TypeString, - Default: defaultHashAlgorithm, - Description: `Hash algorithm to use (POST body parameter). Valid values are: - -* sha1 -* sha2-224 -* sha2-256 -* sha2-384 -* sha2-512 -* sha3-224 -* sha3-256 -* sha3-384 -* sha3-512 -* none - -Defaults to "sha2-256". Not valid for all key types, -including ed25519. Using none requires setting prehashed=true and -signature_algorithm=pkcs1v15, yielding a PKCSv1_5_NoOID instead of -the usual PKCSv1_5_DERnull signature.`, - }, - - "algorithm": { - Type: framework.TypeString, - Default: defaultHashAlgorithm, - Description: `Deprecated: use "hash_algorithm" instead.`, - }, - - "urlalgorithm": { - Type: framework.TypeString, - Description: `Hash algorithm to use (POST URL parameter)`, - }, - - "key_version": { - Type: framework.TypeInt, - Description: `The version of the key to use for signing. -Must be 0 (for latest) or a value greater than or equal -to the min_encryption_version configured on the key.`, - }, - - "prehashed": { - Type: framework.TypeBool, - Description: `Set to 'true' when the input is already hashed. If the key type is 'rsa-2048', 'rsa-3072' or 'rsa-4096', then the algorithm used to hash the input should be indicated by the 'algorithm' parameter.`, - }, - - "signature_algorithm": { - Type: framework.TypeString, - Description: `The signature algorithm to use for signing. Currently only applies to RSA key types. -Options are 'pss' or 'pkcs1v15'. Defaults to 'pss'`, - }, - - "marshaling_algorithm": { - Type: framework.TypeString, - Default: "asn1", - Description: `The method by which to marshal the signature. The default is 'asn1' which is used by openssl and X.509. It can also be set to 'jws' which is used for JWT signatures; setting it to this will also cause the encoding of the signature to be url-safe base64 instead of using standard base64 encoding. Currently only valid for ECDSA P-256 key types".`, - }, - - "salt_length": { - Type: framework.TypeString, - Default: "auto", - Description: `The salt length used to sign. Currently only applies to the RSA PSS signature scheme. -Options are 'auto' (the default used by Golang, causing the salt to be as large as possible when signing), 'hash' (causes the salt length to equal the length of the hash used in the signature), or an integer between the minimum and the maximum permissible salt lengths for the given RSA key size. Defaults to 'auto'.`, - }, - - "batch_input": { - Type: framework.TypeSlice, - Description: `Specifies a list of items for processing. When this parameter is set, -any supplied 'input' or 'context' parameters will be ignored. Responses are returned in the -'batch_results' array component of the 'data' element of the response. Any batch output will -preserve the order of the batch input`, - }, - }, + Fields: pathSignFieldArgs(), Callbacks: map[logical.Operation]framework.OperationFunc{ logical.UpdateOperation: b.pathSignWrite, @@ -195,52 +141,154 @@ func (b *backend) pathVerify() *framework.Path { OperationSuffix: "|with-algorithm", }, - Fields: map[string]*framework.FieldSchema{ - "name": { - Type: framework.TypeString, - Description: "The key to use", - }, + Fields: pathVerifyFieldArgs(), - "context": { - Type: framework.TypeString, - Description: `Base64 encoded context for key derivation. Required if key + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathVerifyWrite, + }, + + HelpSynopsis: pathVerifyHelpSyn, + HelpDescription: pathVerifyHelpDesc, + } +} + +func pathSignFieldArgs() map[string]*framework.FieldSchema { + fields := map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "The key to use", + }, + + "input": { + Type: framework.TypeString, + Description: "The base64-encoded input data", + }, + + "context": { + Type: framework.TypeString, + Description: `Base64 encoded context for key derivation. Required if key derivation is enabled; currently only available with ed25519 keys.`, - }, + }, - "signature": { - Type: framework.TypeString, - Description: "The signature, including vault header/key version", - }, + "hash_algorithm": { + Type: framework.TypeString, + Default: defaultHashAlgorithm, + Description: `Hash algorithm to use (POST body parameter). Valid values are: - "hmac": { - Type: framework.TypeString, - Description: "The HMAC, including vault header/key version", - }, +* sha1 +* sha2-224 +* sha2-256 +* sha2-384 +* sha2-512 +* sha3-224 +* sha3-256 +* sha3-384 +* sha3-512 +* none - "cmac": { - Type: framework.TypeString, - Description: "The CMAC, including vault header/key version", - }, +Defaults to "sha2-256". Not valid for all key types, +including ed25519. Using none requires setting prehashed=true and +signature_algorithm=pkcs1v15, yielding a PKCSv1_5_NoOID instead of +the usual PKCSv1_5_DERnull signature.`, + }, - "input": { - Type: framework.TypeString, - Description: "The base64-encoded input data to verify", - }, + "algorithm": { + Type: framework.TypeString, + Default: defaultHashAlgorithm, + Description: `Deprecated: use "hash_algorithm" instead.`, + }, - "urlalgorithm": { - Type: framework.TypeString, - Description: `Hash algorithm to use (POST URL parameter)`, - }, + "urlalgorithm": { + Type: framework.TypeString, + Description: `Hash algorithm to use (POST URL parameter)`, + }, - "mac_length": { - Type: framework.TypeInt, - Description: `MAC length to use (POST body parameter). Valid values are:`, - }, + "key_version": { + Type: framework.TypeInt, + Description: `The version of the key to use for signing. +Must be 0 (for latest) or a value greater than or equal +to the min_encryption_version configured on the key.`, + }, - "hash_algorithm": { - Type: framework.TypeString, - Default: defaultHashAlgorithm, - Description: `Hash algorithm to use (POST body parameter). Valid values are: + "prehashed": { + Type: framework.TypeBool, + Description: `Set to 'true' when the input is already hashed. If the key type is 'rsa-2048', 'rsa-3072' or 'rsa-4096', then the algorithm used to hash the input should be indicated by the 'algorithm' parameter.`, + }, + + "signature_algorithm": { + Type: framework.TypeString, + Description: `The signature algorithm to use for signing. Currently only applies to RSA key types. +Options are 'pss' or 'pkcs1v15'. Defaults to 'pss'`, + }, + + "marshaling_algorithm": { + Type: framework.TypeString, + Default: "asn1", + Description: `The method by which to marshal the signature. The default is 'asn1' which is used by openssl and X.509. It can also be set to 'jws' which is used for JWT signatures; setting it to this will also cause the encoding of the signature to be url-safe base64 instead of using standard base64 encoding. Currently only valid for ECDSA P-256 key types".`, + }, + + "salt_length": { + Type: framework.TypeString, + Default: "auto", + Description: `The salt length used to sign. Currently only applies to the RSA PSS signature scheme. +Options are 'auto' (the default used by Golang, causing the salt to be as large as possible when signing), 'hash' (causes the salt length to equal the length of the hash used in the signature), or an integer between the minimum and the maximum permissible salt lengths for the given RSA key size. Defaults to 'auto'.`, + }, + + "batch_input": { + Type: framework.TypeSlice, + Description: `Specifies a list of items for processing. When this parameter is set, +any supplied 'input' or 'context' parameters will be ignored. Responses are returned in the +'batch_results' array component of the 'data' element of the response. Any batch output will +preserve the order of the batch input`, + }, + } + + addEntSignFieldArgs(fields) + return fields +} + +func pathVerifyFieldArgs() map[string]*framework.FieldSchema { + fields := map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "The key to use", + }, + + "context": { + Type: framework.TypeString, + Description: `Base64 encoded context for key derivation. Required if key +derivation is enabled; currently only available with ed25519 keys.`, + }, + + "signature": { + Type: framework.TypeString, + Description: "The signature, including vault header/key version", + }, + + "hmac": { + Type: framework.TypeString, + Description: "The HMAC, including vault header/key version", + }, + + "input": { + Type: framework.TypeString, + Description: "The base64-encoded input data to verify", + }, + + "urlalgorithm": { + Type: framework.TypeString, + Description: `Hash algorithm to use (POST URL parameter)`, + }, + + "mac_length": { + Type: framework.TypeInt, + Description: `MAC length to use (POST body parameter). Valid values are:`, + }, + + "hash_algorithm": { + Type: framework.TypeString, + Default: defaultHashAlgorithm, + Description: `Hash algorithm to use (POST body parameter). Valid values are: * sha1 * sha2-224 @@ -255,57 +303,52 @@ derivation is enabled; currently only available with ed25519 keys.`, Defaults to "sha2-256". Not valid for all key types. See note about none on signing path.`, - }, + }, - "algorithm": { - Type: framework.TypeString, - Default: defaultHashAlgorithm, - Description: `Deprecated: use "hash_algorithm" instead.`, - }, + "algorithm": { + Type: framework.TypeString, + Default: defaultHashAlgorithm, + Description: `Deprecated: use "hash_algorithm" instead.`, + }, - "prehashed": { - Type: framework.TypeBool, - Description: `Set to 'true' when the input is already hashed. If the key type is 'rsa-2048', 'rsa-3072' or 'rsa-4096', then the algorithm used to hash the input should be indicated by the 'algorithm' parameter.`, - }, + "prehashed": { + Type: framework.TypeBool, + Description: `Set to 'true' when the input is already hashed. If the key type is 'rsa-2048', 'rsa-3072' or 'rsa-4096', then the algorithm used to hash the input should be indicated by the 'algorithm' parameter.`, + }, - "signature_algorithm": { - Type: framework.TypeString, - Description: `The signature algorithm to use for signature verification. Currently only applies to RSA key types. + "signature_algorithm": { + Type: framework.TypeString, + Description: `The signature algorithm to use for signature verification. Currently only applies to RSA key types. Options are 'pss' or 'pkcs1v15'. Defaults to 'pss'`, - }, + }, - "marshaling_algorithm": { - Type: framework.TypeString, - Default: "asn1", - Description: `The method by which to unmarshal the signature when verifying. The default is 'asn1' which is used by openssl and X.509; can also be set to 'jws' which is used for JWT signatures in which case the signature is also expected to be url-safe base64 encoding instead of standard base64 encoding. Currently only valid for ECDSA P-256 key types".`, - }, + "marshaling_algorithm": { + Type: framework.TypeString, + Default: "asn1", + Description: `The method by which to unmarshal the signature when verifying. The default is 'asn1' which is used by openssl and X.509; can also be set to 'jws' which is used for JWT signatures in which case the signature is also expected to be url-safe base64 encoding instead of standard base64 encoding. Currently only valid for ECDSA P-256 key types".`, + }, - "salt_length": { - Type: framework.TypeString, - Default: "auto", - Description: `The salt length used to sign. Currently only applies to the RSA PSS signature scheme. + "salt_length": { + Type: framework.TypeString, + Default: "auto", + Description: `The salt length used to sign. Currently only applies to the RSA PSS signature scheme. Options are 'auto' (the default used by Golang, causing the salt to be as large as possible when signing), 'hash' (causes the salt length to equal the length of the hash used in the signature), or an integer between the minimum and the maximum permissible salt lengths for the given RSA key size. Defaults to 'auto'.`, - }, + }, - "batch_input": { - Type: framework.TypeSlice, - Description: `Specifies a list of items for processing. When this parameter is set, + "batch_input": { + Type: framework.TypeSlice, + Description: `Specifies a list of items for processing. When this parameter is set, any supplied 'input', 'hmac', 'cmac' or 'signature' parameters will be ignored. Responses are returned in the 'batch_results' array component of the 'data' element of the response. Any batch output will preserve the order of the batch input`, - }, }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: b.pathVerifyWrite, - }, - - HelpSynopsis: pathVerifyHelpSyn, - HelpDescription: pathVerifyHelpDesc, } + + addEntVerifyFieldArgs(fields) + return fields } -func (b *backend) getSaltLength(d *framework.FieldData) (int, error) { +func getSaltLength(d *framework.FieldData) (int, error) { rawSaltLength, ok := d.GetOk("salt_length") // This should only happen when something is wrong with the schema, // so this is a reasonable default. @@ -333,33 +376,10 @@ func (b *backend) getSaltLength(d *framework.FieldData) (int, error) { } func (b *backend) pathSignWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - name := d.Get("name").(string) - ver := d.Get("key_version").(int) - hashAlgorithmStr := d.Get("urlalgorithm").(string) - if hashAlgorithmStr == "" { - hashAlgorithmStr = d.Get("hash_algorithm").(string) - if hashAlgorithmStr == "" { - hashAlgorithmStr = d.Get("algorithm").(string) - if hashAlgorithmStr == "" { - hashAlgorithmStr = defaultHashAlgorithm - } - } - } - - hashAlgorithm, ok := keysutil.HashTypeMap[hashAlgorithmStr] - if !ok { - return logical.ErrorResponse(fmt.Sprintf("invalid hash algorithm %q", hashAlgorithmStr)), logical.ErrInvalidRequest - } - - marshalingStr := d.Get("marshaling_algorithm").(string) - marshaling, ok := keysutil.MarshalingTypeMap[marshalingStr] - if !ok { - return logical.ErrorResponse(fmt.Sprintf("invalid marshaling type %q", marshalingStr)), logical.ErrInvalidRequest - } - - prehashed := d.Get("prehashed").(bool) - sigAlgorithm := d.Get("signature_algorithm").(string) - saltLength, err := b.getSaltLength(d) + // Fetch the top-level arguments for the sign api. These do not include + // the values that are within the batch input parameters, only those that + // apply globally. + apiArgs, err := getSignApiArgs(d) if err != nil { return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest } @@ -367,7 +387,7 @@ func (b *backend) pathSignWrite(ctx context.Context, req *logical.Request, d *fr // Get the policy p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, - Name: name, + Name: apiArgs.keyName, }, b.GetRandomReader()) if err != nil { return nil, err @@ -380,15 +400,8 @@ func (b *backend) pathSignWrite(ctx context.Context, req *logical.Request, d *fr } defer p.Unlock() - if !p.Type.SigningSupported() { - return logical.ErrorResponse(fmt.Sprintf("key type %v does not support signing", p.Type)), logical.ErrInvalidRequest - } - - // Allow managed keys to specify no hash algo without additional conditions. - if hashAlgorithm == keysutil.HashTypeNone && p.Type != keysutil.KeyType_MANAGED_KEY { - if !prehashed || sigAlgorithm != "pkcs1v15" { - return logical.ErrorResponse("hash_algorithm=none requires both prehashed=true and signature_algorithm=pkcs1v15"), logical.ErrInvalidRequest - } + if err := validateCommonSignVerifyApiArgs(p, apiArgs.commonSignVerifyApiArgs); err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest } batchInputRaw := d.Raw["batch_input"] @@ -409,65 +422,23 @@ func (b *backend) pathSignWrite(ctx context.Context, req *logical.Request, d *fr "input": d.Get("input").(string), "context": d.Get("context").(string), } + + // Only defined within the ENT schema, so this is useless on CE. + if sigContext, ok := d.GetOk("signature_context"); ok { + batchInputItems[0]["signature_context"] = sigContext.(string) + } } response := make([]batchResponseSignItem, len(batchInputItems)) for i, item := range batchInputItems { - - rawInput, ok := item["input"] - if !ok { - response[i].Error = "missing input" - response[i].err = logical.ErrInvalidRequest - continue - } - - input, err := base64.StdEncoding.DecodeString(rawInput) + psa, err := b.getPolicySignArgs(ctx, p, apiArgs, item) if err != nil { - response[i].Error = fmt.Sprintf("unable to decode input as base64: %s", err) + response[i].Error = err.Error() response[i].err = logical.ErrInvalidRequest continue } - if p.Type.HashSignatureInput() && !prehashed { - hf := keysutil.HashFuncMap[hashAlgorithm]() - if hf != nil { - hf.Write(input) - input = hf.Sum(nil) - } - } - - contextRaw := item["context"] - var context []byte - if len(contextRaw) != 0 { - context, err = base64.StdEncoding.DecodeString(contextRaw) - if err != nil { - response[i].Error = "failed to base64-decode context" - response[i].err = logical.ErrInvalidRequest - continue - } - } - - var managedKeyParameters keysutil.ManagedKeyParameters - if p.Type == keysutil.KeyType_MANAGED_KEY { - managedKeySystemView, ok := b.System().(logical.ManagedKeySystemView) - if !ok { - return nil, errors.New("unsupported system view") - } - - managedKeyParameters = keysutil.ManagedKeyParameters{ - ManagedKeySystemView: managedKeySystemView, - BackendUUID: b.backendUUID, - Context: ctx, - } - } - - sig, err := p.SignWithOptions(ver, context, input, &keysutil.SigningOptions{ - HashAlgorithm: hashAlgorithm, - Marshaling: marshaling, - SaltLength: saltLength, - SigAlgorithm: sigAlgorithm, - ManagedKeyParams: managedKeyParameters, - }) + sig, err := p.SignWithOptions(psa.keyVersion, psa.keyContext, psa.input, &psa.options) if err != nil { if batchInputRaw != nil { response[i].Error = err.Error() @@ -476,7 +447,7 @@ func (b *backend) pathSignWrite(ctx context.Context, req *logical.Request, d *fr } else if sig == nil { response[i].err = fmt.Errorf("signature could not be computed") } else { - keyVersion := ver + keyVersion := apiArgs.keyVersion if keyVersion == 0 { keyVersion = p.LatestVersion } @@ -519,36 +490,129 @@ func (b *backend) pathSignWrite(ctx context.Context, req *logical.Request, d *fr return resp, nil } +func (b *backend) getPolicySignArgs(ctx context.Context, p *keysutil.Policy, args signApiArgs, item batchRequestSignItem) (policySignArgs, error) { + rawInput, ok := item["input"] + if !ok { + return policySignArgs{}, fmt.Errorf("missing input") + } + + input, err := base64.StdEncoding.DecodeString(rawInput) + if err != nil { + return policySignArgs{}, fmt.Errorf("unable to decode input: %s", err) + } + + if p.Type.HashSignatureInput() && !args.prehashed { + hf := keysutil.HashFuncMap[args.hashAlgorithm]() + if hf != nil { + hf.Write(input) + input = hf.Sum(nil) + } + } + + keyContext, err := decodeBase64Arg(item["context"]) + if err != nil { + return policySignArgs{}, fmt.Errorf("failed to base64-decode context") + } + + psa := policySignArgs{ + keyVersion: args.keyVersion, + keyContext: keyContext, + input: input, + options: keysutil.SigningOptions{ + HashAlgorithm: args.hashAlgorithm, + Marshaling: args.marshaling, + SaltLength: args.saltLength, + SigAlgorithm: args.sigAlgorithm, + }, + } + + if err := b.populateEntPolicySigningOptions(ctx, p, args, item, &psa); err != nil { + return policySignArgs{}, nil + } + return psa, nil +} + +func validateCommonSignVerifyApiArgs(p *keysutil.Policy, apiArgs commonSignVerifyApiArgs) error { + if !p.Type.SigningSupported() { + return fmt.Errorf("key type %v does not support signing", p.Type) + } + + // Perform Vault version specific checks (CE vs ENT) + return validateSignApiArgsVersionSpecific(p, apiArgs) +} + +func getSignApiArgs(d *framework.FieldData) (signApiArgs, error) { + keyVersion := d.Get("key_version").(int) + commonArgs, err := getCommonSignVerifyApiArgs(d) + if err != nil { + return signApiArgs{}, err + } + + return signApiArgs{ + commonSignVerifyApiArgs: commonArgs, + keyVersion: keyVersion, + }, nil +} + +func getCommonSignVerifyApiArgs(d *framework.FieldData) (commonSignVerifyApiArgs, error) { + keyName := d.Get("name").(string) + hashAlgorithmStr, hashAlgorithm, ok := getHashAlgorithmFromArgs(d) + if !ok { + return commonSignVerifyApiArgs{}, fmt.Errorf("invalid hash algorithm %q", hashAlgorithmStr) + } + + marshalingStr := d.Get("marshaling_algorithm").(string) + marshaling, ok := keysutil.MarshalingTypeMap[marshalingStr] + if !ok { + return commonSignVerifyApiArgs{}, fmt.Errorf("invalid marshaling type %q", marshalingStr) + } + + prehashed := d.Get("prehashed").(bool) + sigAlgorithm := d.Get("signature_algorithm").(string) + saltLength, err := getSaltLength(d) + if err != nil { + return commonSignVerifyApiArgs{}, err + } + + return commonSignVerifyApiArgs{ + keyName: keyName, + hashAlgorithm: hashAlgorithm, + marshaling: marshaling, + prehashed: prehashed, + sigAlgorithm: sigAlgorithm, + saltLength: saltLength, + }, nil +} + +func getHashAlgorithmFromArgs(d *framework.FieldData) (string, keysutil.HashType, bool) { + hashAlgorithmStr := d.Get("urlalgorithm").(string) + if hashAlgorithmStr == "" { + hashAlgorithmStr = d.Get("hash_algorithm").(string) + if hashAlgorithmStr == "" { + hashAlgorithmStr = d.Get("algorithm").(string) + if hashAlgorithmStr == "" { + hashAlgorithmStr = defaultHashAlgorithm + } + } + } + + hashAlgorithm, ok := keysutil.HashTypeMap[hashAlgorithmStr] + return hashAlgorithmStr, hashAlgorithm, ok +} + +func decodeBase64Arg(fieldVal interface{}) ([]byte, error) { + parsedStr, err := parseutil.ParseString(fieldVal) + if err != nil { + return nil, err + } + + return base64.StdEncoding.DecodeString(parsedStr) +} + func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - batchInputRaw := d.Raw["batch_input"] - var batchInputItems []batchRequestVerifyItem - if batchInputRaw != nil { - err := mapstructure.Decode(batchInputRaw, &batchInputItems) - if err != nil { - return nil, fmt.Errorf("failed to parse batch input: %w", err) - } - - if len(batchInputItems) == 0 { - return logical.ErrorResponse("missing batch input to process"), logical.ErrInvalidRequest - } - } else { - // use empty string if input is missing - not an error - inputB64 := d.Get("input").(string) - - batchInputItems = make([]batchRequestVerifyItem, 1) - batchInputItems[0] = batchRequestVerifyItem{ - "input": inputB64, - } - if sig, ok := d.GetOk("signature"); ok { - batchInputItems[0]["signature"] = sig.(string) - } - if hmac, ok := d.GetOk("hmac"); ok { - batchInputItems[0]["hmac"] = hmac.(string) - } - if cmac, ok := d.GetOk("cmac"); ok { - batchInputItems[0]["cmac"] = cmac.(string) - } - batchInputItems[0]["context"] = d.Get("context").(string) + batchInputItems, isBatchInput, err := getVerifyBatchItems(d) + if err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest } // For simplicity, 'signature' and 'hmac' cannot be mixed across batch_input elements. @@ -572,11 +636,11 @@ func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d * optionsSet := numBooleansTrue(sigFound, hmacFound, cmacFound) switch { - case batchInputRaw == nil && optionsSet > 1: + case !isBatchInput && optionsSet > 1: return logical.ErrorResponse("provide one of 'signature', 'hmac' or 'cmac'"), logical.ErrInvalidRequest - case batchInputRaw == nil && optionsSet == 0: - return logical.ErrorResponse("missing 'signature', 'hmac' or 'cmac' were given to verify"), logical.ErrInvalidRequest + case !isBatchInput && optionsSet == 0: + return logical.ErrorResponse("missing one of 'signature', 'hmac' or 'cmac' arguments to verify"), logical.ErrInvalidRequest case optionsSet > 1: return logical.ErrorResponse("elements of batch_input must all provide either 'signature', 'hmac' or 'cmac'"), logical.ErrInvalidRequest @@ -600,32 +664,7 @@ func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d * return b.pathCMACVerify(ctx, req, d) } - name := d.Get("name").(string) - hashAlgorithmStr := d.Get("urlalgorithm").(string) - if hashAlgorithmStr == "" { - hashAlgorithmStr = d.Get("hash_algorithm").(string) - if hashAlgorithmStr == "" { - hashAlgorithmStr = d.Get("algorithm").(string) - if hashAlgorithmStr == "" { - hashAlgorithmStr = defaultHashAlgorithm - } - } - } - - hashAlgorithm, ok := keysutil.HashTypeMap[hashAlgorithmStr] - if !ok { - return logical.ErrorResponse(fmt.Sprintf("invalid hash algorithm %q", hashAlgorithmStr)), logical.ErrInvalidRequest - } - - marshalingStr := d.Get("marshaling_algorithm").(string) - marshaling, ok := keysutil.MarshalingTypeMap[marshalingStr] - if !ok { - return logical.ErrorResponse(fmt.Sprintf("invalid marshaling type %q", marshalingStr)), logical.ErrInvalidRequest - } - - prehashed := d.Get("prehashed").(bool) - sigAlgorithm := d.Get("signature_algorithm").(string) - saltLength, err := b.getSaltLength(d) + apiArgs, err := getVerifyApiArgs(d) if err != nil { return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest } @@ -633,7 +672,7 @@ func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d * // Get the policy p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{ Storage: req.Storage, - Name: name, + Name: apiArgs.keyName, }, b.GetRandomReader()) if err != nil { return nil, err @@ -646,106 +685,28 @@ func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d * } defer p.Unlock() - if !p.Type.SigningSupported() { - return logical.ErrorResponse(fmt.Sprintf("key type %v does not support verification", p.Type)), logical.ErrInvalidRequest - } - - // Allow managed keys to specify no hash algo without additional conditions. - if hashAlgorithm == keysutil.HashTypeNone && p.Type != keysutil.KeyType_MANAGED_KEY { - if !prehashed || sigAlgorithm != "pkcs1v15" { - return logical.ErrorResponse("hash_algorithm=none requires both prehashed=true and signature_algorithm=pkcs1v15"), logical.ErrInvalidRequest - } + if err := validateCommonSignVerifyApiArgs(p, apiArgs.commonSignVerifyApiArgs); err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest } response := make([]batchResponseVerifyItem, len(batchInputItems)) for i, item := range batchInputItems { - rawInput, ok := item["input"] - if !ok { - response[i].Error = "missing input" - response[i].err = logical.ErrInvalidRequest - continue - } - strInput, err := parseutil.ParseString(rawInput) + pva, err := b.getPolicyVerifyArgs(ctx, p, apiArgs, item) if err != nil { - response[i].Error = fmt.Sprintf("unable to parse input as string: %s", err) + response[i].Error = fmt.Sprintf("failed to parse item: %s", err) response[i].err = logical.ErrInvalidRequest continue } - input, err := base64.StdEncoding.DecodeString(strInput) - if err != nil { - response[i].Error = fmt.Sprintf("unable to decode input as base64: %s", err) - response[i].err = logical.ErrInvalidRequest - continue - } - - sigRaw, ok := item["signature"].(string) - if !ok { - response[i].Error = "missing signature" - response[i].err = logical.ErrInvalidRequest - continue - } - sig, err := parseutil.ParseString(sigRaw) - if err != nil { - response[i].Error = fmt.Sprintf("failed to parse signature as a string: %s", err) - response[i].err = logical.ErrInvalidRequest - continue - } - - if p.Type.HashSignatureInput() && !prehashed { - hf := keysutil.HashFuncMap[hashAlgorithm]() - if hf != nil { - hf.Write(input) - input = hf.Sum(nil) - } - } - - contextRaw, err := parseutil.ParseString(item["context"]) - if err != nil { - response[i].Error = fmt.Sprintf("failed to parse context as a string: %s", err) - response[i].err = logical.ErrInvalidRequest - continue - } - var context []byte - if len(contextRaw) != 0 { - context, err = base64.StdEncoding.DecodeString(contextRaw) - if err != nil { - response[i].Error = "failed to base64-decode context" - response[i].err = logical.ErrInvalidRequest - continue - } - } - var managedKeyParameters keysutil.ManagedKeyParameters - if p.Type == keysutil.KeyType_MANAGED_KEY { - managedKeySystemView, ok := b.System().(logical.ManagedKeySystemView) - if !ok { - return nil, errors.New("unsupported system view") - } - - managedKeyParameters = keysutil.ManagedKeyParameters{ - ManagedKeySystemView: managedKeySystemView, - BackendUUID: b.backendUUID, - Context: ctx, - } - } - - signingOptions := &keysutil.SigningOptions{ - HashAlgorithm: hashAlgorithm, - Marshaling: marshaling, - SaltLength: saltLength, - SigAlgorithm: sigAlgorithm, - ManagedKeyParams: managedKeyParameters, - } - - valid, err := p.VerifySignatureWithOptions(context, input, sig, signingOptions) + valid, err := p.VerifySignatureWithOptions(pva.keyContext, pva.input, pva.sig, &pva.options) if err != nil { switch err.(type) { case errutil.UserError: response[i].Error = err.Error() response[i].err = logical.ErrInvalidRequest default: - if batchInputRaw != nil { + if isBatchInput { response[i].Error = err.Error() } response[i].err = err @@ -757,7 +718,7 @@ func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d * // Generate the response resp := &logical.Response{} - if batchInputRaw != nil { + if isBatchInput { // Copy the references for i := range batchInputItems { if ref, err := parseutil.ParseString(batchInputItems[i]["reference"]); err == nil { @@ -782,6 +743,102 @@ func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d * return resp, nil } +func getVerifyApiArgs(d *framework.FieldData) (verifyApiArgs, error) { + commonArgs, err := getCommonSignVerifyApiArgs(d) + if err != nil { + return verifyApiArgs{}, err + } + + return verifyApiArgs{ + commonSignVerifyApiArgs: commonArgs, + }, nil +} + +func getVerifyBatchItems(d *framework.FieldData) ([]batchRequestVerifyItem, bool, error) { + if batchInputRaw, ok := d.Raw["batch_input"]; ok && batchInputRaw != nil { + var batchInputItems []batchRequestVerifyItem + err := mapstructure.Decode(batchInputRaw, &batchInputItems) + if err != nil { + return nil, false, fmt.Errorf("failed to parse batch input: %w", err) + } + + if len(batchInputItems) == 0 { + return nil, false, fmt.Errorf("missing batch input to process") + } + + return batchInputItems, true, nil + } + + // use empty string if input is missing - not an error + item := batchRequestVerifyItem{ + "input": d.Get("input").(string), + } + if sig, ok := d.GetOk("signature"); ok { + item["signature"] = sig.(string) + } + if hmac, ok := d.GetOk("hmac"); ok { + item["hmac"] = hmac.(string) + } + if cmac, ok := d.GetOk("cmac"); ok { + item["cmac"] = cmac.(string) + } + + item["context"] = d.Get("context").(string) + + if sigContext, ok := d.GetOk("signature_context"); ok { + item["signature_context"] = sigContext.(string) + } + + return []batchRequestVerifyItem{item}, false, nil +} + +func (b *backend) getPolicyVerifyArgs(ctx context.Context, p *keysutil.Policy, apiArgs verifyApiArgs, item batchRequestVerifyItem) (policyVerifyArgs, error) { + input, err := decodeBase64Arg(item["input"]) + if err != nil { + return policyVerifyArgs{}, fmt.Errorf("failed to parse input: %s", err) + } + + sigRaw, ok := item["signature"] + if !ok { + return policyVerifyArgs{}, fmt.Errorf("missing signature") + } + sig, err := parseutil.ParseString(sigRaw) + if err != nil { + return policyVerifyArgs{}, fmt.Errorf("failed to parse signature as a string: %s", err) + } + + if p.Type.HashSignatureInput() && !apiArgs.prehashed { + hf := keysutil.HashFuncMap[apiArgs.hashAlgorithm]() + if hf != nil { + hf.Write(input) + input = hf.Sum(nil) + } + } + + keyContext, err := decodeBase64Arg(item["context"]) + if err != nil { + return policyVerifyArgs{}, fmt.Errorf("failed to parse context: %s", err) + } + + vsa := policyVerifyArgs{ + keyContext: keyContext, + input: input, + sig: sig, + options: keysutil.SigningOptions{ + HashAlgorithm: apiArgs.hashAlgorithm, + Marshaling: apiArgs.marshaling, + SaltLength: apiArgs.saltLength, + SigAlgorithm: apiArgs.sigAlgorithm, + }, + } + + if err := b.populateEntPolicyVerifyOptions(ctx, p, apiArgs, item, &vsa); err != nil { + return policyVerifyArgs{}, fmt.Errorf("failed to parse batch item: %s", err) + } + + return vsa, nil +} + func numBooleansTrue(bools ...bool) int { numSet := 0 for _, value := range bools { @@ -792,43 +849,6 @@ func numBooleansTrue(bools ...bool) int { return numSet } -func decodeTransitSignature(sig string) ([]byte, int, error) { - if !strings.HasPrefix(sig, "vault:v") { - return nil, 0, fmt.Errorf("prefix is not vault:v") - } - - splitVerification := strings.SplitN(strings.TrimPrefix(sig, "vault:v"), ":", 2) - if len(splitVerification) != 2 { - return nil, 0, fmt.Errorf("wrong number of fields delimited by ':', got %d expected 2", len(splitVerification)) - } - - ver, err := strconv.Atoi(splitVerification[0]) - if err != nil { - return nil, 0, fmt.Errorf("key version number %s count not be decoded", splitVerification[0]) - } - - if ver < 1 { - return nil, 0, fmt.Errorf("key version less than 1 are invalid got: %d", ver) - } - - if len(strings.TrimSpace(splitVerification[1])) == 0 { - return nil, 0, fmt.Errorf("missing base64 verification string from vault signature") - } - - verBytes, err := base64.StdEncoding.DecodeString(splitVerification[1]) - if err != nil { - return nil, 0, fmt.Errorf("unable to decode verification string as base64: %s", err) - } - - return verBytes, ver, nil -} - -func encodeTransitSignature(value []byte, keyVersion int) string { - retStr := base64.StdEncoding.EncodeToString(value) - retStr = fmt.Sprintf("vault:v%d:%s", keyVersion, retStr) - return retStr -} - const pathSignHelpSyn = `Generate a signature for input data using the named key` const pathSignHelpDesc = ` diff --git a/builtin/logical/transit/path_sign_verify_ce.go b/builtin/logical/transit/path_sign_verify_ce.go new file mode 100644 index 0000000000..41d23aaa05 --- /dev/null +++ b/builtin/logical/transit/path_sign_verify_ce.go @@ -0,0 +1,61 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package transit + +import ( + "context" + "fmt" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/keysutil" +) + +// addEntSignFieldArgs adds the enterprise only fields to the field schema definition +func addEntSignFieldArgs(_ map[string]*framework.FieldSchema) { + // Do nothing +} + +// addEntVerifyFieldArgs adds the enterprise only fields to the field schema definition +func addEntVerifyFieldArgs(_ map[string]*framework.FieldSchema) { + // Do nothing +} + +// validateSignApiArgsVersionSpecific will perform a validation of the Sign API parameters +// from the Enterprise or CE point of view. +func validateSignApiArgsVersionSpecific(p *keysutil.Policy, apiArgs commonSignVerifyApiArgs) error { + if err := _validateEntSpecificKeyType(p); err != nil { + return err + } + + if apiArgs.hashAlgorithm == keysutil.HashTypeNone { + if !apiArgs.prehashed || apiArgs.sigAlgorithm != "pkcs1v15" { + return fmt.Errorf("hash_algorithm=none requires both prehashed=true and signature_algorithm=pkcs1v15") + } + } + + return nil +} + +// populateEntPolicySigning augments or tweaks the input parameters to the SDK policy.SignWithOptions for +// Enterprise usage. +func (b *backend) populateEntPolicySigningOptions(_ context.Context, _ *keysutil.Policy, _ signApiArgs, _ batchRequestSignItem, _ *policySignArgs) error { + return nil +} + +// populateEntPolicyVerifyOptions augments or tweaks the input parameters to the SDK policy.VerifyWithOptions for +// Enterprise usage. +func (b *backend) populateEntPolicyVerifyOptions(ctx context.Context, p *keysutil.Policy, args verifyApiArgs, item batchRequestVerifyItem, vsa *policyVerifyArgs) error { + return nil +} + +func _validateEntSpecificKeyType(p *keysutil.Policy) error { + switch p.Type { + case keysutil.KeyType_AES128_CMAC, keysutil.KeyType_AES256_CMAC, keysutil.KeyType_MANAGED_KEY: + return fmt.Errorf("enterprise specific key type %q can not be used on CE", p.Type) + default: + return nil + } +} diff --git a/sdk/helper/keysutil/policy.go b/sdk/helper/keysutil/policy.go index 234b4413cf..7d3d738523 100644 --- a/sdk/helper/keysutil/policy.go +++ b/sdk/helper/keysutil/policy.go @@ -10,6 +10,7 @@ import ( "crypto/aes" "crypto/cipher" "crypto/ecdsa" + stdlibEd25519 "crypto/ed25519" "crypto/elliptic" "crypto/hmac" "crypto/rand" @@ -133,6 +134,7 @@ type SigningOptions struct { Marshaling MarshalingType SaltLength int SigAlgorithm string + SigContext string // Provide a context for Ed25519ctx signatures ManagedKeyParams ManagedKeyParameters } @@ -1300,9 +1302,12 @@ func (p *Policy) SignWithOptions(ver int, context, input []byte, options *Signin key = ed25519.PrivateKey(keyParams.Key) } + opts := genEd25519Options(hashAlgorithm, options.SigContext) + // Per docs, do not pre-hash ed25519; it does two passes and performs - // its own hashing - sig, err = key.Sign(rand.Reader, input, crypto.Hash(0)) + // its own hashing when we specify crypto.Hash(0). Ed25519Ph assumes + // pre-hashed with SHA512 + sig, err = key.Sign(rand.Reader, input, opts) if err != nil { return nil, err } @@ -1498,7 +1503,13 @@ func (p *Policy) VerifySignatureWithOptions(context, input []byte, sig string, o pub = ed25519.PublicKey(raw) } - return ed25519.Verify(pub, input, sigBytes), nil + opts := genEd25519Options(hashAlgorithm, options.SigContext) + if err := stdlibEd25519.VerifyWithOptions(pub, input, sigBytes, opts); err != nil { + // We drop the error, just report back that we failed signature verification + return false, nil + } + + return true, nil case KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096: keyEntry, err := p.safeGetKeyEntry(ver) @@ -1550,6 +1561,20 @@ func (p *Policy) VerifySignatureWithOptions(context, input []byte, sig string, o } } +func genEd25519Options(hashAlgorithm HashType, sigContext string) *stdlibEd25519.Options { + opts := &stdlibEd25519.Options{ + Hash: crypto.Hash(0), + } + if hashAlgorithm == HashTypeSHA2512 { + // activate ph mode, we assume input is prehashed + opts.Hash = crypto.SHA512 + } + if len(sigContext) > 0 { + opts.Context = sigContext + } + return opts +} + func (p *Policy) Import(ctx context.Context, storage logical.Storage, key []byte, randReader io.Reader) error { return p.ImportPublicOrPrivate(ctx, storage, key, true, randReader) } diff --git a/website/content/api-docs/secret/transit.mdx b/website/content/api-docs/secret/transit.mdx index 248283cd61..6cbc2a6148 100644 --- a/website/content/api-docs/secret/transit.mdx +++ b/website/content/api-docs/secret/transit.mdx @@ -1516,6 +1516,9 @@ supports signing. Required if key derivation is enabled; currently only available with ed25519 keys. +- `signature_context` `(string: "")` - Base64 + encoded context for Ed25519ctx and Ed25519ph signatures. + - `prehashed` `(bool: false)` - Set to `true` when the input is already hashed. If the key type is `rsa-2048`, `rsa-3072` or `rsa-4096`, then the algorithm used to hash the input should be indicated by the `hash_algorithm` parameter. Just as the @@ -1523,6 +1526,9 @@ supports signing. data you want signed, when set, `input` is expected to be base64-encoded binary hashed data, not hex-formatted. (As an example, on the command line, you could generate a suitable input via `openssl dgst -sha256 -binary | base64`.) + On Enterprise , enabling this will activate + Ed25519ph signatures for Ed25519 keys along with hash_algorithm being either `none` + or `sha2-512`. - `signature_algorithm` `(string: "pss")` – When using a RSA key, specifies the RSA signature algorithm to use for signing. Supported signature types are: @@ -1714,9 +1720,19 @@ or [generate CMAC](#generate-cmac) API calls. Required if key derivation is enabled; currently only available with ed25519 keys. -- `prehashed` `(bool: false)` - Set to `true` when the input is already - hashed. If the key type is `rsa-2048`, `rsa-3072` or `rsa-4096`, then the algorithm used - to hash the input should be indicated by the `hash_algorithm` parameter. +- `signature_context` `(string: "")` - Base64 + encoded context for Ed25519ctx and Ed25519ph signatures. + +- `prehashed` `(bool: false)` - Set to `true` when the input is already hashed. + If the key type is `rsa-2048`, `rsa-3072` or `rsa-4096`, then the algorithm used to hash + the input should be indicated by the `hash_algorithm` parameter. Just as the + value to sign should be the base64-encoded representation of the exact binary + data you want signed, when set, `input` is expected to be base64-encoded + binary hashed data, not hex-formatted. (As an example, on the command line, + you could generate a suitable input via `openssl dgst -sha256 -binary | base64`.) + On Enterprise , enabling this will activate + Ed25519ph signatures for Ed25519 keys along with hash_algorithm being either `none` + or `sha2-512`. - `signature_algorithm` `(string: "pss")` – When using a RSA key, specifies the RSA signature algorithm to use for signature verification. Supported signature types diff --git a/website/content/docs/secrets/transit/index.mdx b/website/content/docs/secrets/transit/index.mdx index 14266cfafc..dd97f12032 100644 --- a/website/content/docs/secrets/transit/index.mdx +++ b/website/content/docs/secrets/transit/index.mdx @@ -88,7 +88,7 @@ types also generate separate HMAC keys): - `hmac`: HMAC; supporting HMAC generation and verification. - `managed_key`: Managed key; supports a variety of operations depending on the backing key management solution. See [Managed Keys](/vault/docs/enterprise/managed-keys) - for more information. + for more information. - `aes128-cmac`: CMAC with a 128-bit AES key; supporting CMAC generation and verification. - `aes256-cmac`: CMAC with a 256-bit AES key; supporting CMAC generation and verification. @@ -262,7 +262,7 @@ have Transit generate and manage a key within Vault. ### Via the Command Line The Vault command line tool [includes a helper](/vault/docs/commands/transit/) to perform the steps described -in Manual below. +in Manual below. ### Via the API