diff --git a/blindsign/blindrsa/brsa.go b/blindsign/blindrsa/brsa.go index 1de9a2031..745bbb268 100644 --- a/blindsign/blindrsa/brsa.go +++ b/blindsign/blindrsa/brsa.go @@ -19,6 +19,19 @@ // - RSABSSA-SHA384-PSS-Randomized // - RSABSSA-SHA384-PSSZERO-Randomized // +// # Security considerations for deterministic variants +// +// The deterministic variants (RSABSSA-SHA384-PSS-Deterministic and +// RSABSSA-SHA384-PSSZERO-Deterministic) use an empty Prepare prefix and, in the +// PSSZERO case, a zero-length salt, so the EMSA-PSS encoding of a given message +// is fixed. Blind enforces the RFC 9474 coprimality check (rejecting encoded +// messages that share a factor with the modulus), which prevents a malicious +// signer with an invalid modulus from learning gcd(m, N) from a single blinded +// request. Nevertheless, deployments that sign low-entropy deterministic +// messages in a malicious-signer setting should still prefer the randomized +// variants (or otherwise ensure high-entropy encodings) and verify that the +// signer's key is honestly generated. +// // [RFC-9474]: https://www.rfc-editor.org/info/rfc9474 package blindrsa @@ -135,6 +148,13 @@ func (c Client) fixedBlind(message, salt []byte, r, rInv *big.Int) (blindedMsg [ m := new(big.Int).SetBytes(encodedMsg) + // RFC 9474 requires the encoded message to be coprime to the modulus + // before blinding; otherwise a malicious signer with an invalid modulus + // could learn gcd(m, N) from the blinded message. + if new(big.Int).GCD(nil, nil, m, c.v.pk.N).Cmp(big.NewInt(1)) != 0 { + return nil, State{}, common.ErrInvalidMessage + } + bigE := big.NewInt(int64(c.v.pk.E)) x := new(big.Int).Exp(r, bigE, c.v.pk.N) z := new(big.Int).Set(m) @@ -243,4 +263,5 @@ var ( ErrInvalidBlind = common.ErrInvalidBlind ErrInvalidRandomness = common.ErrInvalidRandomness ErrUnsupportedHashFunction = common.ErrUnsupportedHashFunction + ErrInvalidMessage = common.ErrInvalidMessage ) diff --git a/blindsign/blindrsa/brsa_test.go b/blindsign/blindrsa/brsa_test.go index 53f851974..9c7c3f188 100644 --- a/blindsign/blindrsa/brsa_test.go +++ b/blindsign/blindrsa/brsa_test.go @@ -184,6 +184,55 @@ func TestDeterministicBlindFailure(t *testing.T) { } } +// TestBlindRejectsNonCoprimeMessage ensures Blind enforces the RFC 9474 +// coprimality check. A malicious signer that publishes an invalid (even) +// modulus would otherwise learn gcd(encoded_msg, N) from the blinded request, +// leaking information about low-entropy deterministic messages. Since the +// EMSA-PSS encoding always ends in the 0xbc trailer, the encoded message is +// always even, so any even modulus shares the factor 2 with it. +func TestBlindRejectsNonCoprimeMessage(t *testing.T) { + message := []byte("hello world") + key, err := loadPrivateKey() + if err != nil { + t.Fatal(err) + } + + // Build a malicious public key with an even (invalid) modulus of the same + // bit length so that the EMSA-PSS encoding is unchanged. + maliciousN := new(big.Int).Set(key.N) + maliciousN.SetBit(maliciousN, 0, 0) // clear the low bit -> even modulus + maliciousPub := &rsa.PublicKey{N: maliciousN, E: key.E} + + for _, variant := range []Variant{ + SHA384PSSDeterministic, + SHA384PSSZeroDeterministic, + SHA384PSSRandomized, + SHA384PSSZeroRandomized, + } { + t.Run(variant.String(), func(tt *testing.T) { + client, err := NewClient(variant, maliciousPub) + if err != nil { + tt.Fatal(err) + } + + inputMsg, err := client.Prepare(rand.Reader, message) + if err != nil { + tt.Fatal(err) + } + + // Use an invertible blind (r = rInv = 1) so the coprimality + // check, rather than the blinding-factor invertibility check, + // is what rejects the request. + one := big.NewInt(1) + salt := make([]byte, client.v.SaltLength) + _, _, err = client.fixedBlind(inputMsg, salt, one, one) + if err != ErrInvalidMessage { + tt.Fatalf("expected ErrInvalidMessage, got %v", err) + } + }) + } +} + func TestRandomSignVerify(t *testing.T) { message := []byte("hello world") key, err := loadPrivateKey() diff --git a/blindsign/blindrsa/internal/common/common.go b/blindsign/blindrsa/internal/common/common.go index 55e1c8bfa..957a07220 100644 --- a/blindsign/blindrsa/internal/common/common.go +++ b/blindsign/blindrsa/internal/common/common.go @@ -137,6 +137,9 @@ var ( // ErrInvalidMessageLength is the error used if the size of a protocol message does not match its expected value. ErrInvalidMessageLength = errors.New("blindsign/blindrsa: invalid message length") + // ErrInvalidMessage is the error used if the encoded message is not coprime to the public modulus. + ErrInvalidMessage = errors.New("blindsign/blindrsa: invalid input") + // ErrInvalidBlind is the error used if the blind generated by the Verifier fails. ErrInvalidBlind = errors.New("blindsign/blindrsa: invalid blind") diff --git a/blindsign/blindrsa/partiallyblindrsa/pbrsa.go b/blindsign/blindrsa/partiallyblindrsa/pbrsa.go index f168b3e56..cf6ec8c22 100644 --- a/blindsign/blindrsa/partiallyblindrsa/pbrsa.go +++ b/blindsign/blindrsa/partiallyblindrsa/pbrsa.go @@ -121,6 +121,13 @@ func fixedPartiallyBlind(message, salt []byte, r, rInv *big.Int, pk *keys.BigPub m := new(big.Int).SetBytes(encodedMsg) + // The partially-blind RSA draft requires the encoded message to be coprime + // to the modulus before blinding; otherwise a malicious signer with an + // invalid modulus could learn gcd(m, N) from the blinded message. + if new(big.Int).GCD(nil, nil, m, pk.N).Cmp(big.NewInt(1)) != 0 { + return nil, VerifierState{}, common.ErrInvalidMessage + } + bigE := pk.E x := new(big.Int).Exp(r, bigE, pk.N) z := new(big.Int).Set(m) @@ -331,4 +338,5 @@ var ( ErrUnexpectedSize = common.ErrUnexpectedSize ErrInvalidMessageLength = common.ErrInvalidMessageLength ErrInvalidRandomness = common.ErrInvalidRandomness + ErrInvalidMessage = common.ErrInvalidMessage )