diff --git a/builtin/logical/transit/backend_test.go b/builtin/logical/transit/backend_test.go index 6c4edd7d2a5e..33f257c2f3e7 100644 --- a/builtin/logical/transit/backend_test.go +++ b/builtin/logical/transit/backend_test.go @@ -205,12 +205,9 @@ func testTransit_RSA(t *testing.T, keyType string) { "hash_algorithm": "invalid", } resp, err = b.HandleRequest(context.Background(), signReq) - if err != nil { + if err == nil { t.Fatal(err) } - if resp == nil || !resp.IsError() { - t.Fatal("expected an error response") - } signReq.Data = map[string]interface{}{ "input": plaintext, diff --git a/builtin/logical/transit/path_sign_verify.go b/builtin/logical/transit/path_sign_verify.go index 10c99474c5ff..615696ab3f0b 100644 --- a/builtin/logical/transit/path_sign_verify.go +++ b/builtin/logical/transit/path_sign_verify.go @@ -2,11 +2,8 @@ package transit import ( "context" - "crypto/sha256" - "crypto/sha512" "encoding/base64" "fmt" - "hash" "github.com/hashicorp/vault/helper/errutil" "github.com/hashicorp/vault/helper/keysutil" @@ -18,23 +15,23 @@ func (b *backend) pathSign() *framework.Path { return &framework.Path{ Pattern: "sign/" + framework.GenericNameRegex("name") + framework.OptionalParamRegex("urlalgorithm"), Fields: map[string]*framework.FieldSchema{ - "name": &framework.FieldSchema{ + "name": { Type: framework.TypeString, Description: "The key to use", }, - "input": &framework.FieldSchema{ + "input": { Type: framework.TypeString, Description: "The base64-encoded input data", }, - "context": &framework.FieldSchema{ + "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": &framework.FieldSchema{ + "hash_algorithm": { Type: framework.TypeString, Default: "sha2-256", Description: `Hash algorithm to use (POST body parameter). Valid values are: @@ -48,33 +45,40 @@ Defaults to "sha2-256". Not valid for all key types, including ed25519.`, }, - "algorithm": &framework.FieldSchema{ + "algorithm": { Type: framework.TypeString, Default: "sha2-256", Description: `Deprecated: use "hash_algorithm" instead.`, }, - "urlalgorithm": &framework.FieldSchema{ + "urlalgorithm": { Type: framework.TypeString, Description: `Hash algorithm to use (POST URL parameter)`, }, - "key_version": &framework.FieldSchema{ + "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": &framework.FieldSchema{ + "prehashed": { Type: framework.TypeBool, Description: `Set to 'true' when the input is already hashed. If the key type is 'rsa-2048' or 'rsa-4096', then the algorithm used to hash the input should be indicated by the 'algorithm' parameter.`, }, - "signature_algorithm": &framework.FieldSchema{ + + "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".`, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -95,33 +99,33 @@ func (b *backend) pathVerify() *framework.Path { Description: "The key to use", }, - "context": &framework.FieldSchema{ + "context": { Type: framework.TypeString, Description: `Base64 encoded context for key derivation. Required if key derivation is enabled; currently only available with ed25519 keys.`, }, - "signature": &framework.FieldSchema{ + "signature": { Type: framework.TypeString, Description: "The signature, including vault header/key version", }, - "hmac": &framework.FieldSchema{ + "hmac": { Type: framework.TypeString, Description: "The HMAC, including vault header/key version", }, - "input": &framework.FieldSchema{ + "input": { Type: framework.TypeString, Description: "The base64-encoded input data to verify", }, - "urlalgorithm": &framework.FieldSchema{ + "urlalgorithm": { Type: framework.TypeString, Description: `Hash algorithm to use (POST URL parameter)`, }, - "hash_algorithm": &framework.FieldSchema{ + "hash_algorithm": { Type: framework.TypeString, Default: "sha2-256", Description: `Hash algorithm to use (POST body parameter). Valid values are: @@ -133,21 +137,29 @@ derivation is enabled; currently only available with ed25519 keys.`, Defaults to "sha2-256". Not valid for all key types.`, }, - "algorithm": &framework.FieldSchema{ + + "algorithm": { Type: framework.TypeString, Default: "sha2-256", Description: `Deprecated: use "hash_algorithm" instead.`, }, - "prehashed": &framework.FieldSchema{ + "prehashed": { Type: framework.TypeBool, Description: `Set to 'true' when the input is already hashed. If the key type is 'rsa-2048' or 'rsa-4096', then the algorithm used to hash the input should be indicated by the 'algorithm' parameter.`, }, - "signature_algorithm": &framework.FieldSchema{ + + "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".`, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -163,13 +175,25 @@ func (b *backend) pathSignWrite(ctx context.Context, req *logical.Request, d *fr name := d.Get("name").(string) ver := d.Get("key_version").(int) inputB64 := d.Get("input").(string) - hashAlgorithm := d.Get("urlalgorithm").(string) - if hashAlgorithm == "" { - hashAlgorithm = d.Get("hash_algorithm").(string) - if hashAlgorithm == "" { - hashAlgorithm = d.Get("algorithm").(string) + hashAlgorithmStr := d.Get("urlalgorithm").(string) + if hashAlgorithmStr == "" { + hashAlgorithmStr = d.Get("hash_algorithm").(string) + if hashAlgorithmStr == "" { + hashAlgorithmStr = d.Get("algorithm").(string) } } + + 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) @@ -209,25 +233,12 @@ func (b *backend) pathSignWrite(ctx context.Context, req *logical.Request, d *fr } if p.Type.HashSignatureInput() && !prehashed { - var hf hash.Hash - switch hashAlgorithm { - case "sha2-224": - hf = sha256.New224() - case "sha2-256": - hf = sha256.New() - case "sha2-384": - hf = sha512.New384() - case "sha2-512": - hf = sha512.New() - default: - p.Unlock() - return logical.ErrorResponse(fmt.Sprintf("unsupported hash algorithm %s", hashAlgorithm)), nil - } + hf := keysutil.HashFuncMap[hashAlgorithm]() hf.Write(input) input = hf.Sum(nil) } - sig, err := p.Sign(ver, context, input, hashAlgorithm, sigAlgorithm) + sig, err := p.Sign(ver, context, input, hashAlgorithm, sigAlgorithm, marshaling) if err != nil { p.Unlock() return nil, err @@ -253,7 +264,6 @@ func (b *backend) pathSignWrite(ctx context.Context, req *logical.Request, d *fr } func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - sig := d.Get("signature").(string) hmac := d.Get("hmac").(string) switch { @@ -269,13 +279,25 @@ func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d * name := d.Get("name").(string) inputB64 := d.Get("input").(string) - hashAlgorithm := d.Get("urlalgorithm").(string) - if hashAlgorithm == "" { - hashAlgorithm = d.Get("hash_algorithm").(string) - if hashAlgorithm == "" { - hashAlgorithm = d.Get("algorithm").(string) + hashAlgorithmStr := d.Get("urlalgorithm").(string) + if hashAlgorithmStr == "" { + hashAlgorithmStr = d.Get("hash_algorithm").(string) + if hashAlgorithmStr == "" { + hashAlgorithmStr = d.Get("algorithm").(string) } } + + 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) @@ -315,25 +337,12 @@ func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d * } if p.Type.HashSignatureInput() && !prehashed { - var hf hash.Hash - switch hashAlgorithm { - case "sha2-224": - hf = sha256.New224() - case "sha2-256": - hf = sha256.New() - case "sha2-384": - hf = sha512.New384() - case "sha2-512": - hf = sha512.New() - default: - p.Unlock() - return logical.ErrorResponse(fmt.Sprintf("unsupported hash algorithm %s", hashAlgorithm)), nil - } + hf := keysutil.HashFuncMap[hashAlgorithm]() hf.Write(input) input = hf.Sum(nil) } - valid, err := p.VerifySignature(context, input, sig, hashAlgorithm, sigAlgorithm) + valid, err := p.VerifySignature(context, input, hashAlgorithm, sigAlgorithm, marshaling, sig) if err != nil { switch err.(type) { case errutil.UserError: diff --git a/builtin/logical/transit/path_sign_verify_test.go b/builtin/logical/transit/path_sign_verify_test.go index 1c05a060390f..f3ffc6b92a36 100644 --- a/builtin/logical/transit/path_sign_verify_test.go +++ b/builtin/logical/transit/path_sign_verify_test.go @@ -83,6 +83,7 @@ func TestTransit_SignVerify_P256(t *testing.T) { } signRequest := func(req *logical.Request, errExpected bool, postpath string) string { + t.Helper() req.Path = "sign/foo" + postpath resp, err := b.HandleRequest(context.Background(), req) if err != nil && !errExpected { @@ -108,6 +109,7 @@ func TestTransit_SignVerify_P256(t *testing.T) { } verifyRequest := func(req *logical.Request, errExpected bool, postpath, sig string) { + t.Helper() req.Path = "verify/foo" + postpath req.Data["signature"] = sig resp, err := b.HandleRequest(context.Background(), req) @@ -166,6 +168,22 @@ func TestTransit_SignVerify_P256(t *testing.T) { verifyRequest(req, false, "", sig) delete(req.Data, "prehashed") + // Test marshaling selection + // Bad value + req.Data["marshaling_algorithm"] = "asn2" + sig = signRequest(req, true, "") + // Use the default, verify we can't validate with jws + req.Data["marshaling_algorithm"] = "asn1" + sig = signRequest(req, false, "") + req.Data["marshaling_algorithm"] = "jws" + verifyRequest(req, true, "", sig) + // Sign with jws, verify we can validate + sig = signRequest(req, false, "") + verifyRequest(req, false, "", sig) + // If we change marshaling back to asn1 we shouldn't be able to verify + delete(req.Data, "marshaling_algorithm") + verifyRequest(req, true, "", sig) + // Test 512 and save sig for later to ensure we can't validate once min // decryption version is set req.Data["hash_algorithm"] = "sha2-512" diff --git a/helper/keysutil/consts.go b/helper/keysutil/consts.go new file mode 100644 index 000000000000..738a381f1984 --- /dev/null +++ b/helper/keysutil/consts.go @@ -0,0 +1,46 @@ +package keysutil + +import ( + "crypto/sha256" + "crypto/sha512" + "hash" +) + +type HashType uint32 + +const ( + _ = iota + HashTypeSHA2224 HashType = iota + HashTypeSHA2256 + HashTypeSHA2384 + HashTypeSHA2512 +) + +type MarshalingType uint32 + +const ( + _ = iota + MarshalingTypeASN1 MarshalingType = iota + MarshalingTypeJWS +) + +var ( + HashTypeMap = map[string]HashType{ + "sha2-224": HashTypeSHA2224, + "sha2-256": HashTypeSHA2256, + "sha2-384": HashTypeSHA2384, + "sha2-512": HashTypeSHA2512, + } + + HashFuncMap = map[HashType]func() hash.Hash{ + HashTypeSHA2224: sha256.New224, + HashTypeSHA2256: sha256.New, + HashTypeSHA2384: sha512.New384, + HashTypeSHA2512: sha512.New, + } + + MarshalingTypeMap = map[string]MarshalingType{ + "asn1": MarshalingTypeASN1, + "jws": MarshalingTypeJWS, + } +) diff --git a/helper/keysutil/policy.go b/helper/keysutil/policy.go index 4654c647b32a..38b988bb74c7 100644 --- a/helper/keysutil/policy.go +++ b/helper/keysutil/policy.go @@ -1043,7 +1043,7 @@ func (p *Policy) HMACKey(version int) ([]byte, error) { return p.Keys[strconv.Itoa(version)].HMACKey, nil } -func (p *Policy) Sign(ver int, context, input []byte, hashAlgorithm, sigAlgorithm string) (*SigningResult, error) { +func (p *Policy) Sign(ver int, context, input []byte, hashAlgorithm HashType, sigAlgorithm string, marshaling MarshalingType) (*SigningResult, error) { if !p.Type.SigningSupported() { return nil, fmt.Errorf("message signing not supported for key type %v", p.Type) } @@ -1064,6 +1064,7 @@ func (p *Policy) Sign(ver int, context, input []byte, hashAlgorithm, sigAlgorith var err error switch p.Type { case KeyType_ECDSA_P256: + curveBits := 256 keyParams := p.Keys[strconv.Itoa(ver)] key := &ecdsa.PrivateKey{ PublicKey: ecdsa.PublicKey{ @@ -1073,18 +1074,46 @@ func (p *Policy) Sign(ver int, context, input []byte, hashAlgorithm, sigAlgorith }, D: keyParams.EC_D, } + r, s, err := ecdsa.Sign(rand.Reader, key, input) if err != nil { return nil, err } - marshaledSig, err := asn1.Marshal(ecdsaSignature{ - R: r, - S: s, - }) - if err != nil { - return nil, err + + switch marshaling { + case MarshalingTypeASN1: + // This is used by openssl and X.509 + sig, err = asn1.Marshal(ecdsaSignature{ + R: r, + S: s, + }) + if err != nil { + return nil, err + } + + case MarshalingTypeJWS: + // This is used by JWS + + // First we have to get the length of the curve in bytes. Although + // we only support 256 now, we'll do this in an agnostic way so we + // can reuse this marshaling if we support e.g. 521. Getting the + // number of bytes without rounding up would be 65.125 so we need + // to add one in that case. + keyLen := curveBits / 8 + if curveBits%8 > 0 { + keyLen++ + } + + // Now create the output array + sig = make([]byte, keyLen*2) + rb := r.Bytes() + sb := s.Bytes() + copy(sig[keyLen-len(rb):], rb) + copy(sig[2*keyLen-len(sb):], sb) + + default: + return nil, errutil.UserError{Err: "requested marshaling type is invalid"} } - sig = marshaledSig case KeyType_ED25519: var key ed25519.PrivateKey @@ -1113,16 +1142,16 @@ func (p *Policy) Sign(ver int, context, input []byte, hashAlgorithm, sigAlgorith var algo crypto.Hash switch hashAlgorithm { - case "sha2-224": + case HashTypeSHA2224: algo = crypto.SHA224 - case "sha2-256": + case HashTypeSHA2256: algo = crypto.SHA256 - case "sha2-384": + case HashTypeSHA2384: algo = crypto.SHA384 - case "sha2-512": + case HashTypeSHA2512: algo = crypto.SHA512 default: - return nil, errutil.InternalError{Err: fmt.Sprintf("unsupported hash algorithm %s", hashAlgorithm)} + return nil, errutil.InternalError{Err: "unsupported hash algorithm"} } if sigAlgorithm == "" { @@ -1149,7 +1178,13 @@ func (p *Policy) Sign(ver int, context, input []byte, hashAlgorithm, sigAlgorith } // Convert to base64 - encoded := base64.StdEncoding.EncodeToString(sig) + var encoded string + switch marshaling { + case MarshalingTypeASN1: + encoded = base64.StdEncoding.EncodeToString(sig) + case MarshalingTypeJWS: + encoded = base64.RawURLEncoding.EncodeToString(sig) + } res := &SigningResult{ Signature: p.getVersionPrefix(ver) + encoded, PublicKey: pubKey, @@ -1158,7 +1193,7 @@ func (p *Policy) Sign(ver int, context, input []byte, hashAlgorithm, sigAlgorith return res, nil } -func (p *Policy) VerifySignature(context, input []byte, sig, hashAlgorithm string, sigAlgorithm string) (bool, error) { +func (p *Policy) VerifySignature(context, input []byte, hashAlgorithm HashType, sigAlgorithm string, marshaling MarshalingType, sig string) (bool, error) { if !p.Type.SigningSupported() { return false, errutil.UserError{Err: fmt.Sprintf("message verification not supported for key type %v", p.Type)} } @@ -1191,7 +1226,15 @@ func (p *Policy) VerifySignature(context, input []byte, sig, hashAlgorithm strin return false, errutil.UserError{Err: ErrTooOld} } - sigBytes, err := base64.StdEncoding.DecodeString(splitVerSig[1]) + var sigBytes []byte + switch marshaling { + case MarshalingTypeASN1: + sigBytes, err = base64.StdEncoding.DecodeString(splitVerSig[1]) + case MarshalingTypeJWS: + sigBytes, err = base64.RawURLEncoding.DecodeString(splitVerSig[1]) + default: + return false, errutil.UserError{Err: "requested marshaling type is invalid"} + } if err != nil { return false, errutil.UserError{Err: "invalid base64 signature value"} } @@ -1199,12 +1242,25 @@ func (p *Policy) VerifySignature(context, input []byte, sig, hashAlgorithm strin switch p.Type { case KeyType_ECDSA_P256: var ecdsaSig ecdsaSignature - rest, err := asn1.Unmarshal(sigBytes, &ecdsaSig) - if err != nil { - return false, errutil.UserError{Err: "supplied signature is invalid"} - } - if rest != nil && len(rest) != 0 { - return false, errutil.UserError{Err: "supplied signature contains extra data"} + + switch marshaling { + case MarshalingTypeASN1: + rest, err := asn1.Unmarshal(sigBytes, &ecdsaSig) + if err != nil { + return false, errutil.UserError{Err: "supplied signature is invalid"} + } + if rest != nil && len(rest) != 0 { + return false, errutil.UserError{Err: "supplied signature contains extra data"} + } + + case MarshalingTypeJWS: + paramLen := len(sigBytes) / 2 + rb := sigBytes[:paramLen] + sb := sigBytes[paramLen:] + ecdsaSig.R = new(big.Int) + ecdsaSig.R.SetBytes(rb) + ecdsaSig.S = new(big.Int) + ecdsaSig.S.SetBytes(sb) } keyParams := p.Keys[strconv.Itoa(ver)] @@ -1237,16 +1293,16 @@ func (p *Policy) VerifySignature(context, input []byte, sig, hashAlgorithm strin var algo crypto.Hash switch hashAlgorithm { - case "sha2-224": + case HashTypeSHA2224: algo = crypto.SHA224 - case "sha2-256": + case HashTypeSHA2256: algo = crypto.SHA256 - case "sha2-384": + case HashTypeSHA2384: algo = crypto.SHA384 - case "sha2-512": + case HashTypeSHA2512: algo = crypto.SHA512 default: - return false, errutil.InternalError{Err: fmt.Sprintf("unsupported hash algorithm %s", hashAlgorithm)} + return false, errutil.InternalError{Err: "unsupported hash algorithm"} } if sigAlgorithm == "" { diff --git a/website/source/api/secret/transit/index.html.md b/website/source/api/secret/transit/index.html.md index a0afa70e874f..88f596adbf0a 100644 --- a/website/source/api/secret/transit/index.html.md +++ b/website/source/api/secret/transit/index.html.md @@ -825,6 +825,12 @@ supports signing. - `pss` - `pkcs1v15` +- `marshaling_algorithm` `(string: "asn1")` – Specifies the way in which the signature should be marshaled. This currently only applies to ECDSA keys. Supported types are: + + - `asn1`: The default, used by OpenSSL and X.509 + - `jws`: The version used by JWS (and thus for JWTs). Selecting this will + also change the output encoding to URL-safe Base64 encoding instead of + standard Base64-encoding. ### Sample Payload @@ -901,6 +907,13 @@ data. - `pss` - `pkcs1v15` +- `marshaling_algorithm` `(string: "asn1")` – Specifies the way in which the signature was originally marshaled. This currently only applies to ECDSA keys. Supported types are: + + - `asn1`: The default, used by OpenSSL and X.509 + - `jws`: The version used by JWS (and thus for JWTs). Selecting this will + also expect the input encoding to URL-safe Base64 encoding instead of + standard Base64-encoding. + ### Sample Payload ```json