Skip to content

Commit 5f7bc5e

Browse files
rootvector2dghgit
authored andcommitted
SignedMailValidator now compares the certificate and From addresses lower cased in ASCII only, independently of the default locale, incorporating github PR #2485.
1 parent bbf7cc5 commit 5f7bc5e

4 files changed

Lines changed: 113 additions & 9 deletions

File tree

‎CONTRIBUTORS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@ We also wish to acknowledge financial and collaborative support from [CISCO](htt
532532
- subbudvk \<https://github.com/subbudvk\> - initial author on S2K parser hardening work for OpenPGP API.
533533
- mkarasik \<https://github.com/mkarasik\> - initial work on EST server-side key generation (RFC 7030 4.4).
534534
- Bernd Prünster (A-SIT Plus) \<bernd.pruenster&#064;a-sit.at\> - reported lenient ASN.1 UTCTime/GeneralizedTime parsing accepting structurally malformed content, with fuzzing-derived test cases. Additional fuzzing around date validity as well.
535-
- Naveed Khan \<https://github.com/rootvector2\> - constant time comparison of membership and confirmation tags in the MLS API. Reported unbounded array allocation in the BKS/UBER keystore load path (OutOfMemoryError DoS from a crafted keystore) and introduction of a capped decompression limit in PGPCompressedData.getDataStream(), both with POC. Original submission creating S/MIME backing temp files with owner-only permissions (PR #2326). Rejecting empty sequences in the X.509 extension parsers whose RFC 5280 syntax is SEQUENCE SIZE (1..MAX) - CRLDistPoint, CertificatePolicies, ExtendedKeyUsage, PolicyMappings, and SubjectDirectoryAttributes (PR #2331). Hardening asn1.cms.SignerInfo decode to reject a non-INTEGER version or non-tagged unsignedAttrs via getInstance rather than leaking a ClassCastException (PR #2342). Fixing an off-by-4 header-length guard in the OpenPGP NotationData signature subpacket parser (PR #2346). Rejecting CR/LF in the S/MIME streaming writer header names and values (SMIMEEnvelopedWriter/SMIMESignedWriter withHeader) to prevent MIME header injection (PR #2348). Adding minimum-length guards to the SM2 decrypt and GOST28147/DSTU7624/DESede/RC2 key-wrap unwrap paths (PR #2359). Guarding the ECDH session-key length in the JCE OpenPGP decryptor (PR #2383). Fixing the identity fast-path type guards in the OER getInstance factories and the transposed isInstance arguments in OEROptional.getObject (PR #2373). Requiring the signature handed to AIMerSigner.verifySignature to be exactly the parameter set's signature size (PR #2401). Constant time comparison of the SRP-6a evidence messages (PR #2406). Surfacing a malformed TSTInfo in a time-stamp token as TSPException rather than an unchecked exception out of the TimeStampResponse constructors (PR #2415). Rejecting a PKCS#12 PBMAC1 KDF keyLength below the RFC 9579 sec. 9 floor of 20 octets (PR #2431). Initial implementation of the empty-label guard on name-constraint host matching, having found that the canonicalisation removed only one RFC 1034 root-label dot, so a dNSName, rfc822Name host or URI host carrying more than one matched no constraint at all (PR #2436). Initial implementation of the lookup of indirect CRL entries by issuer and serial number, having found that the certification path validators compared only the issuer of the first entry carrying a certificate's serial number (PR #2464). Initial implementation of the malformed-input handling of the OCSPResp and OCSPReq parsers (PR #2468). Rejecting short sequences in the CMS content-type decoders SignedData, EnvelopedData, AuthenticatedData, AuthEnvelopedData and EncryptedData (PR #2469). Finding the Hashtable.contains() guard on the CMS algorithm protection attribute in the signed and authenticated attribute table generators (PR #2480).
535+
- Naveed Khan \<https://github.com/rootvector2\> - constant time comparison of membership and confirmation tags in the MLS API. Reported unbounded array allocation in the BKS/UBER keystore load path (OutOfMemoryError DoS from a crafted keystore) and introduction of a capped decompression limit in PGPCompressedData.getDataStream(), both with POC. Original submission creating S/MIME backing temp files with owner-only permissions (PR #2326). Rejecting empty sequences in the X.509 extension parsers whose RFC 5280 syntax is SEQUENCE SIZE (1..MAX) - CRLDistPoint, CertificatePolicies, ExtendedKeyUsage, PolicyMappings, and SubjectDirectoryAttributes (PR #2331). Hardening asn1.cms.SignerInfo decode to reject a non-INTEGER version or non-tagged unsignedAttrs via getInstance rather than leaking a ClassCastException (PR #2342). Fixing an off-by-4 header-length guard in the OpenPGP NotationData signature subpacket parser (PR #2346). Rejecting CR/LF in the S/MIME streaming writer header names and values (SMIMEEnvelopedWriter/SMIMESignedWriter withHeader) to prevent MIME header injection (PR #2348). Adding minimum-length guards to the SM2 decrypt and GOST28147/DSTU7624/DESede/RC2 key-wrap unwrap paths (PR #2359). Guarding the ECDH session-key length in the JCE OpenPGP decryptor (PR #2383). Fixing the identity fast-path type guards in the OER getInstance factories and the transposed isInstance arguments in OEROptional.getObject (PR #2373). Requiring the signature handed to AIMerSigner.verifySignature to be exactly the parameter set's signature size (PR #2401). Constant time comparison of the SRP-6a evidence messages (PR #2406). Surfacing a malformed TSTInfo in a time-stamp token as TSPException rather than an unchecked exception out of the TimeStampResponse constructors (PR #2415). Rejecting a PKCS#12 PBMAC1 KDF keyLength below the RFC 9579 sec. 9 floor of 20 octets (PR #2431). Initial implementation of the empty-label guard on name-constraint host matching, having found that the canonicalisation removed only one RFC 1034 root-label dot, so a dNSName, rfc822Name host or URI host carrying more than one matched no constraint at all (PR #2436). Initial implementation of the lookup of indirect CRL entries by issuer and serial number, having found that the certification path validators compared only the issuer of the first entry carrying a certificate's serial number (PR #2464). Initial implementation of the malformed-input handling of the OCSPResp and OCSPReq parsers (PR #2468). Rejecting short sequences in the CMS content-type decoders SignedData, EnvelopedData, AuthenticatedData, AuthEnvelopedData and EncryptedData (PR #2469). Finding the Hashtable.contains() guard on the CMS algorithm protection attribute in the signed and authenticated attribute table generators (PR #2480). Locale-independent address folding in SignedMailValidator (PR #2485).
536536
- suraj0208 \<https://github.com/suraj0208\> - initial work on auto-detecting private key reader (JcaPrivateKeyReader).
537537
- liamgilligan \<https://github.com/liamgilligan\> - noticing the BIP-340 step numbering in the BIP340Signer signing comments was incorrect (PR #2340).
538538
- digi-scrypt \<https://github.com/digi-scrypt\> - disabling DTD and external-entity resolution in KMIPInputStream to close an XXE (local file disclosure / SSRF) exposure in KMIP XML parsing (PR #2315).

‎docs/releasenotes.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ Date: 2026, TBD
8585

8686
- The jdk1.4 and jdk1.3 providers registered the PKCS12-AES256-AES128, PKCS12-AES256-AES128-GCM, PKCS12-DEF-AES256-AES128 and PKCS12-DEF-AES256-AES128-GCM KeyStores against classes those distributions do not contain. They are no longer registered there; the legacy PKCS12 stores can still read PBES2/AES protected files.
8787
- The OCSPResp(byte[]) and OCSPReq(byte[]) constructors reported a malformed encoding only when the ASN.1 decode raised IllegalArgumentException, ClassCastException or ASN1Exception, so a top-level SEQUENCE that is empty or truncated - which makes OCSPResponse/OCSPRequest read seq.getObjectAt(0) out of bounds - escaped the declared IOException as an ArrayIndexOutOfBoundsException. Both parsers now surface any RuntimeException from the decode as a CertIOException, as X509CertificateHolder and X509CRLHolder already do, so an OCSP client parsing a response and an OCSP responder parsing a request reject malformed input through the contract rather than crashing. The cause was in the ASN.1 types, which read fixed positions of their SEQUENCE without checking its size: OCSPResponse, OCSPRequest, BasicOCSPResponse, ResponseData, TBSRequest and Request now reject a short one with IllegalArgumentException, as SingleResponse and CertID already did. The OCSP ASN.1 types also cast their elements rather than passing them to getInstance(), so an element of the wrong type surfaced as a ClassCastException and a signature BIT STRING in a legal encoding other than DER was refused; they now use getInstance() throughout, and BasicOCSPResponse and Signature accept any BIT STRING encoding. The provider's OCSP revocation checking parses these types directly and let an unchecked exception out of CertPathValidator.validate() for a response that would not decode, whether stapled by the caller or returned by the responder; a stapled one is now rejected with a CertPathValidatorException, and a fetched one is treated as an unreachable responder is, so the check can fall back to CRLs. A successful response carrying no response bytes, which failed with a NullPointerException, is rejected the same way.
88+
- SignedMailValidator lower cased the signer certificate's email addresses and the message's From addresses with the default locale captured when the class was loaded, so on a Turkish or Azerbaijani locale, where 'I' lower cases to the dotless 'i' (U+0131), a correctly signed message from a mailbox written with an upper case I was reported as SignedMailValidator.emailFromCertMismatch. Lower casing with any locale also mapped some non-ASCII characters in a UTF8String or BMPString emailAddress attribute onto ASCII letters - U+212A KELVIN SIGN onto 'k', for example - so a certificate could match a From address it was not issued for. Both sides of the comparison are now lower cased in ASCII only, independently of the locale (PR #2485).
8889

8990
- TLS: A TLS 1.3 client rejected every server certificate whose TLS feature extension (RFC 7633) listed status_request ("must-staple") whenever it had offered status_request itself, since the feature was looked for only in EncryptedExtensions, where RFC 8446 does not permit it; TLS 1.3 carries the OCSP response in the end-entity certificate's CertificateEntry. A feature is now also satisfied by the extensions of that CertificateEntry, and a feature whose extension has no place in TLS 1.3, such as status_request_v2, is no longer enforced once TLS 1.3 is negotiated (github #2458).
9091
- (D)TLS: A server certificate whose TLS feature extension listed status_request or status_request_v2 was accepted without any OCSP response for it when the server acknowledged the status request but then sent no CertificateStatus message, which RFC 6066 permits, or sent an ocsp_multi list with no response for the end-entity certificate. Such a certificate now requires a stapled OCSP response for the end-entity certificate, in every protocol version. A TLS feature in force against a (D)TLS 1.2 ServerHello that carried no extensions at all now fails with a certificate_unknown alert rather than a NullPointerException.

‎mail/src/main/java/org/bouncycastle/mail/smime/validator/SignedMailValidator.java‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import java.util.Iterator;
2525
import java.util.LinkedHashSet;
2626
import java.util.List;
27-
import java.util.Locale;
2827
import java.util.Map;
2928
import java.util.Set;
3029

@@ -67,6 +66,7 @@
6766
import org.bouncycastle.pkix.util.filter.TrustedInput;
6867
import org.bouncycastle.pkix.util.filter.UntrustedInput;
6968
import org.bouncycastle.util.Integers;
69+
import org.bouncycastle.util.Strings;
7070

7171
public class SignedMailValidator
7272
{
@@ -84,8 +84,6 @@ public class SignedMailValidator
8484
private static final int KU_DIGITAL_SIGNATURE = 0;
8585
private static final int KU_NON_REPUDIATION = 1;
8686

87-
private static final Locale locale = Locale.getDefault();
88-
8987
private CertStore certs;
9088

9189
private SignerInformationStore signers;
@@ -423,7 +421,7 @@ public static Set getEmailAddresses(X509Certificate cert) throws IOException, Ce
423421
{
424422
if (PKCSObjectIdentifiers.pkcs_9_at_emailAddress.equals(atVs[j].getType()))
425423
{
426-
String email = ((ASN1String)atVs[j].getValue()).getString().toLowerCase(locale);
424+
String email = Strings.toLowerCase(((ASN1String)atVs[j].getValue()).getString());
427425
addresses.add(email);
428426
}
429427
}
@@ -440,7 +438,7 @@ public static Set getEmailAddresses(X509Certificate cert) throws IOException, Ce
440438
GeneralName name = names[i];
441439
if (name.getTagNo() == GeneralName.rfc822Name)
442440
{
443-
String email = ASN1IA5String.getInstance(name.getName()).getString().toLowerCase(locale);
441+
String email = Strings.toLowerCase(ASN1IA5String.getInstance(name.getName()).getString());
444442
addresses.add(email);
445443
}
446444
}
@@ -542,7 +540,7 @@ static boolean hasAnyFromAddress(Set certEmails, String[] fromAddresses)
542540
// check if email in cert is equal to the from address in the message
543541
for (int i = 0; i < fromAddresses.length; ++i)
544542
{
545-
if (certEmails.contains(fromAddresses[i].toLowerCase(locale)))
543+
if (certEmails.contains(Strings.toLowerCase(fromAddresses[i])))
546544
{
547545
return true;
548546
}

‎mail/src/test/java/org/bouncycastle/mail/smime/test/SignedMailValidatorTest.java‎

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,14 @@
3939
import org.bouncycastle.asn1.ASN1EncodableVector;
4040
import org.bouncycastle.asn1.ASN1Encoding;
4141
import org.bouncycastle.asn1.DERSet;
42+
import org.bouncycastle.asn1.DERUTF8String;
4243
import org.bouncycastle.asn1.cms.Attribute;
4344
import org.bouncycastle.asn1.cms.AttributeTable;
4445
import org.bouncycastle.asn1.cms.CMSAttributes;
4546
import org.bouncycastle.asn1.cms.Time;
4647
import org.bouncycastle.asn1.x500.X500Name;
48+
import org.bouncycastle.asn1.x500.X500NameBuilder;
49+
import org.bouncycastle.asn1.x500.style.BCStyle;
4750
import org.bouncycastle.asn1.x509.BasicConstraints;
4851
import org.bouncycastle.asn1.x509.Extension;
4952
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
@@ -164,6 +167,101 @@ public void testMultiEmail() throws Exception
164167
assertTrue(addresses.contains("domain-confidentiality-authority@bekb.ch "));
165168
}
166169

170+
public void testEmailAddressFoldingIsLocaleIndependent() throws Exception
171+
{
172+
// Turkish rules fold 'I' to the dotless U+0131; the class used to capture the default locale at load time.
173+
Locale defaultLocale = Locale.getDefault();
174+
175+
try
176+
{
177+
Locale.setDefault(new Locale("tr", "TR"));
178+
179+
X500Name signDN = new X500Name("CN=Ian Echidna, E=IAN@EXAMPLE.ORG, O=Bouncy Castle, C=AU");
180+
181+
assertTrue("From address must match the certificate address whatever the default locale",
182+
validateEmailBinding(signDN, "ian@example.org").isValidSignature());
183+
}
184+
finally
185+
{
186+
Locale.setDefault(defaultLocale);
187+
}
188+
}
189+
190+
public void testEmailAddressFoldingIsAsciiOnly() throws Exception
191+
{
192+
// U+212A KELVIN SIGN lower cases to 'k', U+0130 to 'i' (Turkish) or "i̇", so neither may match an ASCII mailbox.
193+
checkNonAsciiEmailRejected("Kate@example.org", "kate@example.org");
194+
checkNonAsciiEmailRejected("İan@example.org", "ian@example.org");
195+
}
196+
197+
private void checkNonAsciiEmailRejected(String certEmail, String fromEmail) throws Exception
198+
{
199+
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
200+
nameBuilder.addRDN(BCStyle.CN, "Test Signer");
201+
nameBuilder.addRDN(BCStyle.EmailAddress, new DERUTF8String(certEmail));
202+
nameBuilder.addRDN(BCStyle.O, "Bouncy Castle");
203+
nameBuilder.addRDN(BCStyle.C, "AU");
204+
205+
SignedMailValidator.ValidationResult result = validateEmailBinding(nameBuilder.build(), fromEmail);
206+
207+
assertTrue(result.isVerifiedSignature());
208+
assertFalse(result.isValidSignature());
209+
210+
Iterator it = result.getErrors().iterator();
211+
boolean found = false;
212+
while (it.hasNext())
213+
{
214+
if (((ErrorBundle)it.next()).getId().equals("SignedMailValidator.emailFromCertMismatch"))
215+
{
216+
found = true;
217+
}
218+
}
219+
assertTrue("expected emailFromCertMismatch for " + fromEmail, found);
220+
}
221+
222+
private static SignedMailValidator.ValidationResult validateEmailBinding(X500Name signDN, String from)
223+
throws Exception
224+
{
225+
long now = System.currentTimeMillis();
226+
long day = 1000L * 60 * 60 * 24;
227+
Date notBefore = new Date(now - day);
228+
Date notAfter = new Date(now + 100 * day);
229+
230+
X500Name caDN = new X500Name("CN=Test CA, O=Bouncy Castle, C=AU");
231+
232+
KeyPair caKP = CMSTestUtil.makeKeyPair();
233+
KeyPair signKP = CMSTestUtil.makeKeyPair();
234+
235+
X509Certificate caCert = buildCert(caDN, caKP.getPublic(), caDN, caKP, notBefore, notAfter, true);
236+
X509Certificate signCert = buildCert(signDN, signKP.getPublic(), caDN, caKP, notBefore, notAfter, false);
237+
238+
List certList = new ArrayList();
239+
certList.add(signCert);
240+
certList.add(caCert);
241+
242+
SMIMESignedGenerator gen = new SMIMESignedGenerator();
243+
gen.addSignerInfoGenerator(new JcaSimpleSignerInfoGeneratorBuilder().setProvider("BC")
244+
.build("SHA256withRSA", signKP.getPrivate(), signCert));
245+
gen.addCertificates(new JcaCertStore(certList));
246+
247+
MimeMultipart signedMsg = gen.generate(SMIMETestUtil.makeMimeBodyPart("Hello world!\n"));
248+
249+
Session session = Session.getDefaultInstance(System.getProperties(), null);
250+
MimeMessage msg = new MimeMessage(session);
251+
msg.setFrom(new InternetAddress(from));
252+
msg.setRecipient(Message.RecipientType.TO, new InternetAddress("example@bouncycastle.org"));
253+
msg.setContent(signedMsg, signedMsg.getContentType());
254+
msg.saveChanges();
255+
256+
Set trust = new HashSet();
257+
trust.add(new TrustAnchor(caCert, null));
258+
259+
PKIXParameters params = new PKIXParameters(trust);
260+
params.setRevocationEnabled(false);
261+
262+
return firstResult(new SignedMailValidator(msg, params));
263+
}
264+
167265
public void testExtKeyUsage() throws Exception
168266
{
169267
String message = "validator.extKeyUsage.eml";
@@ -343,8 +441,15 @@ private static X509Certificate buildCert(String subDN, java.security.PublicKey s
343441
KeyPair issKP, Date notBefore, Date notAfter, boolean ca)
344442
throws Exception
345443
{
346-
JcaX509v3CertificateBuilder b = new JcaX509v3CertificateBuilder(new X500Name(issDN),
347-
BigInteger.valueOf(System.nanoTime()), notBefore, notAfter, new X500Name(subDN), subPub);
444+
return buildCert(new X500Name(subDN), subPub, new X500Name(issDN), issKP, notBefore, notAfter, ca);
445+
}
446+
447+
private static X509Certificate buildCert(X500Name subDN, java.security.PublicKey subPub, X500Name issDN,
448+
KeyPair issKP, Date notBefore, Date notAfter, boolean ca)
449+
throws Exception
450+
{
451+
JcaX509v3CertificateBuilder b = new JcaX509v3CertificateBuilder(issDN,
452+
BigInteger.valueOf(System.nanoTime()), notBefore, notAfter, subDN, subPub);
348453
b.addExtension(Extension.basicConstraints, false, new BasicConstraints(ca));
349454
if (ca)
350455
{

0 commit comments

Comments
 (0)