diff --git a/builtin/logical/transit/backend_test.go b/builtin/logical/transit/backend_test.go index cfbd4f4da..97c8a3da3 100644 --- a/builtin/logical/transit/backend_test.go +++ b/builtin/logical/transit/backend_test.go @@ -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) { diff --git a/builtin/logical/transit/path_sign_verify.go b/builtin/logical/transit/path_sign_verify.go index 1f0a9f3cb..25498b79f 100644 --- a/builtin/logical/transit/path_sign_verify.go +++ b/builtin/logical/transit/path_sign_verify.go @@ -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"), @@ -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 @@ -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.`, }, @@ -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 @@ -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.`, }, @@ -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 + } } } @@ -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, @@ -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 + } } } @@ -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, diff --git a/builtin/logical/transit/path_sign_verify_test.go b/builtin/logical/transit/path_sign_verify_test.go index fa411b0a2..f1a7edcfc 100644 --- a/builtin/logical/transit/path_sign_verify_test.go +++ b/builtin/logical/transit/path_sign_verify_test.go @@ -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") @@ -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) diff --git a/changelog/17636.txt b/changelog/17636.txt new file mode 100644 index 000000000..0668f2187 --- /dev/null +++ b/changelog/17636.txt @@ -0,0 +1,3 @@ +```release-note:improvement +secrets/transit: Add support for PKCSv1_5_NoOID RSA signatures +``` diff --git a/sdk/helper/keysutil/consts.go b/sdk/helper/keysutil/consts.go index cbb812351..e6c657b91 100644 --- a/sdk/helper/keysutil/consts.go +++ b/sdk/helper/keysutil/consts.go @@ -13,8 +13,8 @@ import ( type HashType uint32 const ( - _ = iota - HashTypeSHA1 HashType = iota + HashTypeNone HashType = iota + HashTypeSHA1 HashTypeSHA2224 HashTypeSHA2256 HashTypeSHA2384 @@ -35,6 +35,7 @@ const ( var ( HashTypeMap = map[string]HashType{ + "none": HashTypeNone, "sha1": HashTypeSHA1, "sha2-224": HashTypeSHA2224, "sha2-256": HashTypeSHA2256, @@ -47,6 +48,7 @@ var ( } HashFuncMap = map[HashType]func() hash.Hash{ + HashTypeNone: nil, HashTypeSHA1: sha1.New, HashTypeSHA2224: sha256.New224, HashTypeSHA2256: sha256.New, @@ -59,6 +61,7 @@ var ( } CryptoHashMap = map[HashType]crypto.Hash{ + HashTypeNone: 0, HashTypeSHA1: crypto.SHA1, HashTypeSHA2224: crypto.SHA224, HashTypeSHA2256: crypto.SHA256, diff --git a/sdk/helper/keysutil/policy_test.go b/sdk/helper/keysutil/policy_test.go index 91767cfd9..2df73971a 100644 --- a/sdk/helper/keysutil/policy_test.go +++ b/sdk/helper/keysutil/policy_test.go @@ -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 { @@ -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. diff --git a/website/content/api-docs/secret/transit.mdx b/website/content/api-docs/secret/transit.mdx index f5870af1f..41d77cb43 100644 --- a/website/content/api-docs/secret/transit.mdx +++ b/website/content/api-docs/secret/transit.mdx @@ -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 @@ -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. @@ -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 @@ -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.