Skip to content

Commit

Permalink
Add support for PKCSv1_5_NoOID signatures (#17636)
Browse files Browse the repository at this point in the history
* Add support for PKCSv1_5_NoOID signatures

This assumes a pre-hashed input has been provided to Vault, but we do
not write the hash's OID into the signature stream. This allows us to
generate the alternative PKCSv1_5_NoOID signature type rather than the
existing PKCSv1_5_DERnull signature type we presently use.

These are specified in RFC 3447 Section 9.2.

Signed-off-by: Alexander Scheel <[email protected]>

* Add changelog

Signed-off-by: Alexander Scheel <[email protected]>

* Exclude new none type from PSS based tests

Signed-off-by: Alexander Scheel <[email protected]>

* Add tests for PKCS#1v1.5 signatures

Signed-off-by: Alexander Scheel <[email protected]>

Signed-off-by: Alexander Scheel <[email protected]>
  • Loading branch information
cipherboy authored Oct 27, 2022
1 parent 0423ffb commit 961e76a
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 9 deletions.
29 changes: 29 additions & 0 deletions builtin/logical/transit/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,35 @@ func testTransit_RSA(t *testing.T, keyType string) {
if !resp.Data["valid"].(bool) {
t.Fatalf("failed to verify the RSA signature")
}

// Take a random hash and sign it using PKCSv1_5_NoOID.
hash := "P8m2iUWdc4+MiKOkiqnjNUIBa3pAUuABqqU2/KdIE8s="
signReq.Data = map[string]interface{}{
"input": hash,
"hash_algorithm": "none",
"signature_algorithm": "pkcs1v15",
"prehashed": true,
}
resp, err = b.HandleRequest(context.Background(), signReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
signature = resp.Data["signature"].(string)

verifyReq.Data = map[string]interface{}{
"input": hash,
"signature": signature,
"hash_algorithm": "none",
"signature_algorithm": "pkcs1v15",
"prehashed": true,
}
resp, err = b.HandleRequest(context.Background(), verifyReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("bad: err: %v\nresp: %#v", err, resp)
}
if !resp.Data["valid"].(bool) {
t.Fatalf("failed to verify the RSA signature")
}
}

func TestBackend_basic(t *testing.T) {
Expand Down
33 changes: 27 additions & 6 deletions builtin/logical/transit/path_sign_verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ type batchResponseVerifyItem struct {
err error
}

const defaultHashAlgorithm = "sha2-256"

func (b *backend) pathSign() *framework.Path {
return &framework.Path{
Pattern: "sign/" + framework.GenericNameRegex("name") + framework.OptionalParamRegex("urlalgorithm"),
Expand All @@ -83,7 +85,7 @@ derivation is enabled; currently only available with ed25519 keys.`,

"hash_algorithm": {
Type: framework.TypeString,
Default: "sha2-256",
Default: defaultHashAlgorithm,
Description: `Hash algorithm to use (POST body parameter). Valid values are:
* sha1
Expand All @@ -95,14 +97,17 @@ derivation is enabled; currently only available with ed25519 keys.`,
* sha3-256
* sha3-384
* sha3-512
* none
Defaults to "sha2-256". Not valid for all key types,
including ed25519.`,
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: "sha2-256",
Default: defaultHashAlgorithm,
Description: `Deprecated: use "hash_algorithm" instead.`,
},

Expand Down Expand Up @@ -189,7 +194,7 @@ derivation is enabled; currently only available with ed25519 keys.`,

"hash_algorithm": {
Type: framework.TypeString,
Default: "sha2-256",
Default: defaultHashAlgorithm,
Description: `Hash algorithm to use (POST body parameter). Valid values are:
* sha1
Expand All @@ -201,13 +206,15 @@ derivation is enabled; currently only available with ed25519 keys.`,
* sha3-256
* sha3-384
* sha3-512
* none
Defaults to "sha2-256". Not valid for all key types.`,
Defaults to "sha2-256". Not valid for all key types. See note about
none on signing path.`,
},

"algorithm": {
Type: framework.TypeString,
Default: "sha2-256",
Default: defaultHashAlgorithm,
Description: `Deprecated: use "hash_algorithm" instead.`,
},

Expand Down Expand Up @@ -280,6 +287,9 @@ func (b *backend) pathSignWrite(ctx context.Context, req *logical.Request, d *fr
hashAlgorithmStr = d.Get("hash_algorithm").(string)
if hashAlgorithmStr == "" {
hashAlgorithmStr = d.Get("algorithm").(string)
if hashAlgorithmStr == "" {
hashAlgorithmStr = defaultHashAlgorithm
}
}
}

Expand All @@ -301,6 +311,10 @@ func (b *backend) pathSignWrite(ctx context.Context, req *logical.Request, d *fr
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}

if hashAlgorithm == keysutil.HashTypeNone && (!prehashed || sigAlgorithm != "pkcs1v15") {
return logical.ErrorResponse("hash_algorithm=none requires both prehashed=true and signature_algorithm=pkcs1v15"), logical.ErrInvalidRequest
}

// Get the policy
p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{
Storage: req.Storage,
Expand Down Expand Up @@ -507,6 +521,9 @@ func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d *
hashAlgorithmStr = d.Get("hash_algorithm").(string)
if hashAlgorithmStr == "" {
hashAlgorithmStr = d.Get("algorithm").(string)
if hashAlgorithmStr == "" {
hashAlgorithmStr = defaultHashAlgorithm
}
}
}

Expand All @@ -528,6 +545,10 @@ func (b *backend) pathVerifyWrite(ctx context.Context, req *logical.Request, d *
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}

if hashAlgorithm == keysutil.HashTypeNone && (!prehashed || sigAlgorithm != "pkcs1v15") {
return logical.ErrorResponse("hash_algorithm=none requires both prehashed=true and signature_algorithm=pkcs1v15"), logical.ErrInvalidRequest
}

// Get the policy
p, _, err := b.GetPolicy(ctx, keysutil.PolicyRequest{
Storage: req.Storage,
Expand Down
5 changes: 4 additions & 1 deletion builtin/logical/transit/path_sign_verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func testTransit_SignVerify_ECDSA(t *testing.T, bits int) {
req.Path = "sign/foo" + postpath
resp, err := b.HandleRequest(context.Background(), req)
if err != nil && !errExpected {
t.Fatal(err)
t.Fatalf("request: %v\nerror: %v", req, err)
}
if resp == nil {
t.Fatal("expected non-nil response")
Expand Down Expand Up @@ -950,6 +950,9 @@ func testTransit_SignVerify_RSA_PSS(t *testing.T, bits int) {

for hashAlgorithm := range keysutil.HashTypeMap {
t.Log("Hash algorithm:", hashAlgorithm)
if hashAlgorithm == "none" {
continue
}

for marshalingName := range keysutil.MarshalingTypeMap {
t.Log("\t", "Marshaling type:", marshalingName)
Expand Down
3 changes: 3 additions & 0 deletions changelog/17636.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
secrets/transit: Add support for PKCSv1_5_NoOID RSA signatures
```
7 changes: 5 additions & 2 deletions sdk/helper/keysutil/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
type HashType uint32

const (
_ = iota
HashTypeSHA1 HashType = iota
HashTypeNone HashType = iota
HashTypeSHA1
HashTypeSHA2224
HashTypeSHA2256
HashTypeSHA2384
Expand All @@ -35,6 +35,7 @@ const (

var (
HashTypeMap = map[string]HashType{
"none": HashTypeNone,
"sha1": HashTypeSHA1,
"sha2-224": HashTypeSHA2224,
"sha2-256": HashTypeSHA2256,
Expand All @@ -47,6 +48,7 @@ var (
}

HashFuncMap = map[HashType]func() hash.Hash{
HashTypeNone: nil,
HashTypeSHA1: sha1.New,
HashTypeSHA2224: sha256.New224,
HashTypeSHA2256: sha256.New,
Expand All @@ -59,6 +61,7 @@ var (
}

CryptoHashMap = map[HashType]crypto.Hash{
HashTypeNone: 0,
HashTypeSHA1: crypto.SHA1,
HashTypeSHA2224: crypto.SHA224,
HashTypeSHA2256: crypto.SHA256,
Expand Down
89 changes: 89 additions & 0 deletions sdk/helper/keysutil/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,9 @@ func Test_RSA_PSS(t *testing.T) {
// 2. For each hash algorithm...
for hashAlgorithm, hashType := range HashTypeMap {
t.Log(tabs[1], "Hash algorithm:", hashAlgorithm)
if hashAlgorithm == "none" {
continue
}

// 3. For each marshaling type...
for marshalingName, marshalingType := range MarshalingTypeMap {
Expand All @@ -964,6 +967,92 @@ func Test_RSA_PSS(t *testing.T) {
}
}

func Test_RSA_PKCS1(t *testing.T) {
t.Log("Testing RSA PKCS#1v1.5")

ctx := context.Background()
storage := &logical.InmemStorage{}
// https://crypto.stackexchange.com/a/1222
input := []byte("Sphinx of black quartz, judge my vow")
sigAlgorithm := "pkcs1v15"

tabs := make(map[int]string)
for i := 1; i <= 6; i++ {
tabs[i] = strings.Repeat("\t", i)
}

test_RSA_PKCS1 := func(t *testing.T, p *Policy, rsaKey *rsa.PrivateKey, hashType HashType,
marshalingType MarshalingType,
) {
unsaltedOptions := SigningOptions{
HashAlgorithm: hashType,
Marshaling: marshalingType,
SigAlgorithm: sigAlgorithm,
}
cryptoHash := CryptoHashMap[hashType]

// PKCS#1v1.5 NoOID uses a direct input and assumes it is pre-hashed.
if hashType != 0 {
hash := cryptoHash.New()
hash.Write(input)
input = hash.Sum(nil)
}

// 1. Make a signature with the given key size and hash algorithm.
t.Log(tabs[3], "Make an automatic signature")
sig, err := p.Sign(0, nil, input, hashType, sigAlgorithm, marshalingType)
if err != nil {
// A bit of a hack but FIPS go does not support some hash types
if isUnsupportedGoHashType(hashType, err) {
t.Skip(tabs[4], "skipping test as FIPS Go does not support hash type")
return
}
t.Fatal(tabs[4], "❌ Failed to automatically sign:", err)
}

// 1.1 Verify this signature using the *inferred* salt length.
autoVerify(4, t, p, input, sig, unsaltedOptions)
}

rsaKeyTypes := []KeyType{KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096}
testKeys, err := generateTestKeys()
if err != nil {
t.Fatalf("error generating test keys: %s", err)
}

// 1. For each standard RSA key size 2048, 3072, and 4096...
for _, rsaKeyType := range rsaKeyTypes {
t.Log("Key size: ", rsaKeyType)
p := &Policy{
Name: fmt.Sprint(rsaKeyType), // NOTE: crucial to create a new key per key size
Type: rsaKeyType,
}

rsaKeyBytes := testKeys[rsaKeyType]
err := p.Import(ctx, storage, rsaKeyBytes, rand.Reader)
if err != nil {
t.Fatal(tabs[1], "❌ Failed to import key:", err)
}
rsaKeyAny, err := x509.ParsePKCS8PrivateKey(rsaKeyBytes)
if err != nil {
t.Fatalf("error parsing test keys: %s", err)
}
rsaKey := rsaKeyAny.(*rsa.PrivateKey)

// 2. For each hash algorithm...
for hashAlgorithm, hashType := range HashTypeMap {
t.Log(tabs[1], "Hash algorithm:", hashAlgorithm)

// 3. For each marshaling type...
for marshalingName, marshalingType := range MarshalingTypeMap {
t.Log(tabs[2], "Marshaling type:", marshalingName)
testName := fmt.Sprintf("%s-%s-%s", rsaKeyType, hashAlgorithm, marshalingName)
t.Run(testName, func(t *testing.T) { test_RSA_PKCS1(t, p, rsaKey, hashType, marshalingType) })
}
}
}
}

// Normal Go builds support all the hash functions for RSA_PSS signatures but the
// FIPS Go build does not support at this time the SHA3 hashes as FIPS 140_2 does
// not accept them.
Expand Down
12 changes: 12 additions & 0 deletions website/content/api-docs/secret/transit.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,7 @@ supports signing.
- `sha3-256`
- `sha3-384`
- `sha3-512`
- `none`

~> **Note**: In FIPS 140-2 mode, the following algorithms are not certified
and thus should not be used: `sha3-224`, `sha3-256`, `sha3-384`, and
Expand All @@ -1140,6 +1141,11 @@ supports signing.
}
```

~> **Note**: using `hash_algorithm=none` requires setting `prehashed=true`
and `signature_algorithm=pkcs1v15`. This generates a `PKCSv1_5_NoOID`
signature rather than the `PKCSv1_5_DERnull` signature type usually
created. See [RFC 3447 Section 9.2](https://www.rfc-editor.org/rfc/rfc3447#section-9.2).

- `input` `(string: "")` – Specifies the **base64 encoded** input data. One of
`input` or `batch_input` must be supplied.

Expand Down Expand Up @@ -1293,6 +1299,7 @@ data.
- `sha3-256`
- `sha3-384`
- `sha3-512`
- `none`

~> **Note**: In FIPS 140-2 mode, the following algorithms are not certified
and thus should not be used: `sha3-224`, `sha3-256`, `sha3-384`, and
Expand All @@ -1309,6 +1316,11 @@ data.
}
```

~> **Note**: using `hash_algorithm=none` requires setting `prehashed=true`
and `signature_algorithm=pkcs1v15`. This verifies a `PKCSv1_5_NoOID`
signature rather than the `PKCSv1_5_DERnull` signature type usually
verified. See [RFC 3447 Section 9.2](https://www.rfc-editor.org/rfc/rfc3447#section-9.2).

- `input` `(string: "")` – Specifies the **base64 encoded** input data. One of
`input` or `batch_input` must be supplied.

Expand Down

0 comments on commit 961e76a

Please sign in to comment.