From ab52b4c32c4dcff2ae767333dc77af1141ddf1c0 Mon Sep 17 00:00:00 2001 From: Naveed Khan Date: Fri, 2 Oct 2026 20:20:29 +0530 Subject: [PATCH] use Locale.ENGLISH for address folding in SignedMailValidator --- docs/releasenotes.md | 1 + .../smime/validator/SignedMailValidator.java | 8 +-- .../smime/test/SignedMailValidatorTest.java | 66 +++++++++++++++++++ 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/docs/releasenotes.md b/docs/releasenotes.md index 3983bd59b6..214f8dcdb5 100644 --- a/docs/releasenotes.md +++ b/docs/releasenotes.md @@ -63,6 +63,7 @@ Date: 2026, TBD - The ML-KEM KeyGenerator (KEMGenerateSpec/KEMExtractSpec), Cipher (wrap/unwrap), javax.crypto.KEM and KeyFactory.translateKey services accepted only BC's own ML-KEM key objects, and the KeyGenerator failed with a ClassCastException at generateKey() rather than at init, so an ML-KEM key from another provider could not be used with BC even though its standard encoding was one BC reads. This broke BCJSSE handshakes over the ML-KEM and hybrid groups whenever another provider ahead of BC decoded the peer's key or generated the ephemeral key pair. A foreign key is now converted from its X.509 or PKCS#8 encoding, with the usual parameter-set checks, and an unusable one is rejected at init (github #2466). - The raw JCA provider bounded the PBKDF2 iteration count taken from an encoding (org.bouncycastle.pbe.max_iteration_count, default 10,000,000) but not the counts of the legacy PBES1 (PKCS#5 scheme 1) and PKCS#12 PBE families beside it. Their AlgorithmParameters (PKCS12PBE and its OID aliases, PBKDF1) accepted any count, narrowing one beyond the int range with intValue() so that 2^32 arrived as 0, and every Cipher, Mac and SecretKeyFactory derivation ran with whatever count it was given - including a count decoded by another provider's AlgorithmParameters, as when javax.crypto.EncryptedPrivateKeyInfo.getKeySpec() decrypts a PKCS#12 PBE-protected key with BC. As these schemes carry the count in unauthenticated parameters and derive before anything can be checked, a supplied blob could hold a derivation for tens of minutes. The parameter parse now rejects a negative, beyond-int or over-limit count, and the derivations reject a negative or over-limit count, under the same property as PBKDF2. The PKCS#12 key store derives through the same code, so a org.bouncycastle.pkcs12.max_it_count raised above 10,000,000 now needs org.bouncycastle.pbe.max_iteration_count raised with it. - The light-weight CryptoProWrapEngine (RFC 4357 sec. 6.3) diversified the key encryption key in the caller's own array, so after init the KeyParameter it was given held the diversified key, and initialising again with the same parameters - to unwrap what had just been wrapped, say - diversified it a second time and used a different key. It also failed with a NullPointerException when given no S-box, although init has a branch for that case. It now diversifies a copy, and given no S-box uses the GOST 28147 engine's default S-box for the diversification, the one the wrap itself then uses. The provider's GOST 28147 key wrap ciphers were unaffected, as they always supply an S-box and build a new KeyParameter on every init. +- SignedMailValidator folded the signer certificate's email addresses and the message's From addresses with Locale.getDefault(), captured in a static field when the class was loaded, so the address comparison that decides whether a signature belongs to the sender the message claims answered differently depending on where the JVM was running. The Turkish and Azerbaijani rules fold 'I' (U+0049) to the dotless 'i' (U+0131) rather than to 'i', so on those locales the two sides disagree whenever only one of them spells the mailbox with an upper case I and a legitimately signed message is reported as SignedMailValidator.emailFromCertMismatch; in the other direction the subject's emailAddress attribute is read through ASN1String rather than being restricted to IA5String, so a value carrying 'I' with a combining dot above (U+0130) folds to a plain 'i' under those rules and a certificate issued for one address satisfies the check for another. All three foldings - the emailAddress attribute of the subject, the rfc822Name subject alternative names, and the From addresses they are compared against - now use Locale.ENGLISH, as every other case-folding comparison in the library does (the JSSE HostnameUtil, BCSNIHostName and DisabledAlgorithmConstraints use Locale.ENGLISH, the EST hostname authorizer the locale-independent Strings.toLowerCase). Behaviour on every other default locale is unchanged. ### 2.1.3 Additional Features and Functionality diff --git a/mail/src/main/java/org/bouncycastle/mail/smime/validator/SignedMailValidator.java b/mail/src/main/java/org/bouncycastle/mail/smime/validator/SignedMailValidator.java index 8742c43688..8ca3d18d16 100644 --- a/mail/src/main/java/org/bouncycastle/mail/smime/validator/SignedMailValidator.java +++ b/mail/src/main/java/org/bouncycastle/mail/smime/validator/SignedMailValidator.java @@ -84,8 +84,6 @@ public class SignedMailValidator private static final int KU_DIGITAL_SIGNATURE = 0; private static final int KU_NON_REPUDIATION = 1; - private static final Locale locale = Locale.getDefault(); - private CertStore certs; private SignerInformationStore signers; @@ -423,7 +421,7 @@ public static Set getEmailAddresses(X509Certificate cert) throws IOException, Ce { if (PKCSObjectIdentifiers.pkcs_9_at_emailAddress.equals(atVs[j].getType())) { - String email = ((ASN1String)atVs[j].getValue()).getString().toLowerCase(locale); + String email = ((ASN1String)atVs[j].getValue()).getString().toLowerCase(Locale.ENGLISH); addresses.add(email); } } @@ -440,7 +438,7 @@ public static Set getEmailAddresses(X509Certificate cert) throws IOException, Ce GeneralName name = names[i]; if (name.getTagNo() == GeneralName.rfc822Name) { - String email = ASN1IA5String.getInstance(name.getName()).getString().toLowerCase(locale); + String email = ASN1IA5String.getInstance(name.getName()).getString().toLowerCase(Locale.ENGLISH); addresses.add(email); } } @@ -542,7 +540,7 @@ static boolean hasAnyFromAddress(Set certEmails, String[] fromAddresses) // check if email in cert is equal to the from address in the message for (int i = 0; i < fromAddresses.length; ++i) { - if (certEmails.contains(fromAddresses[i].toLowerCase(locale))) + if (certEmails.contains(fromAddresses[i].toLowerCase(Locale.ENGLISH))) { return true; } diff --git a/mail/src/test/java/org/bouncycastle/mail/smime/test/SignedMailValidatorTest.java b/mail/src/test/java/org/bouncycastle/mail/smime/test/SignedMailValidatorTest.java index f08f62203c..ba7da6bf12 100644 --- a/mail/src/test/java/org/bouncycastle/mail/smime/test/SignedMailValidatorTest.java +++ b/mail/src/test/java/org/bouncycastle/mail/smime/test/SignedMailValidatorTest.java @@ -164,6 +164,72 @@ public void testMultiEmail() throws Exception assertTrue(addresses.contains("domain-confidentiality-authority@bekb.ch ")); } + public void testEmailAddressFoldingIsLocaleIndependent() throws Exception + { + // The Turkish and Azerbaijani rules fold 'I' (U+0049) to the dotless 'i' (U+0131), so a + // comparison that folds with the default locale answers differently depending on where the + // JVM runs. Note the folding locale used to be captured in a static field at class load, so + // reproducing the original defect needs -Duser.language=tr -Duser.country=TR at JVM start + // as well; setting it here catches a return to a dynamically read default locale. + Locale defaultLocale = Locale.getDefault(); + + try + { + Locale.setDefault(new Locale("tr", "TR")); + + long now = System.currentTimeMillis(); + long day = 1000L * 60 * 60 * 24; + Date notBefore = new Date(now - day); + Date notAfter = new Date(now + 100 * day); + + String caDN = "CN=Test CA, O=Bouncy Castle, C=AU"; + String signDN = "CN=Ian Echidna, E=IAN@EXAMPLE.ORG, O=Bouncy Castle, C=AU"; + + KeyPair caKP = CMSTestUtil.makeKeyPair(); + KeyPair signKP = CMSTestUtil.makeKeyPair(); + + X509Certificate caCert = buildCert(caDN, caKP.getPublic(), caDN, caKP, notBefore, notAfter, true); + X509Certificate signCert = buildCert(signDN, signKP.getPublic(), caDN, caKP, notBefore, notAfter, false); + + Set addresses = SignedMailValidator.getEmailAddresses(signCert); + + assertTrue("upper case I must fold to the ASCII i", addresses.contains("ian@example.org")); + assertFalse("upper case I must not fold to the dotless i", + addresses.contains("\u0131an@example.org")); + + List certList = new ArrayList(); + certList.add(signCert); + certList.add(caCert); + + SMIMESignedGenerator gen = new SMIMESignedGenerator(); + gen.addSignerInfoGenerator(new JcaSimpleSignerInfoGeneratorBuilder().setProvider("BC") + .build("SHA256withRSA", signKP.getPrivate(), signCert)); + gen.addCertificates(new JcaCertStore(certList)); + + MimeMultipart signedMsg = gen.generate(SMIMETestUtil.makeMimeBodyPart("Hello world!\n")); + + Session session = Session.getDefaultInstance(System.getProperties(), null); + MimeMessage msg = new MimeMessage(session); + msg.setFrom(new InternetAddress("ian@example.org")); + msg.setRecipient(Message.RecipientType.TO, new InternetAddress("example@bouncycastle.org")); + msg.setContent(signedMsg, signedMsg.getContentType()); + msg.saveChanges(); + + Set trust = new HashSet(); + trust.add(new TrustAnchor(caCert, null)); + + PKIXParameters params = new PKIXParameters(trust); + params.setRevocationEnabled(false); + + assertTrue("From address must match the certificate address whatever the default locale", + firstResult(new SignedMailValidator(msg, params)).isValidSignature()); + } + finally + { + Locale.setDefault(defaultLocale); + } + } + public void testExtKeyUsage() throws Exception { String message = "validator.extKeyUsage.eml";