#1142266 ca-certificates-java: doesn't refresh /etc/ssl/certs/java/cacerts on upgrade from bookworm #1142266
- Package:
- ca-certificates-java
- Source:
- ca-certificates-java
- Submitter:
- Cyril Brulebois
- Date:
- 2026-10-22 07:29:01 UTC
- Severity:
- normal
- Tags:
Package: ca-certificates-java
Version: 20240118
Severity: serious
Tags: security
Justification: obsolete root CAs for Java applications
X-Debbugs-Cc: Debian Security Team <team@security.debian.org>
Hi,
I'm helping a number of customers upgrade their Debian systems from
stable releases to stable releases, and I have a compare-etc script that
checks how /etc compare between a fresh Debian 13 system and a system
upgraded from Debian 12.
On the ca-certificates side, everything looks good: ca-certificates.crt
is identical (modulo the order in which certificates appear).
On the ca-certificates-java side, that's not the case: cacerts has been
left unchanged by the upgrade.
This doesn't appear to be specific to those specific systems, as I'm
able to reproduce the bug with a fresh bookworm debootstrap, installing
ca-certificates{,-java}, switching apt configuration to trixie, and
upgrading both packages. The cacerts file is left unchanged, and
comparing the certificates between upgraded bookworm and fresh trixie
gives the same number of additions/deletions as reported by
ca-certificates during the upgrade.
To reproduce:
- install bookworm:
cd /tmp
sudo debootstrap bookworm bookworm
sudo mount --bind /proc bookworm/proc
sudo chroot bookworm apt-get install -y default-jre ca-certificates-java
- partial upgrade from bookworm to trixie:
sudo sed s/bookworm/trixie/ -i bookworm/etc/apt/sources.list
sudo chroot bookworm apt-get update
sudo chroot bookworm apt-get install -y ca-certificates ca-certificates-java
- upgrade output:
Preparing to unpack .../ca-certificates_20250419_all.deb ...
Unpacking ca-certificates (20250419) over (20230311+deb12u1) ...
Preparing to unpack .../ca-certificates-java_20240118_all.deb ...
Unpacking ca-certificates-java (20240118) over (20230710~deb12u1) ...
Setting up ca-certificates (20250419) ...
Updating certificates in /etc/ssl/certs...
rehash: warning: skipping ca-certificates.crt,it does not contain exactly one certificate or CRL
21 added, 13 removed; done.
Setting up ca-certificates-java (20240118) ...
done.
Processing triggers for ca-certificates (20250419) ...
Updating certificates in /etc/ssl/certs...
0 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.
Processing triggers for ca-certificates-java (20240118) ...
done.
- install trixie:
cd /tmp
sudo debootstrap trixie trixie
sudo mount --bind /proc trixie/proc
sudo chroot trixie apt-get install -y default-jre ca-certificates-java
To compare:
echo changeit | sudo chroot bookworm keytool -list -keystore /etc/ssl/certs/java/cacerts 2>/dev/null | grep ^Certificate | sort > bookworm.txt
echo changeit | sudo chroot trixie keytool -list -keystore /etc/ssl/certs/java/cacerts 2>/dev/null | grep ^Certificate | sort > trixie.txt
diff -u bookworm.txt trixie.txt|tail -n +3|grep -c ^+
diff -u bookworm.txt trixie.txt|tail -n +3|grep -c ^-
which yields 21 additions and 13 deletions, which matches what's
happening at the ca-certificates level when upgrading.
According to my notes, we already had to implement a workaround when
upgrading from Debian 10 to Debian 11 (tracked as #922981), but that
wasn't the case when upgrading from Debian 11 to Debian 12.
I haven't tried to understand exactly what's happening here yet, the
levels of indirection are a little intimidating…
Cheers,
Cyril Brulebois <cyril@debamax.com> (2026-07-17): […] Initial investigation ===================== - postinst enables “fresh” on initial installation and also when upgrading from specific, very old versions; that's definitely not the case here. - update-ca-certificates isn't called with --fresh during the upgrade; - therefore I'm quite certain there's no chance to enter the first “then” of this block: https://sources.debian.org/src/ca-certificates-java/20240118/debian/ca-certificates-java.postinst#L119-L123 - in the context of an upgrade, it would make sense to go through a to-do list left behind by the hook, which would be the “pending” file as I understand it, and that'd go through the second “then” of the aforementioned block. Now, I don't have any definitive proof yet, but seeing how the upgrade output gives two update runs (cherry-picking lines from the log above): Updating certificates in /etc/ssl/certs... 21 added, 13 removed; done. […] Updating certificates in /etc/ssl/certs... 0 added, 0 removed; done. for a single hook run, after the second update run: Running hooks in /etc/ca-certificates/update.d... It seems safe to assume this hook run doesn't see any changes, and leaves an empty (or maybe missing) “pending” file. If that hook had run twice, the first run might have generated an interesting “pending” file (with the additions/deletions), while the second one would have been a no-op (the hook uses >> to append to the pending file, it doesn't start from scratch, so a second hook run wouldn't reset what was generated by the first one). Redoing the upgrade under strace confirms we go through all of the update_certs() function twice, with the hook running in-between, creating an empty “pending” file via: cat - >> /var/lib/ca-certificates-java/pending which is confirmed by the very few syscalls performed by PID 74084 (not pictured below), and by st_size=0 on the last newfstatat() call: 73273 newfstatat(AT_FDCWD, "/var/lib/ca-certificates-java/convert_pkcs12_keystore_to_jks", 0x7ffdf65011b0, 0) = -1 ENOENT (No such file or directory) 73273 newfstatat(AT_FDCWD, "/var/lib/ca-certificates-java/fresh", 0x7ffdf6501200, 0) = -1 ENOENT (No such file or directory) 73273 newfstatat(AT_FDCWD, "/var/lib/ca-certificates-java/fresh", 0x7ffdf6501250, 0) = -1 ENOENT (No such file or directory) 73273 newfstatat(AT_FDCWD, "/var/lib/ca-certificates-java/pending", 0x7ffdf6501200, 0) = -1 ENOENT (No such file or directory) 73297 newfstatat(AT_FDCWD, "/var/lib/ca-certificates-java/fresh", 0x55e4ea327c70, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory) 73298 newfstatat(AT_FDCWD, "/var/lib/ca-certificates-java/pending", 0x562f3f97fc70, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory) 74081 openat(AT_FDCWD, "/var/lib/ca-certificates-java/pending", O_WRONLY|O_CREAT|O_APPEND, 0666) = 3 74081 fcntl(1, F_DUPFD, 10) = 11 74081 close(1) = 0 74081 fcntl(11, F_SETFD, FD_CLOEXEC) = 0 74081 dup2(3, 1) = 1 74081 close(3) = 0 […] 74084 execve("/usr/bin/cat", ["cat", "-"] […] 74089 newfstatat(AT_FDCWD, "/var/lib/ca-certificates-java/convert_pkcs12_keystore_to_jks", 0x7ffe43dfba90, 0) = -1 ENOENT (No such file or directory) 74089 newfstatat(AT_FDCWD, "/var/lib/ca-certificates-java/fresh", 0x7ffe43dfbae0, 0) = -1 ENOENT (No such file or directory) 74089 newfstatat(AT_FDCWD, "/var/lib/ca-certificates-java/fresh", 0x7ffe43dfbb30, 0) = -1 ENOENT (No such file or directory) 74089 newfstatat(AT_FDCWD, "/var/lib/ca-certificates-java/pending", {st_dev=makedev(0xfd, 0x1), st_ino=2363271, st_mode=S_IFREG|0644, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=0, st_size=0, st_atime=1784260504 /* 2026-07-17T05:55:04.578209913+0200 */, st_atime_nsec=578209913, st_mtime=1784260504 /* 2026-07-17T05:55:04.578209913+0200 */, st_mtime_nsec=578209913, st_ctime=1784260504 /* 2026-07-17T05:55:04.578209913+0200 */, st_ctime_nsec=578209913}, 0) = 0 https://sources.debian.org/src/ca-certificates-java/20240118/debian/ca-certificates-java.postinst#L62-L128 All of this confirms why we end up with a no-op regarding the cacerts file. I'm not sure what a fix would look like. Running hooks after each update might be risky, because it could lead to trigger cycles or something? Possible workaround =================== In the meanwhile, `update-ca-certificates --fresh` might be tempting, but it's not sufficient: it only ensures all current certificates get added to cacerts, it doesn't reflect deletions. Removing the cacerts file beforehands makes it possible to finally get in sync with what a fresh trixie system looks like: rm -f /etc/ssl/certs/java/cacerts update-ca-certificates --fresh The 13 deletions, resulting in 149 entries total: -debian:autoridad_de_certificacion_firmaprofesional_cif_a62634068_2.pem, Jul 17, 2026, trustedCertEntry, -debian:entrust_root_certification_authority_-_g4.pem, Jul 17, 2026, trustedCertEntry, -debian:e-tugra_certification_authority.pem, Jul 17, 2026, trustedCertEntry, -debian:e-tugra_global_root_ca_ecc_v3.pem, Jul 17, 2026, trustedCertEntry, -debian:e-tugra_global_root_ca_rsa_v3.pem, Jul 17, 2026, trustedCertEntry, -debian:hongkong_post_root_ca_1.pem, Jul 17, 2026, trustedCertEntry, -debian:securesign_rootca11.pem, Jul 17, 2026, trustedCertEntry, -debian:security_communication_rootca3.pem, Jul 17, 2026, trustedCertEntry, -debian:security_communication_root_ca.pem, Jul 17, 2026, trustedCertEntry, -debian:swisssign_silver_ca_-_g2.pem, Jul 17, 2026, trustedCertEntry, -debian:trustcor_eca-1.pem, Jul 17, 2026, trustedCertEntry, -debian:trustcor_rootcert_ca-1.pem, Jul 17, 2026, trustedCertEntry, -debian:trustcor_rootcert_ca-2.pem, Jul 17, 2026, trustedCertEntry, (out of 150 because of some encoding fun, but that's another story: /etc/ssl/certs/NetLock_Arany_=Class_Gold=_F?tan?s?tv?ny.pem) Cheers,
Hi,
Some additional data on this bug, plus root causes for why the
`update-ca-certificates --fresh` workaround does not reflect deletions.
1. Impact: the eMudhra intermediate compromise (CVE-2026-91403)
================================================================
On Ubuntu 24.04 (ca-certificates 20260816ubuntu0.24.04.1,
ca-certificates-java 20240118), the security update that deselects
"emSign ECC Root CA - G3" and "Comodo AAA Services root" was applied by
unattended-upgrades. /etc/ssl/certs was updated correctly ("4 added,
5 removed"), but /etc/ssl/certs/java/cacerts was left untouched (mtime
unchanged since the package was first installed). Java clients on that
host kept trusting the compromised chains; curl did not:
curl https://dv-g3b-emudhra.we-have-a-problem.com/ -> rejected
java (system JRE, same host, same URL) -> trusted
The same happened on the previous ca-certificates upgrade on that host
(20240203 -> 20260601~24.04.1), so every distrust since installation was
missed.
2. Upgrade path (the original report): still present on sid
===========================================================
This is the same root cause Arnaud analysed in #1069251 (now assigned to
src:ca-certificates). Reproduced in a debian:sid container with
ca-certificates 20260816+nmu1 and ca-certificates-java 20260311 (script
attached): ca-certificates.postinst runs
`update-ca-certificates --hooksdir ""` first, which removes the symlink
without running hooks, and then the deferred trigger run sees
"0 added, 0 removed" and hands an empty list to the jks-keystore hook.
To confirm that the pre-run is the cause (not proposed as the fix),
replacing the hook-less pre-run with a no-op makes the deselected root
disappear from cacerts on the same upgrade path.
A real fix probably needs the hook-less run to preserve its
ADDED/REMOVED lists for the deferred run, or the deferred run to
reconcile cacerts against /etc/ssl/certs instead of relying on a diff.
3. `--fresh` does not reach the hook
====================================
update-ca-certificates never passes --fresh to the hooks:
( cat "$ADDED"
cat "$REMOVED" ) | "$hook" || echo "E: $hook exited with code $?."
so /etc/ca-certificates/update.d/jks-keystore always takes the `*)` branch
and fires `update-ca-certificates-java` rather than
`update-ca-certificates-java-fresh`. With --fresh, update-ca-certificates
first clears all symlinks and recreates them, so the hook receives only
"+" lines and nothing is ever removed from cacerts. That explains why
`update-ca-certificates --fresh` alone is not a workaround.
Passing --fresh through:
( cat "$ADDED"
cat "$REMOVED" ) | "$hook" $( [ "$fresh" = 1 ] && echo --fresh ) || ...
makes the deselected root disappear, but only under an English locale,
because of the next issue.
4. The "fresh" reconciliation is locale-dependent
=================================================
ca-certificates-java.postinst sets only LC_CTYPE=C.UTF-8, and then parses
the existing aliases from translated keytool output:
cacerts_aliases=$(keytool ${castore} -storepass "$storepass" -list -rfc \
| sed -n 's/^Alias name: *debian://ip' | tr '\n' ' ')
keytool follows LC_MESSAGES/LANG for its output ("별칭 이름:" in Korean,
"Aliasname:" in German), so cacerts_aliases is empty and no "-" lines are
generated. Same environment the postinst uses, on sid:
LANG=C.UTF-8 LC_CTYPE=C.UTF-8 -> 120 aliases parsed
LANG=ko_KR.UTF-8 LC_CTYPE=C.UTF-8 -> 0
LANG=de_DE.UTF-8 LC_CTYPE=C.UTF-8 -> 0
Suggested fix:
cacerts_aliases=$(LC_ALL=C.UTF-8 keytool ${castore} ...
(or export LC_ALL=C.UTF-8 at the top of the postinst instead of
LC_CTYPE only).
5. Results (debian:sid, ISRG Root X1 deselected, then checked in cacerts)
=========================================================================
C.UTF-8 ko_KR de_DE
package upgrade path, unpatched present present present
package upgrade path, no pre-run (*) absent absent absent
update-ca-certificates --fresh present present present
--fresh + hook receives --fresh absent present present
--fresh + hook --fresh + LC_ALL fix absent absent absent
(*) diagnostic only, see section 2
Items 2 and 3 are in src:ca-certificates (postinst and
update-ca-certificates), item 4 is in src:ca-certificates-java, hence
the "affects".
Workaround for affected systems, which does not depend on the locale
because the keystore is rebuilt from scratch (as Cyril found):
rm -f /etc/ssl/certs/java/cacerts
update-ca-certificates --fresh
The reproducer script is attached.
Thanks,
Kamilake