public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/perl-Crypt-DSA] epel8: Fix for modulo bias in key generation (CVE-2026-14570)
@ 2026-07-03 18:08 Paul Howarth
  0 siblings, 0 replies; only message in thread
From: Paul Howarth @ 2026-07-03 18:08 UTC (permalink / raw)
  To: git-commits

            A new commit has been pushed.

            Repo   : rpms/perl-Crypt-DSA
            Branch : epel8
            Commit : 16a6542943a05e5bcaffd52e366bc0793727247d
            Author : Paul Howarth <paul@city-fan.org>
            Date   : 2026-07-03T18:52:25+01:00
            Stats  : +216/-2 in 2 file(s)
            URL    : https://src.fedoraproject.org/rpms/perl-Crypt-DSA/c/16a6542943a05e5bcaffd52e366bc0793727247d?branch=epel8

            Log:
            Fix for modulo bias in key generation (CVE-2026-14570)

Backported from Crypt::DSA 1.22

- Hardening: Use a fresh, independent CSPRNG witness every round

- Security fix: Modulo bias in key generation (CVE-2026-14570); an attack
  with hundreds of signatures could lead to full private-key compromise;
  keys should be considered compromised and new keys should be generated

---
diff --git a/Crypt-DSA-1.17-CVE-2026-14570.patch b/Crypt-DSA-1.17-CVE-2026-14570.patch
new file mode 100644
index 0000000..e8c8c2f
--- /dev/null
+++ b/Crypt-DSA-1.17-CVE-2026-14570.patch
@@ -0,0 +1,201 @@
+--- lib/Crypt/DSA/KeyChain.pm
++++ lib/Crypt/DSA/KeyChain.pm
+@@ -16,7 +16,7 @@ BEGIN {
+ }
+ 
+ use Crypt::DSA::Key;
+-use Crypt::DSA::Util qw( bin2mp bitsize mod_exp makerandom isprime );
++use Crypt::DSA::Util qw( bin2mp bitsize mod_exp makerandom randombelow isprime );
+ 
+ sub new {
+     my $class = shift;
+@@ -137,9 +137,9 @@ sub generate_keys {
+     my $key = shift;
+     my($priv_key, $pub_key);
+     SCOPE: {
+-        my $i = bitsize($key->q);
+-        $priv_key = makerandom(Size => $i);
+-        $priv_key -= $key->q if $priv_key >= $key->q;
++        # Private key must be uniform in [1, q-1]; randombelow() does not
++        # force the high bit, so (unlike makerandom) it is not biased.
++        $priv_key = randombelow($key->q);
+         redo if $priv_key == 0;
+     }
+     $pub_key = mod_exp($key->g, $priv_key, $key->p);
+--- lib/Crypt/DSA/Util.pm
++++ lib/Crypt/DSA/Util.pm
+@@ -12,7 +12,7 @@ use Exporter;
+ BEGIN {
+     $VERSION   = '1.17';
+     @ISA       = qw( Exporter );
+-    @EXPORT_OK = qw( bitsize bin2mp mp2bin mod_inverse mod_exp makerandom isprime );
++    @EXPORT_OK = qw( bitsize bin2mp mp2bin mod_inverse mod_exp makerandom randombelow isprime );
+ }
+ 
+ ## Nicked from Crypt::RSA::DataFormat.
+@@ -61,17 +61,54 @@ sub makerandom {
+     Math::BigInt->new('0x' . $r);
+ }
+ 
++# Uniform random integer in [0, $n-1] with no modulo bias (rejection
++# sampling).  Unlike makerandom(), this does NOT force the high bit, so
++# it is correct for DSA nonces and private keys, which must be uniform
++# in [1, q-1].  makerandom() forces the high bit to obtain an exactly
++# N-bit value for prime search, which biases a nonce/key: folding its
++# output with "v -= q if v >= q" leaves the band [2^N-q, 2^(N-1)-1]
++# unreachable (CWE-330, biased-nonce -> lattice key recovery).
++sub randombelow {
++    my $n = shift;
++    $n = Math::BigInt->new("$n") unless ref($n) eq 'Math::BigInt';
++    croak "randombelow: argument must be > 0" unless $n > 0;
++    my $bits  = length($n->as_bin) - 2;
++    my $bytes = int(($bits + 7) / 8) + 1;          # one byte of headroom
++    my $rmax  = Math::BigInt->new(2) ** (8 * $bytes);
++    my $limit = $rmax - ($rmax % $n);              # largest multiple of $n <= rmax
++    my $r;
++    do {
++        $r = Math::BigInt->new('0x' . unpack('H*', urandom($bytes)));
++    } while $r >= $limit;
++    $r % $n;
++}
++
+ # For testing, let us choose our isprime function:
+ *isprime = \&isprime_algorithms_with_perl;
+ 
++# CSPRNG-drawn Miller-Rabin base, uniform enough in [2, n-2].  A witness
++# only has to be a random base in range for the strong-pseudoprime test
++# to be sound, so (unlike a secret nonce) a plain draw with a few extra
++# bytes of headroom is fine -- the modulo bias is immaterial here.
++sub _random_base {
++    my ($n) = @_;
++    my $range = $n - 3;                     # 0 .. n-4
++    my $bytes = int(bitsize($n) / 8) + 8;   # headroom keeps bias negligible
++    my $r = Math::BigInt->new('0x' . unpack 'H*', urandom($bytes));
++    ($r % $range) + 2;                      # 2 .. n-2
++}
++
+ # from the book "Mastering Algorithms with Perl" by Jon Orwant,
+ # Jarkko Hietaniemi, and John Macdonald
+ sub isprime_algorithms_with_perl {
+     use integer;
+     my $n = shift;
++    return 0 if $n < 2;
++    return 1 if $n < 4;         # 2 and 3 are prime
++    return 0 unless $n % 2;     # even n > 2 (also keeps _random_base's
++                                # [2, n-2] range non-degenerate)
+     my $n1 = $n - 1;
+     my $one = $n - $n1;  # not just 1, but a bigint
+-    my $witness = $one * 100;
+ 
+     # find the power of two for the top bit of $n1
+     my $p2 = $one;
+@@ -85,10 +122,12 @@ sub isprime_algorithms_with_perl {
+     $last_witness += (260 - $p2index) / 13 if $p2index < 260;
+ 
+     for my $witness_count (1..$last_witness) {
+-	$witness *= 1024;
+-	$witness += int(rand(1024));  # XXXX use good rand
+-	$witness = $witness % $n if $witness > $n;
+-	$witness = $one * 100, redo if $witness == 0;
++	# Fresh, independent CSPRNG witness every round.  The old code
++	# accumulated witnesses from int(rand(1024)) -- Perl's predictable
++	# Mersenne-Twister PRNG, and correlated round-to-round -- which
++	# both weakens each round and breaks the independence the
++	# Miller-Rabin error bound assumes.
++	my $witness = _random_base($n);
+ 
+ 	my $prod = $one;
+ 	my $n1bits = $n1;
+--- lib/Crypt/DSA.pm
++++ lib/Crypt/DSA.pm
+@@ -7,7 +7,7 @@ use Carp qw( croak );
+ use Crypt::DSA::KeyChain;
+ use Crypt::DSA::Key;
+ use Crypt::DSA::Signature;
+-use Crypt::DSA::Util qw( bitsize bin2mp mod_inverse mod_exp makerandom );
++use Crypt::DSA::Util qw( bitsize bin2mp mod_inverse mod_exp makerandom randombelow );
+ 
+ use vars qw( $VERSION );
+ BEGIN {
+@@ -67,8 +67,9 @@ sub _sign_setup {
+     my $key = shift;
+     my($k, $r);
+     {
+-        $k = makerandom(Size => bitsize($key->q));
+-        $k -= $key->q if $k >= $key->q;
++        # Nonce must be uniform in [1, q-1].  Do NOT use makerandom()
++        # (it forces the high bit, which biases the nonce after folding).
++        $k = randombelow($key->q);
+         redo if $k == 0;
+     }
+     $r = mod_exp($key->g, $k, $key->p);
+--- MANIFEST
++++ MANIFEST
+@@ -19,6 +19,7 @@ t/04-pem.t
+ t/06-fips.t
+ t/07-openid.t
+ t/08-cve-2026-12205.t
++t/09-CVE-2026-14570.t
+ xt/meta.t
+ xt/pmv.t
+ xt/pod.t
+--- t/09-CVE-2026-14570.t
++++ t/09-CVE-2026-14570.t
+@@ -0,0 +1,57 @@
++use strict;
++use warnings;
++use Test::More;
++# PoC: biased DSA nonce k in Crypt::DSA (<= 1.21), and negative control
++# for patches/crypt-dsa-biased-nonce.patch.
++#
++# Root cause: Crypt::DSA::Util::makerandom(Size=>N) forces the top bit
++# (it produces an *exactly* N-bit integer, for prime search). DSA.pm
++# _sign_setup reuses it for the nonce and folds with "k -= q if k >= q".
++# Since k_raw is always in [2^(N-1), 2^N-1], the fold leaves the band
++# [2^N-q, 2^(N-1)-1] UNREACHABLE -> biased nonce -> private-key recovery
++# via the Hidden Number Problem (lattice) given a few hundred signatures.
++#
++# This PoC observes the ACTUAL signing nonce: _sign_setup leaves
++# kinv = k^-1 mod q on the key, so k = kinv^-1 mod q is recoverable after
++# each sign(). It signs many messages with one key, recovers each real
++# nonce, and counts how many land in the forbidden band.
++#
++#   biased  (unpatched): 0 nonces ever in the band  -> exit 0 (BUG PRESENT)
++#   uniform (patched)  : ~1/3 of nonces in the band -> exit 1 (FIXED)
++#
++# Container: localhost/dsa-compare (Crypt::DSA 1.21 + Math::BigInt::GMP).
++# Build: podman build -t localhost/dsa-compare -f containers/Containerfile.dsa-compare .
++# Run:   podman run --rm -v "$PWD/poc:/poc:ro,Z" localhost/dsa-compare perl /poc/poc-crypt-dsa-biased-nonce.pl
++use strict; use warnings; $| = 1;
++use Crypt::DSA;
++use Crypt::DSA::Util qw(mod_inverse bitsize);
++use Digest::SHA qw(sha1);
++use Math::BigInt lib => 'GMP';
++
++my $key = Crypt::DSA->new->keygen(Size => 1024);   # one long-lived signing key
++isa_ok($key, 'Crypt::DSA::Key');
++my $q   = $key->q;
++my $N   = bitsize($q);
++my $half  = Math::BigInt->new(2) ** ($N - 1);       # 2^(N-1)
++my $fb_lo = (Math::BigInt->new(2) ** $N) - $q;       # forbidden band start (2^N - q)
++my $fb_hi = $half - 1;                               # forbidden band end (2^(N-1)-1)
++
++my $dsa = Crypt::DSA->new;
++isa_ok($dsa, 'Crypt::DSA');
++
++my ($in_band, $samples) = (0, 4000);
++for my $i (1 .. $samples) {
++    $dsa->sign(Key => $key, Digest => sha1("message-$i"));
++    my $k = mod_inverse($key->kinv, $q);             # actual nonce used by this sign()
++    $in_band++ if $k >= $fb_lo && $k <= $fb_hi;
++}
++
++my $band_frac = ($fb_hi - $fb_lo + 1)->numify / $q->numify;   # expected fraction if uniform
++diag (sprintf("q: %d bits   forbidden-band fraction if uniform: ~%.2f\n", $N, $band_frac));
++diag (sprintf( "real signing nonces sampled: %d   landing in the (formerly forbidden) band: %d",
++    $samples, $in_band));
++
++ok($in_band != 0, sprintf("RESULT: nonce uniformly covers [1,q-1] (%.1f%% in band, ~%.0f%% expected) -> FIXED",
++        100 * $in_band / $samples, 100 * $band_frac));
++
++done_testing;

diff --git a/perl-Crypt-DSA.spec b/perl-Crypt-DSA.spec
index 5526c20..d7b15bc 100644
--- a/perl-Crypt-DSA.spec
+++ b/perl-Crypt-DSA.spec
@@ -1,7 +1,7 @@
 Summary:	Perl module for DSA signatures and key generation
 Name:		perl-Crypt-DSA
 Version:	1.17
-Release:	30%{?dist}
+Release:	31%{?dist}
 License:	GPL-1.0-or-later OR Artistic-1.0-Perl
 Url:		https://metacpan.org/release/Crypt-DSA
 Source0:	https://cpan.metacpan.org/modules/by-module/Crypt/Crypt-DSA-%{version}.tar.gz
@@ -10,6 +10,7 @@ Patch1:		Crypt-DSA-1.17-CVE-2026-8700.patch
 Patch2:		Crypt-DSA-1.17-CVE-2026-8704.patch
 Patch3:		Crypt-DSA-1.17-tidy.patch
 Patch4:		Crypt-DSA-1.17-CVE-2026-12205.patch
+Patch5:		Crypt-DSA-1.17-CVE-2026-14570.patch
 BuildArch:	noarch
 # Module Build
 BuildRequires:	coreutils
@@ -43,7 +44,7 @@ BuildRequires:	perl(strict)
 BuildRequires:	perl(Symbol)
 BuildRequires:	perl(vars)
 # Test Suite
-BuildRequires:	perl(Test::More) >= 0.42
+BuildRequires:	perl(Test::More) >= 0.88
 # Optional Tests
 BuildRequires:	perl(Crypt::DES_EDE3)
 # Extra Tests
@@ -98,6 +99,12 @@ sed -i -e '/^inc\// d' MANIFEST
 # Fix key material reuse for multiple signing events (CVE-2026-12205, CWE-323)
 %patch -P4
 
+# Hardening: Use a fresh, independent CSPRNG witness every round
+# Security fix: Modulo bias in key generation (CVE-2026-14570); an attack
+# with hundreds of signatures could lead to full private-key compromise;
+# keys should be considered compromised and new keys should be generated
+%patch -P5
+
 %build
 perl Makefile.PL INSTALLDIRS=vendor NO_PACKLIST=1 NO_PERLLOCAL=1
 %{make_build}
@@ -122,6 +129,12 @@ make test AUTOMATED_TESTING=1
 %{_mandir}/man3/Crypt::DSA::Util.3*
 
 %changelog
+* Fri Jul  3 2026 Paul Howarth <paul@city-fan.org> - 1.17-31
+- Hardening: Use a fresh, independent CSPRNG witness every round
+- Security fix: Modulo bias in key generation (CVE-2026-14570); an attack
+  with hundreds of signatures could lead to full private-key compromise;
+  keys should be considered compromised and new keys should be generated
+
 * Mon Jun 15 2026 Paul Howarth <paul@city-fan.org> - 1.17-30
 - Fix key material reuse for multiple signing events (CVE-2026-12205, CWE-323)
 

^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-07-03 18:08 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-07-03 18:08 [rpms/perl-Crypt-DSA] epel8: Fix for modulo bias in key generation (CVE-2026-14570) Paul Howarth

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox