public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Paul Howarth <paul@city-fan.org>
To: git-commits@fedoraproject.org
Subject: [rpms/perl-Crypt-DSA] epel9: Fix for modulo bias in key generation (CVE-2026-14570)
Date: Fri, 03 Jul 2026 17:56:01 GMT [thread overview]
Message-ID: <178310136128.1.7693100379933029590.rpms-perl-Crypt-DSA-16a6542943a0@fedoraproject.org> (raw)
A new commit has been pushed.
Repo : rpms/perl-Crypt-DSA
Branch : epel9
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=epel9
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)
reply other threads:[~2026-07-03 17:56 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=178310136128.1.7693100379933029590.rpms-perl-Crypt-DSA-16a6542943a0@fedoraproject.org \
--to=paul@city-fan.org \
--cc=git-commits@fedoraproject.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox