Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions blindsign/blindrsa/brsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -243,4 +263,5 @@ var (
ErrInvalidBlind = common.ErrInvalidBlind
ErrInvalidRandomness = common.ErrInvalidRandomness
ErrUnsupportedHashFunction = common.ErrUnsupportedHashFunction
ErrInvalidMessage = common.ErrInvalidMessage
)
49 changes: 49 additions & 0 deletions blindsign/blindrsa/brsa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions blindsign/blindrsa/internal/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
8 changes: 8 additions & 0 deletions blindsign/blindrsa/partiallyblindrsa/pbrsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -331,4 +338,5 @@ var (
ErrUnexpectedSize = common.ErrUnexpectedSize
ErrInvalidMessageLength = common.ErrInvalidMessageLength
ErrInvalidRandomness = common.ErrInvalidRandomness
ErrInvalidMessage = common.ErrInvalidMessage
)
Loading