Implement JWS-compatible signature marshaling (#6077)

This currently only applies to ECDSA signatures, and is a toggleable
option.
This commit is contained in:
Jeff Mitchell 2019-01-23 12:31:34 -05:00 committed by GitHub
parent 4f05192be3
commit 5e126f6de8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 232 additions and 93 deletions

View File

@ -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,

View File

@ -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:

View File

@ -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"

46
helper/keysutil/consts.go Normal file
View File

@ -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,
}
)

View File

@ -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 == "" {

View File

@ -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