From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id A29411FF0AF for ; Fri, 23 Oct 2026 20:58:09 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 3AA5C21635; Fri, 23 Oct 2026 20:58:04 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=wolfssl-com.20251104.gappssmtp.com; s=20251104; t=1792781871; x=1793386671; darn=lists.proxmox.com; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to:content-type; bh=UGda+IafnNgfeulOVGxbwOxvWaW8nxiK71cEtq5VudA=; b=P31ka5x6AZJexlbQXtbFGh9R4Uh8pH4Ckosq2hQkBytE+oSvYWc/oftnf/l2ZsCKg+ h1knYvzX5p/KWZjrJzTX4Ktlm3US41XHjPS35LD8KXKbilPaqetbke5zdUzLEEz1Jboj eAXNd3nmVWvDd0zjjG0sBl/CzNM23X4NT3IxtdyaW//UGdPmTYZkQNkWNgWWf6N3B09y RvL5tbAWPVuvneBwBtW5mazBcv0yI7WYc5toFGtIPOPMBEOGBQ8PjcyMlxd1aWoSTaS2 App9AsAAEaWnFLGsFWV5O2npaoW1/5XPq5lloXWRbLeFuH7jM/NQlcK36VmHq2vxzXQc cX5Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792781871; x=1793386671; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to:content-type; bh=UGda+IafnNgfeulOVGxbwOxvWaW8nxiK71cEtq5VudA=; b=EPlAl8rFYlmL2qrRq8VIlfYhlwRGz5zoLmdL8IQ6LmsgEz9LHiI4Fvk8djFzK3c5mh HfAfy6MFnj5XLZJWBufRrEDPHxKATGlMAEWBvZ7OLkh7JqHmna5dDlmEFxnHPvPDuLzd T6bQWHydIsRvksXy+PmzDMdK5Yid6X8atN7NsWnZTpQzlCRdKeOKks4zfw8sVLvzEOZB cnTUQkccw5N87LVkirgq0qyuxMNm1mek/MMV/ca1MyHgGDfwkBCeoi6ov0LPKTjfjt6O pzdsrYq6xYqg4wRV2Ua7FI6RU7fhTbdVyU/dYzc6v4bjGQe8Ft8Kso4UTITqash18zln c15w== X-Gm-Message-State: AFq9FYI+uiNDdw0EOyO5OLaLJHvt9DOkWdCDJWDpgNJAhCfFKk09GlE3 JPyUPRG4v5eMMpmScW+L7qV33Dt1+STg+B/3nkdow8ypdzDJU6+Dm7GIRm1M6txkRsIGTltjITd Z0hgI7WU= X-Gm-Gg: AYBFou3Cy+eiEY3VKVFmXeJS2j4R/nEJIDxTAVDGCYj4iSLD2sxv6GNTMx+Cp/axE/+ l/KQqGhXGMgVdN7+p2Nc5DHDEA6kTcPm4EvaV8gcKkeEEUhRKUPkUSEAt2skq5IYYohGaCqhjhT NAATEbYv0prmjl7nNueLcniQDsBiLvXpo/4gbx/vfrZKwettGlo75RMJTxk4GZvSVlFoI/OwL/W u0JXCYbJNAa/DNNjRwDUTmLHVTPVdREC9gJH3LvOuxwFnS3Z/SnRt+bRFbGgSWS27DV4+TwhyZw 5TKSl1KqRGh4gWTlYoELeZVYMwr5JgFEu0DHsI55ZBzafe1AlsS8DvXHNrx+3WaFzxXA5BuOFut Q59j6RroVVvDBexPB5HsgQdMskAlWf2MnfsrBmqsjnATraOczJS4orUfsJgt5uiiiTaJPg7PH3E QjpGVvbcFOnTRwmJcvuFfgZaUk/RgqKt6yJsAgzyyKEcbM01xmItSsejesljOhuE7rg+uMu8rtT dQGF0N5ELFbNxqGAramGqaKfEM8PwGqPM3CV1V3Sc5tDDOIQ6gb2TlsGN4X73EBbc/D+wHadEEM Hf8TFhxD3PPiQIGYQbbdrv/GKMq/z1TuIeFwpetpzkj+aR24nk58i5YyZDTU+ArIJYFoj3r/ubx hHc5G6P+WlBEgJp+g6dcpGhBzT7tcI0G7z3gVx+pC8LJEYrb4IUmtehUmWhQoJ2SDJcCUlww= X-Received: by 2002:a17:90b:3c84:b0:3ab:188b:c5b1 with SMTP id 98e67ed59e1d1-3adeb11f323mr117077a91.56.1792781871157; Fri, 23 Oct 2026 11:57:51 -0700 (PDT) From: Mark Atwood To: pve-devel@lists.proxmox.com Subject: [PATCH common 1/2] ticket: accept RSA tickets signed with SHA-256 Date: Fri, 23 Oct 2026 11:57:48 -0700 Message-ID: <20261023185749.509347-1-mark@wolfssl.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: References: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.001 Adjusted score from AWL reputation of From: address DKIM_SIGNED 0.1 Message has a DKIM or DK signature, not necessarily valid DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature DMARC_PASS -0.1 DMARC pass policy KAM_SHORT 0.001 Use of a URL Shortener for very short URL RCVD_IN_DNSWL_NONE -0.0001 Sender listed at https://www.dnswl.org/, no trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: B2EGZXX4X5W4SHDQY7KFHDVPC3GTS5M6 X-Message-ID-Hash: B2EGZXX4X5W4SHDQY7KFHDVPC3GTS5M6 X-MailFrom: mark@wolfssl.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Crypt::OpenSSL::RSA signs and verifies with SHA-1 unless told otherwise, so all RSA tickets (login, TFA, VNC, tunnel) are RSA PKCS#1 v1.5 signatures over SHA-1. NIST SP 800-131A Rev. 2 no longer allows SHA-1 for creating digital signatures. FIPS providers for OpenSSL 3 can enforce this, for example wolfProvider, or the OpenSSL FIPS provider with its signature digest check enabled. With such a provider no ticket can be issued and nobody can log in. As a first step, make verification accept tickets signed with SHA-256 in addition to SHA-1. Once all nodes of a cluster run a version with this change, the signing side can switch to SHA-256 without locking users out of nodes that were not yet upgraded. The hash is set explicitly before each attempt, since callers cache and reuse the key object. A provider that does not allow SHA-1 raises an error from verify() instead of returning false, so the call is wrapped in eval and such a ticket is treated like any other invalid ticket. Add a test that signs tickets directly with Crypt::OpenSSL::RSA using either hash, and checks that they are accepted and that modified, expired, foreign-key or wrong-secret tickets are rejected. Signed-off-by: Mark Atwood --- src/PVE/Ticket.pm | 15 +++++++++- test/Makefile | 1 + test/ticket_test.pl | 68 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100755 test/ticket_test.pl diff --git a/src/PVE/Ticket.pm b/src/PVE/Ticket.pm index 764c02d..f700f39 100644 --- a/src/PVE/Ticket.pm +++ b/src/PVE/Ticket.pm @@ -79,8 +79,21 @@ sub verify_rsa_ticket { my $sig = $2; my $full = defined($secret_data) ? "$plain:$secret_data" : $plain; + my $raw_sig = decode_base64($sig); + + # Older versions sign with SHA-1, so during a rolling upgrade a cluster has tickets + # with either hash. A provider that refuses SHA-1 raises an error rather than + # returning false, which simply makes such a ticket invalid. + my $valid; + for my $use_hash ('use_sha256_hash', 'use_sha1_hash') { + $valid = eval { + $rsa_pub->$use_hash(); + $rsa_pub->verify($full, $raw_sig); + }; + last if $valid; + } - if ($rsa_pub->verify($full, decode_base64($sig))) { + if ($valid) { if ($plain =~ m/^\Q$prefix\E:(?:(\S+):)?([A-Z0-9]{8})$/) { my $data = $1; # Note: not all tickets contains data my $timestamp = $2; diff --git a/test/Makefile b/test/Makefile index 1bce185..7eac9ea 100644 --- a/test/Makefile +++ b/test/Makefile @@ -16,6 +16,7 @@ TESTS = lock_file.test \ section_config_property_isolation_test.test \ file-test.test \ cmd-test.test \ + ticket_test.test \ all: diff --git a/test/ticket_test.pl b/test/ticket_test.pl new file mode 100755 index 0000000..37e30b4 --- /dev/null +++ b/test/ticket_test.pl @@ -0,0 +1,68 @@ +#!/usr/bin/perl + +# Tests for signing and verifying RSA tickets with PVE::Ticket + +use v5.36; + +use lib '../src'; + +use Crypt::OpenSSL::RSA; +use MIME::Base64; +use Test::More; + +use PVE::Ticket; + +my $rsa_priv = Crypt::OpenSSL::RSA->generate_key(2048); +my $rsa_pub = Crypt::OpenSSL::RSA->new_public_key($rsa_priv->get_public_key_string()); + +# sign like a node would, with an explicitly chosen hash, independent of PVE::Ticket +sub sign_ticket($priv, $hash, $plain, $secret_data = undef) { + my $full = defined($secret_data) ? "$plain:$secret_data" : $plain; + my $use_hash = "use_${hash}_hash"; + $priv->$use_hash(); + return $plain . "::" . encode_base64($priv->sign($full), ''); +} + +my $now = sprintf("%08X", time()); +my $old = sprintf("%08X", time() - 7200); + +for my $hash (qw(sha256 sha1)) { + my $ticket = sign_ticket($rsa_priv, $hash, "PVE:root\@pam:$now"); + my ($data, $age) = PVE::Ticket::verify_rsa_ticket($rsa_pub, 'PVE', $ticket, undef, -1, 60, 1); + is($data, 'root@pam', "$hash signed ticket is accepted"); + ok(defined($age) && $age >= 0 && $age < 60, "$hash signed ticket has plausible age"); + + $ticket = sign_ticket($rsa_priv, $hash, "PVEVNC:$now", 'vnc-secret'); + ok( + PVE::Ticket::verify_rsa_ticket($rsa_pub, 'PVEVNC', $ticket, 'vnc-secret', -20, 40, 1), + "$hash signed ticket with secret data is accepted", + ); + ok( + !PVE::Ticket::verify_rsa_ticket($rsa_pub, 'PVEVNC', $ticket, 'other', -20, 40, 1), + "$hash signed ticket with wrong secret data is rejected", + ); + + $ticket = sign_ticket($rsa_priv, $hash, "PVE:root\@pam:$now"); + (my $tampered = $ticket) =~ s/^PVE:root\@pam:/PVE:admin\@pam:/; + ok( + !PVE::Ticket::verify_rsa_ticket($rsa_pub, 'PVE', $tampered, undef, -1, 60, 1), + "$hash signed ticket with modified data is rejected", + ); + eval { PVE::Ticket::verify_rsa_ticket($rsa_pub, 'PVE', $tampered, undef, -1, 60) }; + like($@, qr/invalid PVE ticket/, "$hash signed modified ticket raises error without noerr"); + + $ticket = sign_ticket($rsa_priv, $hash, "PVE:root\@pam:$old"); + ok( + !PVE::Ticket::verify_rsa_ticket($rsa_pub, 'PVE', $ticket, undef, -1, 60, 1), + "$hash signed expired ticket is rejected", + ); + + my $other_priv = Crypt::OpenSSL::RSA->generate_key(2048); + $ticket = sign_ticket($other_priv, $hash, "PVE:root\@pam:$now"); + ok( + !PVE::Ticket::verify_rsa_ticket($rsa_pub, 'PVE', $ticket, undef, -1, 60, 1), + "$hash signed ticket from another key is rejected", + ); +} + +done_testing(); -- 2.55.0