Skip to content

Commit 4f9db58

Browse files
committed
Fix RSA-PSS salt length to comply with RFC 9421
Use 64-byte salt (PSSSaltLengthEqualsHash) instead of maximum salt (~190 bytes) to match RFC 9421 Section 3.3.1, TLS 1.3, and ensure interoperability with WebCrypto and other RFC-compliant implementations. Verification remains backwards compatible with old signatures by using auto-detect. Fixes #17 Made-with: Cursor
1 parent c2013d0 commit 4f9db58

2 files changed

Lines changed: 85 additions & 1 deletion

File tree

crypto.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,10 @@ func (s Signer) sign(buff []byte) ([]byte, error) {
222222
case "rsa-pss-sha512":
223223
hashed := sha512.Sum512(buff)
224224
key := s.key.(rsa.PrivateKey)
225-
sig, err := rsa.SignPSS(rand.Reader, &key, crypto.SHA512, hashed[:], nil)
225+
// RFC 9421 Section 3.3.1 requires salt length = hash output length (64 bytes for SHA-512)
226+
// to match TLS 1.3 and ensure interoperability with WebCrypto and other implementations
227+
opts := &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}
228+
sig, err := rsa.SignPSS(rand.Reader, &key, crypto.SHA512, hashed[:], opts)
226229
if err != nil {
227230
return nil, fmt.Errorf("RSA-PSS signature failed")
228231
}

signatures_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package httpsign
33
import (
44
"bufio"
55
"bytes"
6+
"crypto"
67
"crypto/ecdsa"
78
"crypto/ed25519"
89
"crypto/elliptic"
910
"crypto/rand"
1011
"crypto/rsa"
12+
"crypto/sha512"
1113
"crypto/x509"
1214
"crypto/x509/pkix"
1315
"encoding/asn1"
@@ -1036,6 +1038,85 @@ func TestMessageSignAndVerifyRSAPSS(t *testing.T) {
10361038
}
10371039
}
10381040

1041+
// TestRSAPSSSaltLength verifies that RSA-PSS signatures use the correct salt length
1042+
// (64 bytes for SHA-512) as required by RFC 9421 Section 3.3.1
1043+
func TestRSAPSSSaltLength(t *testing.T) {
1044+
prvKey, err := loadRSAPSSPrivateKey(rsaPSSPrvKey)
1045+
if err != nil {
1046+
t.Fatalf("cannot read private key: %v", err)
1047+
}
1048+
pubKey, err := parseRsaPublicKeyFromPemStr(rsaPSSPubKey)
1049+
if err != nil {
1050+
t.Fatalf("cannot read public key: %v", err)
1051+
}
1052+
1053+
config := NewSignConfig().SetKeyID("test-key-rsa-pss")
1054+
fields := Headers("@authority", "date")
1055+
signer, err := NewRSAPSSSigner(*prvKey, config, fields)
1056+
assert.NoError(t, err, "failed to create signer")
1057+
1058+
// Use the internal sign method directly on test data
1059+
testData := []byte("test signature base for salt length verification")
1060+
sigBytes, err := signer.sign(testData)
1061+
assert.NoError(t, err, "signing failed")
1062+
1063+
// Hash the test data
1064+
hashed := sha512.Sum512(testData)
1065+
1066+
// Verify with explicit PSSSaltLengthEqualsHash (64 bytes for SHA-512)
1067+
// If this succeeds, it proves our signer uses the correct salt length per RFC 9421
1068+
opts := &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}
1069+
err = rsa.VerifyPSS(pubKey, crypto.SHA512, hashed[:], sigBytes, opts)
1070+
assert.NoError(t, err, "signature should verify with 64-byte salt (PSSSaltLengthEqualsHash)")
1071+
1072+
// Also verify that it would fail with wrong salt length expectation (e.g., 32 bytes)
1073+
optsWrong := &rsa.PSSOptions{SaltLength: 32}
1074+
err = rsa.VerifyPSS(pubKey, crypto.SHA512, hashed[:], sigBytes, optsWrong)
1075+
assert.Error(t, err, "signature should NOT verify with wrong salt length (32 bytes)")
1076+
1077+
// Verify with PSSSaltLengthAuto should also work (it auto-detects)
1078+
err = rsa.VerifyPSS(pubKey, crypto.SHA512, hashed[:], sigBytes, nil)
1079+
assert.NoError(t, err, "signature should verify with auto-detect (nil options)")
1080+
}
1081+
1082+
// TestRSAPSSBackwardsCompatibility verifies that old signatures (created with PSSSaltLengthAuto,
1083+
// ~190 bytes of salt) can still be verified by the current verifier
1084+
func TestRSAPSSBackwardsCompatibility(t *testing.T) {
1085+
prvKey, err := loadRSAPSSPrivateKey(rsaPSSPrvKey)
1086+
if err != nil {
1087+
t.Fatalf("cannot read private key: %v", err)
1088+
}
1089+
pubKey, err := parseRsaPublicKeyFromPemStr(rsaPSSPubKey)
1090+
if err != nil {
1091+
t.Fatalf("cannot read public key: %v", err)
1092+
}
1093+
1094+
// Simulate an "old" signature created with PSSSaltLengthAuto (maximum salt, ~190 bytes)
1095+
testData := []byte("test signature base for backwards compatibility")
1096+
hashed := sha512.Sum512(testData)
1097+
1098+
// Create a signature with PSSSaltLengthAuto (old behavior)
1099+
optsOld := &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthAuto}
1100+
oldSig, err := rsa.SignPSS(rand.Reader, prvKey, crypto.SHA512, hashed[:], optsOld)
1101+
assert.NoError(t, err, "old-style signing should work")
1102+
1103+
// Create a verifier using the current implementation
1104+
config := NewVerifyConfig().SetKeyID("test-key-rsa-pss")
1105+
fields := Headers("@authority", "date")
1106+
verifier, err := NewRSAPSSVerifier(*pubKey, config, fields)
1107+
assert.NoError(t, err, "failed to create verifier")
1108+
1109+
// Verify the old signature using the verifier's internal method
1110+
// The verifier should accept it because it uses nil options (auto-detect)
1111+
success, err := verifier.verify(testData, oldSig)
1112+
assert.NoError(t, err, "old signature (with max salt ~190 bytes) should still verify")
1113+
assert.True(t, success, "verification should succeed")
1114+
1115+
// Also test with direct VerifyPSS using nil (auto-detect) - this is what the verifier uses
1116+
err = rsa.VerifyPSS(pubKey, crypto.SHA512, hashed[:], oldSig, nil)
1117+
assert.NoError(t, err, "old signature should verify with auto-detect (nil options)")
1118+
}
1119+
10391120
func TestSignAndVerifyRSA(t *testing.T) {
10401121
config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475).SetKeyID("test-key-rsa")
10411122
fields := Headers("@authority", "date", "content-type")

0 commit comments

Comments
 (0)