@@ -72,6 +77,18 @@
https://github.com/bbottema/java-utils-mail-smime/issues
+
+
+
+ org.junit
+ junit-bom
+ ${junit.version}
+ pom
+ import
+
+
+
+
org.jetbrains
@@ -85,10 +102,37 @@
bcjmail-jdk15to18
1.70
+
+ jakarta.activation
+ jakarta.activation-api
+ 2.0.1
+
+
+ com.sun.activation
+ jakarta.activation
+ 2.0.1
+
+
+ jakarta.mail
+ jakarta.mail-api
+ 2.0.1
+
com.sun.mail
jakarta.mail
2.0.1
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
\ No newline at end of file
diff --git a/src/main/java/org/simplejavamail/utils/mail/smime/KeyEncapsulationAlgorithm.java b/src/main/java/org/simplejavamail/utils/mail/smime/KeyEncapsulationAlgorithm.java
new file mode 100644
index 0000000..fe93fc0
--- /dev/null
+++ b/src/main/java/org/simplejavamail/utils/mail/smime/KeyEncapsulationAlgorithm.java
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2022. Ulrich Schuster, 10827 Berlin, Germany
+ */
+
+package org.simplejavamail.utils.mail.smime;
+
+public enum KeyEncapsulationAlgorithm {
+ RSA,
+ RSA_OAEP_SHA224,
+ RSA_OAEP_SHA256,
+ RSA_OAEP_SHA384,
+ RSA_OAEP_SHA512,
+}
\ No newline at end of file
diff --git a/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKey.java b/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKey.java
index 6aeb848..81f5a7c 100644
--- a/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKey.java
+++ b/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKey.java
@@ -89,7 +89,7 @@ private void extractAssociatedAddresses() {
try {
X509Certificate certificate = getCertificate();
if (null != certificate) {
- Principal principal = certificate.getSubjectDN();
+ Principal principal = certificate.getSubjectX500Principal();
if (null != principal) {
String name = principal.getName();
StringTokenizer tokenizer = new StringTokenizer(name, ",");
diff --git a/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKeyStore.java b/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKeyStore.java
index 8189cad..ba0f689 100644
--- a/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKeyStore.java
+++ b/src/main/java/org/simplejavamail/utils/mail/smime/SmimeKeyStore.java
@@ -14,7 +14,7 @@
/**
* A wrapper around a {@link KeyStore} that can be initialized with a PKCS12
* keystore and is used to obtain {@link SmimeKey SmimeKeys}.
- *
+ *
* @author Allen Petersen (akp at sourceforge dot net)
* @author Torsten Krause (tk at markenwerk dot net)
* @since 1.0.0
@@ -26,11 +26,11 @@ public class SmimeKeyStore {
/**
* Creates a new {@code SmimeKeyStore} by loading a PKCS12 keystore from
* the given input stream.
- *
+ *
*
* The character array holding the password is overwritten with {@code 0s}
* after it has been used.
- *
+ *
* @param stream
* The {@link InputStream} to read the PKCS12 keystore from.
* @param password
@@ -43,12 +43,12 @@ public SmimeKeyStore(InputStream stream, char[] password) {
/**
* Creates a new {@code SmimeKeyStore} by loading a PKCS12 keystore from
* the given input stream.
- *
+ *
*
* If {@code discardPassword} is set to {@code true}, the character array
* holding the password is overwritten with {@code 0s} after it has been
* used.
- *
+ *
* @param stream
* The {@link InputStream} to read the PKCS12 keystore from.
* @param password
@@ -95,11 +95,11 @@ public int size() {
/**
* Returns the S/MIME key associated with the given alias, using the given
* password to recover it.
- *
+ *
*
* The character array holding the password is overwritten with {@code 0s}
* after it has been used.
- *
+ *
* @param alias
* The alias.
* @param password
@@ -115,12 +115,12 @@ public SmimeKey getPrivateKey(String alias, char[] password) {
/**
* Returns the S/MIME key associated with the given alias, using the given
* password to recover it.
- *
+ *
*
* If {@code discardPassword} is set to {@code true}, the character array
* holding the password is overwritten with {@code 0s} after it has been
* used.
- *
+ *
* @param alias
* The alias.
* @param password
diff --git a/src/main/java/org/simplejavamail/utils/mail/smime/SmimeUtil.java b/src/main/java/org/simplejavamail/utils/mail/smime/SmimeUtil.java
index 7340ae4..03ba41d 100644
--- a/src/main/java/org/simplejavamail/utils/mail/smime/SmimeUtil.java
+++ b/src/main/java/org/simplejavamail/utils/mail/smime/SmimeUtil.java
@@ -1,9 +1,18 @@
package org.simplejavamail.utils.mail.smime;
import com.sun.mail.smtp.SMTPMessage;
+import jakarta.activation.CommandMap;
+import jakarta.activation.MailcapCommandMap;
+import jakarta.mail.Header;
+import jakarta.mail.MessagingException;
+import jakarta.mail.Multipart;
+import jakarta.mail.Session;
+import jakarta.mail.internet.*;
import org.bouncycastle.asn1.ASN1EncodableVector;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.cms.IssuerAndSerialNumber;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.smime.SMIMECapabilitiesAttribute;
import org.bouncycastle.asn1.smime.SMIMECapability;
import org.bouncycastle.asn1.smime.SMIMECapabilityVector;
@@ -12,47 +21,24 @@
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
-import org.bouncycastle.cms.CMSAlgorithm;
-import org.bouncycastle.cms.CMSException;
-import org.bouncycastle.cms.RecipientInformation;
-import org.bouncycastle.cms.RecipientInformationStore;
-import org.bouncycastle.cms.SignerId;
-import org.bouncycastle.cms.SignerInfoGenerator;
-import org.bouncycastle.cms.SignerInformation;
-import org.bouncycastle.cms.SignerInformationVerifier;
-import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder;
-import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
-import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder;
-import org.bouncycastle.cms.jcajce.JceKeyTransEnvelopedRecipient;
-import org.bouncycastle.cms.jcajce.JceKeyTransRecipient;
-import org.bouncycastle.cms.jcajce.JceKeyTransRecipientId;
-import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator;
+import org.bouncycastle.cms.*;
+import org.bouncycastle.cms.jcajce.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.mail.smime.SMIMEEnveloped;
-import org.bouncycastle.mail.smime.SMIMEEnvelopedGenerator;
-import org.bouncycastle.mail.smime.SMIMESigned;
-import org.bouncycastle.mail.smime.SMIMESignedGenerator;
-import org.bouncycastle.mail.smime.SMIMEUtil;
+import org.bouncycastle.mail.smime.*;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.OutputEncryptor;
+import org.bouncycastle.operator.jcajce.JcaAlgorithmParametersConverter;
import org.bouncycastle.util.Store;
-import jakarta.activation.CommandMap;
-import jakarta.activation.MailcapCommandMap;
-import jakarta.mail.Header;
-import jakarta.mail.MessagingException;
-import jakarta.mail.Multipart;
-import jakarta.mail.Session;
-import jakarta.mail.internet.ContentType;
-import jakarta.mail.internet.MimeBodyPart;
-import jakarta.mail.internet.MimeMessage;
-import jakarta.mail.internet.MimeMultipart;
-import jakarta.mail.internet.MimePart;
+import javax.crypto.spec.OAEPParameterSpec;
+import javax.crypto.spec.PSource;
import java.io.IOException;
import java.math.BigInteger;
+import java.security.InvalidAlgorithmParameterException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
@@ -60,596 +46,656 @@
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Enumeration;
-import java.util.Iterator;
-import java.util.List;
+import java.security.spec.MGF1ParameterSpec;
+import java.util.*;
/**
* Utilities for handling S/MIME specific operations on MIME messages from
* JavaMail.
- *
+ *
* @author Allen Petersen (akp at sourceforge dot net)
* @author Torsten Krause (tk at markenwerk dot net)
* @since 1.0.0
*/
public final class SmimeUtil {
- static {
- if (null == Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)) {
- Security.addProvider(new BouncyCastleProvider());
- updateMailcapCommandMap();
- }
- }
-
- @SuppressWarnings("unused")
- private SmimeUtil() {
- }
-
- private static void updateMailcapCommandMap() {
- MailcapCommandMap map = (MailcapCommandMap) CommandMap.getDefaultCommandMap();
- map.addMailcap("application/pkcs7-signature;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_signature");
- map.addMailcap("application/pkcs7-mime;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_mime");
- map.addMailcap("application/x-pkcs7-signature;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_signature");
- map.addMailcap("application/x-pkcs7-mime;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_mime");
- map.addMailcap("multipart/signed;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.multipart_signed");
- CommandMap.setDefaultCommandMap(map);
- }
-
- /**
- * Encrypts a MIME message and yields a new S/MIME encrypted MIME message.
- *
- * @param session
- * The {@link Session} that is used in conjunction with the
- * original {@link MimeMessage}.
- * @param mimeMessage
- * The original {@link MimeMessage} to be encrypted.
- * @param certificate
- * The {@link X509Certificate} used to obtain the
- * {@link PublicKey} to encrypt the original message with.
- * @return The new S/MIME encrypted {@link MimeMessage}.
- */
- public static MimeMessage encrypt(Session session, MimeMessage mimeMessage, X509Certificate certificate) {
- try {
- MimeMessage encryptedMimeMessage = new MimeMessage(session);
- copyHeaders(mimeMessage, encryptedMimeMessage);
-
- SMIMEEnvelopedGenerator generator = prepareGenerator(certificate);
- OutputEncryptor encryptor = prepareEncryptor();
-
- MimeBodyPart encryptedMimeBodyPart = generator.generate(mimeMessage, encryptor);
- copyContent(encryptedMimeBodyPart, encryptedMimeMessage);
- copyHeaders(encryptedMimeBodyPart, encryptedMimeMessage);
- encryptedMimeMessage.saveChanges();
- return encryptedMimeMessage;
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- /**
- * Encrypts a MIME body part and yields a new S/MIME encrypted MIME body
- * part.
- *
- * @param mimeBodyPart
- * The original {@link MimeBodyPart} to be encrypted.
- * @param certificate
- * The {@link X509Certificate} used to obtain the
- * {@link PublicKey} to encrypt the original body part with.
- * @return The new S/MIME encrypted {@link MimeBodyPart}.
- */
- public static MimeBodyPart encrypt(MimeBodyPart mimeBodyPart, X509Certificate certificate) {
- try {
- SMIMEEnvelopedGenerator generator = prepareGenerator(certificate);
- OutputEncryptor encryptor = prepareEncryptor();
-
- return generator.generate(mimeBodyPart, encryptor);
-
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- private static void copyHeaders(MimeBodyPart fromBodyPart, MimeMessage toMessage) throws MessagingException {
- Enumeration headers = fromBodyPart.getAllHeaders();
- copyHeaders(headers, toMessage);
- }
-
- private static void copyHeaders(MimeMessage fromMessage, MimeMessage toMessage) throws MessagingException {
- Enumeration headers = fromMessage.getAllHeaders();
- copyHeaders(headers, toMessage);
- }
-
- private static void copyHeaders(Enumeration headers, MimeMessage toMessage) throws MessagingException {
- while (headers.hasMoreElements()) {
- Header header = headers.nextElement();
- toMessage.setHeader(header.getName(), header.getValue());
- }
- }
-
- private static SMIMEEnvelopedGenerator prepareGenerator(X509Certificate certificate)
- throws CertificateEncodingException {
- JceKeyTransRecipientInfoGenerator infoGenerator = new JceKeyTransRecipientInfoGenerator(certificate);
- infoGenerator.setProvider(BouncyCastleProvider.PROVIDER_NAME);
- SMIMEEnvelopedGenerator generator = new SMIMEEnvelopedGenerator();
- generator.addRecipientInfoGenerator(infoGenerator);
- return generator;
- }
-
- private static OutputEncryptor prepareEncryptor() throws CMSException {
- return new JceCMSContentEncryptorBuilder(CMSAlgorithm.DES_EDE3_CBC).setProvider(
- BouncyCastleProvider.PROVIDER_NAME).build();
- }
-
- /**
- * Decrypts an S/MIME encrypted MIME message and yields a new MIME message.
- *
- * @param session
- * The {@link Session} that is used in conjunction with the
- * encrypted {@link MimeMessage}.
- * @param mimeMessage
- * The encrypted {@link MimeMessage} to be decrypted.
- * @param smimeKey
- * The {@link SmimeKey} used to obtain the {@link PrivateKey} to
- * decrypt the encrypted message with.
- * @return The new S/MIME decrypted {@link MimeMessage}.
- */
- public static MimeMessage decrypt(Session session, MimeMessage mimeMessage, SmimeKey smimeKey) {
- try {
- byte[] content = decryptContent(new SMIMEEnveloped(mimeMessage), smimeKey);
- MimeBodyPart mimeBodyPart = SMIMEUtil.toMimeBodyPart(content);
-
- MimeMessage decryptedMessage = new MimeMessage(session);
- copyHeaderLines(mimeMessage, decryptedMessage);
- copyContent(mimeBodyPart, decryptedMessage);
- decryptedMessage.setHeader("Content-Type", mimeBodyPart.getContentType());
- return decryptedMessage;
-
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- /**
- * Decrypts an S/MIME encrypted MIME body part and yields a new MIME body
- * part.
- *
- * @param mimeBodyPart
- * The encrypted {@link MimeBodyPart} to be decrypted.
- * @param smimeKey
- * The {@link SmimeKey} used to obtain the {@link PrivateKey} to
- * decrypt the encrypted body part with.
- * @return The new S/MIME decrypted {@link MimeBodyPart}.
- */
- public static MimeBodyPart decrypt(MimeBodyPart mimeBodyPart, SmimeKey smimeKey) {
- try {
- return SMIMEUtil.toMimeBodyPart(decryptContent(new SMIMEEnveloped(mimeBodyPart), smimeKey));
- } catch (Exception e) {
- throw handledException(e);
- }
-
- }
-
- /**
- * Decrypts an S/MIME encrypted MIME multipart and yields a new MIME body
- * part.
- *
- * @param mimeMultipart
- * The encrypted {@link MimeMultipart} to be decrypted.
- * @param smimeKey
- * The {@link SmimeKey} used to obtain the {@link PrivateKey} to
- * decrypt the encrypted multipart with.
- * @return The new S/MIME decrypted {@link MimeBodyPart}.
- */
- public static MimeBodyPart decrypt(MimeMultipart mimeMultipart, SmimeKey smimeKey) {
- try {
- MimeBodyPart mimeBodyPart = new MimeBodyPart();
- mimeBodyPart.setContent(mimeMultipart);
- mimeBodyPart.setHeader("Content-Type", mimeMultipart.getContentType());
- return decrypt(mimeBodyPart, smimeKey);
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- private static byte[] decryptContent(SMIMEEnveloped smimeEnveloped, SmimeKey smimeKey) throws MessagingException,
- CMSException {
- X509Certificate certificate = smimeKey.getCertificate();
- PrivateKey privateKey = smimeKey.getPrivateKey();
-
- RecipientInformationStore recipients = smimeEnveloped.getRecipientInfos();
- RecipientInformation recipient = recipients.get(new JceKeyTransRecipientId(certificate));
-
- if (null == recipient) {
- throw new MessagingException("no recipient");
- }
-
- JceKeyTransRecipient transportRecipient = new JceKeyTransEnvelopedRecipient(privateKey);
- transportRecipient.setProvider(BouncyCastleProvider.PROVIDER_NAME);
- return recipient.getContent(transportRecipient);
- }
-
- private static void copyHeaderLines(MimeMessage fromMessage, MimeMessage toMessage) throws MessagingException {
- Enumeration headerLines = fromMessage.getAllHeaderLines();
- while (headerLines.hasMoreElements()) {
- String nextElement = headerLines.nextElement();
- toMessage.addHeaderLine(nextElement);
- }
- }
-
- private static void copyContent(MimeBodyPart fromBodyPart, MimeMessage toMessage) throws MessagingException,
- IOException {
- toMessage.setContent(fromBodyPart.getContent(), fromBodyPart.getContentType());
- }
-
- /**
- * Signs a MIME body part and yields a new S/MIME signed MIME body part.
- *
- * @param mimeBodyPart
- * The original {@link MimeBodyPart} to be signed.
- * @param smimeKey
- * The {@link SmimeKey} used to obtain the {@link PrivateKey} to
- * sign the original body part with.
- * @return The new S/MIME signed {@link MimeBodyPart}.
- */
- public static MimeBodyPart sign(MimeBodyPart mimeBodyPart, SmimeKey smimeKey) {
- try {
- SMIMESignedGenerator generator = getGenerator(smimeKey);
- MimeMultipart signedMimeMultipart = generator.generate(MimeUtil.canonicalize(mimeBodyPart));
- MimeBodyPart signedMimeBodyPart = new MimeBodyPart();
- signedMimeBodyPart.setContent(signedMimeMultipart);
- return signedMimeBodyPart;
-
- } catch (Exception e) {
- throw handledException(e);
- }
-
- }
-
- private static SMIMESignedGenerator getGenerator(SmimeKey smimeKey) throws CertificateEncodingException,
- OperatorCreationException {
- SMIMESignedGenerator generator = new SMIMESignedGenerator();
- generator.addCertificates(getCertificateStore(smimeKey));
- generator.addSignerInfoGenerator(getInfoGenerator(smimeKey));
- return generator;
- }
-
- private static SignerInfoGenerator getInfoGenerator(SmimeKey smimeKey) throws OperatorCreationException,
- CertificateEncodingException {
- JcaSimpleSignerInfoGeneratorBuilder builder = new JcaSimpleSignerInfoGeneratorBuilder();
- builder.setSignedAttributeGenerator(new AttributeTable(getSignedAttributes(smimeKey)));
- builder.setProvider(BouncyCastleProvider.PROVIDER_NAME);
-
- PrivateKey privateKey = smimeKey.getPrivateKey();
- X509Certificate certificate = smimeKey.getCertificate();
- return builder.build("SHA256withRSA", privateKey, certificate);
- }
-
- private static ASN1EncodableVector getSignedAttributes(SmimeKey smimeKey) {
- ASN1EncodableVector signedAttributes = new ASN1EncodableVector();
- IssuerAndSerialNumber issuerAndSerialNumber = getIssuerAndSerialNumber(smimeKey);
- signedAttributes.add(new SMIMEEncryptionKeyPreferenceAttribute(issuerAndSerialNumber));
- signedAttributes.add(new SMIMECapabilitiesAttribute(getCapabilityVector()));
- return signedAttributes;
- }
-
- private static SMIMECapabilityVector getCapabilityVector() {
- SMIMECapabilityVector capabilityVector = new SMIMECapabilityVector();
- capabilityVector.addCapability(SMIMECapability.dES_EDE3_CBC);
- capabilityVector.addCapability(SMIMECapability.rC2_CBC, 128);
- capabilityVector.addCapability(SMIMECapability.dES_CBC);
- return capabilityVector;
- }
-
- private static IssuerAndSerialNumber getIssuerAndSerialNumber(SmimeKey smimeKey) {
- X509Certificate certificate = smimeKey.getCertificate();
- BigInteger serialNumber = certificate.getSerialNumber();
- X500Name issuerName = new X500Name(certificate.getIssuerDN().getName());
- return new IssuerAndSerialNumber(issuerName, serialNumber);
- }
-
- private static JcaCertStore getCertificateStore(SmimeKey smimeKey) throws CertificateEncodingException {
- Certificate[] certificateChain = smimeKey.getCertificateChain();
- X509Certificate certificate = smimeKey.getCertificate();
-
- final List certificateList;
- if (certificateChain != null && certificateChain.length > 0) {
- certificateList = Arrays.asList(certificateChain);
- } else {
- certificateList = new ArrayList<>();
- certificateList.add(certificate);
- }
- return new JcaCertStore(certificateList);
- }
-
- /**
- * Signs a MIME message and yields a new S/MIME signed MIME message.
- *
- * @param session
- * The {@link Session} that is used in conjunction with the
- * original {@link MimeMessage}.
- * @param mimeMessage
- * The original {@link MimeMessage} or {@link SMTPMessage} to be signed.
- * @param smimeKey
- * The {@link SmimeKey} used to obtain the {@link PrivateKey} to
- * sign the original message with.
- * @return The new S/MIME signed {@link MimeMessage} or {@link SMTPMessage}.
- */
- public static T sign(Session session, T mimeMessage, SmimeKey smimeKey) {
- //noinspection unchecked
- return (mimeMessage instanceof SMTPMessage)
- ? sign(mimeMessage, (T) new SMTPMessage(session), smimeKey)
- : sign(mimeMessage, (T) new MimeMessage(session), smimeKey);
- }
-
- private static T sign(T mimeMessage, T signedMessage, SmimeKey smimeKey) {
- try {
- copyHeaderLines(mimeMessage, signedMessage);
- copyContent(sign(extractMimeBodyPart(mimeMessage), smimeKey), signedMessage);
- return signedMessage;
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- private static MimeBodyPart extractMimeBodyPart(MimeMessage mimeMessage) throws IOException, MessagingException {
- Object content = mimeMessage.getContent();
- UpdatableMimeBodyPart updateableMimeBodyPart = new UpdatableMimeBodyPart();
- if (content instanceof Multipart) {
- updateableMimeBodyPart.setContent((Multipart) content);
- } else {
- updateableMimeBodyPart.setContent(content, mimeMessage.getDataHandler().getContentType());
- }
- updateableMimeBodyPart.updateHeaders();
- return updateableMimeBodyPart;
- }
-
- /**
- * Checks the signature on an S/MIME signed MIME multipart.
- *
- * @param mimeMultipart
- * The {@link MimeMultipart} to be checked.
- * @return {@code true} if the multipart is correctly signed, {@code false}
- * otherwise.
- */
- public static boolean checkSignature(MimeMultipart mimeMultipart) {
- try {
- return checkSignature(new SMIMESigned(mimeMultipart));
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- /**
- * Checks the signature on an S/MIME signed MIME part (i.e. MIME message).
- *
- * @param mimePart
- * The {@link MimePart} to be checked.
- * @return {@code true} if the part is correctly signed, {@code false}
- * otherwise.
- */
- public static boolean checkSignature(MimePart mimePart) {
- try {
- if (mimePart.isMimeType("multipart/signed")) {
- return checkSignature(new SMIMESigned((MimeMultipart) mimePart.getContent()));
- } else if (mimePart.isMimeType("application/pkcs7-mime") || mimePart.isMimeType("application/x-pkcs7-mime")) {
- return checkSignature(new SMIMESigned(mimePart));
- } else {
- throw new SmimeException("Message not signed");
- }
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- /**
- * Checks a SMIMESigned to make sure that the signature matches.
- */
- private static boolean checkSignature(SMIMESigned smimeSigned) {
- try {
- boolean returnValue = true;
-
- @SuppressWarnings("rawtypes")
- Store certificates = smimeSigned.getCertificates();
- Iterator signerInformations = smimeSigned.getSignerInfos().getSigners().iterator();
-
- while (returnValue && signerInformations.hasNext()) {
- SignerInformation signerInformation = signerInformations.next();
- X509Certificate certificate = getCertificate(certificates, signerInformation.getSID());
- SignerInformationVerifier verifier = getVerifier(certificate);
- if (!signerInformation.verify(verifier)) {
- returnValue = false;
- }
- }
- return returnValue;
-
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- /**
- * @param mimeMultipart
- * The {@link MimeMultipart} to be checked.
- * @return The subject / address to which the certificate was issued to. Email clients may use this to show
- * {@code "Signed by: "}
- */
- public static String getSignedByAddress(MimeMultipart mimeMultipart) {
- try {
- return getSignedByAddress(new SMIMESigned(mimeMultipart));
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- /**
- * @param mimePart
- * The {@link MimePart} to be checked.
- * @return The subject / address to which the certificate was issued to. Email clients may use this to show
- * {@code "Signed by: "}
- */
- public static String getSignedByAddress(MimePart mimePart) {
- try {
- if (mimePart.isMimeType("multipart/signed")) {
- return getSignedByAddress(new SMIMESigned((MimeMultipart) mimePart.getContent()));
- } else if (mimePart.isMimeType("application/pkcs7-mime") || mimePart.isMimeType("application/x-pkcs7-mime")) {
- return getSignedByAddress(new SMIMESigned(mimePart));
- } else {
- throw new SmimeException("Message not signed");
- }
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- /**
- * Returns the subject / address to which the certificate was issued to. Email clients may use this to show
- * {@code "Signed by: "}
- */
- private static String getSignedByAddress(SMIMESigned smimeSigned) {
- try {
- @SuppressWarnings("rawtypes")
- Store certificates = smimeSigned.getCertificates();
-
- SignerInformation signerInformation = smimeSigned.getSignerInfos().getSigners().iterator().next();
- X509Certificate certificate = getCertificate(certificates, signerInformation.getSID());
- SignerInformationVerifier verifier = getVerifier(certificate);
- X500Name x500name = verifier.getAssociatedCertificate().getSubject();
- RDN cn = x500name.getRDNs(BCStyle.CN)[0];
- return IETFUtils.valueToString(cn.getFirst().getValue());
-
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- private static X509Certificate getCertificate(@SuppressWarnings("rawtypes") Store certificates, SignerId signerId)
- throws CertificateException {
- @SuppressWarnings({ "unchecked" })
- X509CertificateHolder certificateHolder = (X509CertificateHolder) certificates.getMatches(signerId).iterator()
- .next();
- JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
- certificateConverter.setProvider(BouncyCastleProvider.PROVIDER_NAME);
- return certificateConverter.getCertificate(certificateHolder);
- }
-
- private static SignerInformationVerifier getVerifier(X509Certificate certificate) throws OperatorCreationException {
- JcaSimpleSignerInfoVerifierBuilder builder = new JcaSimpleSignerInfoVerifierBuilder();
- builder.setProvider(BouncyCastleProvider.PROVIDER_NAME);
- return builder.build(certificate);
- }
-
- /**
- * Returns the signed MIME body part of an S/MIME signed MIME multipart.
- *
- * @param mimeMultipart
- * The {@link MimeMultipart} to be stripped off.
- * @return The signed {@link MimeBodyPart} contained in the
- * {@link MimeMultipart}.
- */
- public static MimeBodyPart getSignedContent(MimeMultipart mimeMultipart) {
- try {
- return new SMIMESigned(mimeMultipart).getContent();
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- /**
- * Returns the signed MIME body part of an S/MIME signed MIME part (i.e. MIME
- * message).
- *
- * @param mimePart
- * The {@link MimePart} to be stripped off.
- * @return The signed {@link MimeBodyPart} contained in the {@link MimePart}
- * .
- */
- public static MimeBodyPart getSignedContent(MimePart mimePart) {
- try {
- if (mimePart.isMimeType("multipart/signed")) {
- return new SMIMESigned((MimeMultipart) mimePart.getContent()).getContent();
- } else if (mimePart.isMimeType("application/pkcs7-mime") || mimePart.isMimeType("application/x-pkcs7-mime")) {
- return new SMIMESigned(mimePart).getContent();
- } else {
- throw new SmimeException("Message not signed");
- }
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- /**
- * Returns the S/MIME state of a MIME multipart.
- *
- * @param mimeMultipart
- * The {@link MimeMultipart} to be checked.
- * @return the {@link SmimeState} of the {@link MimeMultipart}.
- */
- public static SmimeState getStatus(MimeMultipart mimeMultipart) {
- try {
- return getStatus(new ContentType(mimeMultipart.getContentType()));
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- /**
- * Returns the S/MIME state of a MIME part (i.e. MIME message).
- *
- * @param mimePart
- * The {@link MimePart} to be checked.
- * @return the {@link SmimeState} of the {@link MimePart}.
- */
- public static SmimeState getStatus(MimePart mimePart) {
- try {
- return getStatus(new ContentType(mimePart.getContentType()));
- } catch (Exception e) {
- throw handledException(e);
- }
- }
-
- private static SmimeState getStatus(ContentType contentType) {
- if (isSmimeSignatureContentType(contentType)) {
- return SmimeState.SIGNED;
- } else if (isSignatureSmimeType(contentType)) {
- return SmimeState.SIGNED_ENVELOPED;
- } else if (isSmimeEncryptionContenttype(contentType)) {
- return SmimeState.ENCRYPTED;
- } else {
- return SmimeState.NEITHER;
- }
- }
-
- private static boolean isSmimeEncryptionContenttype(ContentType contentType) {
- String baseContentType = contentType.getBaseType();
- return baseContentType.equalsIgnoreCase("application/pkcs7-mime")
- || baseContentType.equalsIgnoreCase("application/x-pkcs7-mime");
- }
-
- private static boolean isSmimeSignatureContentType(ContentType contentType) {
- String baseContentType = contentType.getBaseType();
- String protocol = contentType.getParameter("protocol");
- return baseContentType.equalsIgnoreCase("multipart/signed")
- && protocol != null && isSmimeSignatureProtocoll(protocol);
- }
-
- private static boolean isSignatureSmimeType(ContentType contentType) {
- String baseContentType = contentType.getBaseType();
- return baseContentType.equalsIgnoreCase("application/x-pkcs7-mime")
- && "signed-data".equals(contentType.getParameter("smime-type"));
- }
-
- private static boolean isSmimeSignatureProtocoll(String protocol) {
- return protocol.equalsIgnoreCase("application/pkcs7-signature")
- || protocol.equalsIgnoreCase("application/x-pkcs7-signature");
- }
-
- private static SmimeException handledException(Exception e) {
- if (e instanceof SmimeException) {
- return (SmimeException) e;
- }
- return new SmimeException(e.getMessage(), e);
- }
+ private static final String defaultSignatureAlgorithmName = "SHA256withRSA";
+ private static final KeyEncapsulationAlgorithm defaultKeyEncapsulationAlgorithm = KeyEncapsulationAlgorithm.RSA;
+ private static final ASN1ObjectIdentifier defaultCipher = CMSAlgorithm.DES_EDE3_CBC;
+
+ static {
+ if (null == Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)) {
+ Security.addProvider(new BouncyCastleProvider());
+ updateMailcapCommandMap();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private SmimeUtil() {
+ }
+
+ private static void updateMailcapCommandMap() {
+ MailcapCommandMap map = (MailcapCommandMap) CommandMap.getDefaultCommandMap();
+ map.addMailcap("application/pkcs7-signature;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_signature");
+ map.addMailcap("application/pkcs7-mime;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.pkcs7_mime");
+ map.addMailcap("application/x-pkcs7-signature;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_signature");
+ map.addMailcap("application/x-pkcs7-mime;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.x_pkcs7_mime");
+ map.addMailcap("multipart/signed;;x-java-content-handler=org.bouncycastle.mail.smime.handlers.multipart_signed");
+ CommandMap.setDefaultCommandMap(map);
+ }
+
+ /**
+ * Encrypts a MIME message and yields a new S/MIME encrypted MIME message.
+ *
+ * @param session The {@link Session} that is used in conjunction with the
+ * original {@link MimeMessage}.
+ * @param mimeMessage The original {@link MimeMessage} to be encrypted.
+ * @param certificate The {@link X509Certificate} used to obtain the
+ * {@link PublicKey} to encrypt the original message with.
+ * @return The new S/MIME encrypted {@link MimeMessage}.
+ */
+ public static MimeMessage encrypt(Session session, MimeMessage mimeMessage, X509Certificate certificate) {
+ return encrypt(session, mimeMessage, certificate, defaultKeyEncapsulationAlgorithm, defaultCipher);
+ }
+
+ /**
+ * Encrypts a MIME message and yields a new S/MIME encrypted MIME message.
+ *
+ * @param session The {@link Session} that is used in conjunction with the
+ * original {@link MimeMessage}.
+ * @param mimeMessage The original {@link MimeMessage} to be encrypted.
+ * @param certificate The {@link X509Certificate} used to obtain the
+ * {@link PublicKey} to encrypt the original message with.
+ * @param keyEncapsulationAlgorithm Algorithm used to encapsulate the symmetric encryption key.
+ * Currently, RSA RSA-OAEP with various SHA digest lengths are supported.
+ * @param cmsAlgorithm Encryption algorithm for symmetric content encryption.
+ * @return The new S/MIME encrypted {@link MimeMessage}.
+ */
+ public static MimeMessage encrypt(Session session, MimeMessage mimeMessage, X509Certificate certificate, KeyEncapsulationAlgorithm keyEncapsulationAlgorithm, ASN1ObjectIdentifier cmsAlgorithm) {
+ try {
+ MimeMessage encryptedMimeMessage = new MimeMessage(session);
+ copyHeaders(mimeMessage, encryptedMimeMessage);
+
+ SMIMEEnvelopedGenerator generator = prepareGenerator(certificate, keyEncapsulationAlgorithm);
+ OutputEncryptor encryptor = prepareEncryptor(cmsAlgorithm);
+
+ MimeBodyPart encryptedMimeBodyPart = generator.generate(mimeMessage, encryptor);
+ copyContent(encryptedMimeBodyPart, encryptedMimeMessage);
+ copyHeaders(encryptedMimeBodyPart, encryptedMimeMessage);
+ encryptedMimeMessage.saveChanges();
+ return encryptedMimeMessage;
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ /**
+ * Encrypts a MIME body part and yields a new S/MIME encrypted MIME body
+ * part.
+ *
+ * @param mimeBodyPart The original {@link MimeBodyPart} to be encrypted.
+ * @param certificate The {@link X509Certificate} used to obtain the
+ * {@link PublicKey} to encrypt the original body part with.
+ * @return The new S/MIME encrypted {@link MimeBodyPart}.
+ */
+ public static MimeBodyPart encrypt(MimeBodyPart mimeBodyPart, X509Certificate certificate) {
+ return encrypt(mimeBodyPart, certificate, defaultKeyEncapsulationAlgorithm, defaultCipher);
+ }
+
+ /**
+ * Encrypts a MIME body part and yields a new S/MIME encrypted MIME body
+ * part.
+ *
+ * @param mimeBodyPart The original {@link MimeBodyPart} to be encrypted.
+ * @param certificate The {@link X509Certificate} used to obtain the
+ * {@link PublicKey} to encrypt the original body part with.
+ * @param keyEncapsulationAlgorithm Algorithm used to encapsulate the symmetric encryption key.
+ * Currently, RSA RSA-OAEP with various SHA digest lengths are supported.
+ * @param cmsAlgorithm Encryption algorithm for symmetric content encryption.
+ * @return The new S/MIME encrypted {@link MimeBodyPart}.
+ */
+ public static MimeBodyPart encrypt(MimeBodyPart mimeBodyPart,
+ X509Certificate certificate,
+ KeyEncapsulationAlgorithm keyEncapsulationAlgorithm,
+ ASN1ObjectIdentifier cmsAlgorithm) {
+ try {
+ SMIMEEnvelopedGenerator generator = prepareGenerator(certificate, keyEncapsulationAlgorithm);
+ OutputEncryptor encryptor = prepareEncryptor(cmsAlgorithm);
+
+ return generator.generate(mimeBodyPart, encryptor);
+
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ private static void copyHeaders(MimeBodyPart fromBodyPart, MimeMessage toMessage) throws MessagingException {
+ Enumeration headers = fromBodyPart.getAllHeaders();
+ copyHeaders(headers, toMessage);
+ }
+
+ private static void copyHeaders(MimeMessage fromMessage, MimeMessage toMessage) throws MessagingException {
+ Enumeration headers = fromMessage.getAllHeaders();
+ copyHeaders(headers, toMessage);
+ }
+
+ private static void copyHeaders(Enumeration headers, MimeMessage toMessage) throws MessagingException {
+ while (headers.hasMoreElements()) {
+ Header header = headers.nextElement();
+ toMessage.setHeader(header.getName(), header.getValue());
+ }
+ }
+
+ private static SMIMEEnvelopedGenerator prepareGenerator(X509Certificate certificate,
+ KeyEncapsulationAlgorithm keyEncapsulationAlgorithm)
+ throws CertificateEncodingException, InvalidAlgorithmParameterException {
+ JceKeyTransRecipientInfoGenerator infoGenerator;
+ if (keyEncapsulationAlgorithm == KeyEncapsulationAlgorithm.RSA) {
+ infoGenerator = new JceKeyTransRecipientInfoGenerator(certificate);
+ } else {
+ String digestName;
+ if (keyEncapsulationAlgorithm == KeyEncapsulationAlgorithm.RSA_OAEP_SHA224) {
+ digestName = "SHA-234";
+ } else if (keyEncapsulationAlgorithm == KeyEncapsulationAlgorithm.RSA_OAEP_SHA256) {
+ digestName = "SHA-256";
+ } else if (keyEncapsulationAlgorithm == KeyEncapsulationAlgorithm.RSA_OAEP_SHA384) {
+ digestName = "SHA-384";
+ } else if (keyEncapsulationAlgorithm == KeyEncapsulationAlgorithm.RSA_OAEP_SHA512) {
+ digestName = "SHA-512";
+ } else {
+ throw new InvalidAlgorithmParameterException("Unknown S/MIME key encapsulation algorithm: "
+ + keyEncapsulationAlgorithm.name());
+ }
+ JcaAlgorithmParametersConverter paramsConverter = new JcaAlgorithmParametersConverter();
+ AlgorithmIdentifier oaepParams = paramsConverter.getAlgorithmIdentifier(
+ PKCSObjectIdentifiers.id_RSAES_OAEP, new OAEPParameterSpec(
+ digestName, "MGF1", new MGF1ParameterSpec(digestName), PSource.PSpecified.DEFAULT));
+ infoGenerator = new JceKeyTransRecipientInfoGenerator(certificate, oaepParams);
+ }
+ infoGenerator.setProvider(BouncyCastleProvider.PROVIDER_NAME);
+ SMIMEEnvelopedGenerator generator = new SMIMEEnvelopedGenerator();
+ generator.addRecipientInfoGenerator(infoGenerator);
+ return generator;
+ }
+
+ private static OutputEncryptor prepareEncryptor(ASN1ObjectIdentifier cmsAlgorithm) throws CMSException {
+ return new JceCMSContentEncryptorBuilder(cmsAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build();
+ }
+
+ /**
+ * Decrypts an S/MIME encrypted MIME message and yields a new MIME message.
+ *
+ * @param session The {@link Session} that is used in conjunction with the
+ * encrypted {@link MimeMessage}.
+ * @param mimeMessage The encrypted {@link MimeMessage} to be decrypted.
+ * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to
+ * decrypt the encrypted message with.
+ * @return The new S/MIME decrypted {@link MimeMessage}.
+ */
+ public static MimeMessage decrypt(Session session, MimeMessage mimeMessage, SmimeKey smimeKey) {
+ try {
+ byte[] content = decryptContent(new SMIMEEnveloped(mimeMessage), smimeKey);
+ MimeBodyPart mimeBodyPart = SMIMEUtil.toMimeBodyPart(content);
+
+ MimeMessage decryptedMessage = new MimeMessage(session);
+ copyHeaderLines(mimeMessage, decryptedMessage);
+ copyContent(mimeBodyPart, decryptedMessage);
+ decryptedMessage.setHeader("Content-Type", mimeBodyPart.getContentType());
+ return decryptedMessage;
+
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ /**
+ * Decrypts an S/MIME encrypted MIME body part and yields a new MIME body
+ * part.
+ *
+ * @param mimeBodyPart The encrypted {@link MimeBodyPart} to be decrypted.
+ * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to
+ * decrypt the encrypted body part with.
+ * @return The new S/MIME decrypted {@link MimeBodyPart}.
+ */
+ public static MimeBodyPart decrypt(MimeBodyPart mimeBodyPart, SmimeKey smimeKey) {
+ try {
+ return SMIMEUtil.toMimeBodyPart(decryptContent(new SMIMEEnveloped(mimeBodyPart), smimeKey));
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ /**
+ * Decrypts an S/MIME encrypted MIME multipart and yields a new MIME body
+ * part.
+ *
+ * @param mimeMultipart The encrypted {@link MimeMultipart} to be decrypted.
+ * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to
+ * decrypt the encrypted multipart with.
+ * @return The new S/MIME decrypted {@link MimeBodyPart}.
+ */
+ public static MimeBodyPart decrypt(MimeMultipart mimeMultipart, SmimeKey smimeKey) {
+ try {
+ MimeBodyPart mimeBodyPart = new MimeBodyPart();
+ mimeBodyPart.setContent(mimeMultipart);
+ mimeBodyPart.setHeader("Content-Type", mimeMultipart.getContentType());
+ return decrypt(mimeBodyPart, smimeKey);
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ private static byte[] decryptContent(SMIMEEnveloped smimeEnveloped, SmimeKey smimeKey) throws MessagingException, CMSException {
+ X509Certificate certificate = smimeKey.getCertificate();
+ PrivateKey privateKey = smimeKey.getPrivateKey();
+
+ RecipientInformationStore recipients = smimeEnveloped.getRecipientInfos();
+ RecipientInformation recipient = recipients.get(new JceKeyTransRecipientId(certificate));
+
+ if (null == recipient) {
+ throw new MessagingException("no recipient");
+ }
+
+ JceKeyTransRecipient transportRecipient = new JceKeyTransEnvelopedRecipient(privateKey);
+ transportRecipient.setProvider(BouncyCastleProvider.PROVIDER_NAME);
+ return recipient.getContent(transportRecipient);
+ }
+
+ private static void copyHeaderLines(MimeMessage fromMessage, MimeMessage toMessage) throws MessagingException {
+ Enumeration headerLines = fromMessage.getAllHeaderLines();
+ while (headerLines.hasMoreElements()) {
+ String nextElement = headerLines.nextElement();
+ toMessage.addHeaderLine(nextElement);
+ }
+ }
+
+ private static void copyContent(MimeBodyPart fromBodyPart, MimeMessage toMessage) throws MessagingException, IOException {
+ toMessage.setContent(fromBodyPart.getContent(), fromBodyPart.getContentType());
+ }
+
+ /**
+ * Signs a MIME body part and yields a new S/MIME signed MIME body part.
+ *
+ * @param mimeBodyPart The original {@link MimeBodyPart} to be signed.
+ * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to
+ * sign the original body part with.
+ * @return The new S/MIME signed {@link MimeBodyPart}.
+ */
+ public static MimeBodyPart sign(MimeBodyPart mimeBodyPart, SmimeKey smimeKey) {
+ return sign(mimeBodyPart, smimeKey, defaultSignatureAlgorithmName);
+ }
+
+ /**
+ * Signs a MIME body part and yields a new S/MIME signed MIME body part.
+ *
+ * @param mimeBodyPart The original {@link MimeBodyPart} to be signed.
+ * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to
+ * sign the original body part with.
+ * @param algorithmName The name of the signature algorithm to use. Must be an algorithm
+ * supported by the Bouncy Castle security provider.
+ * @return The new S/MIME signed {@link MimeBodyPart}.
+ */
+ public static MimeBodyPart sign(MimeBodyPart mimeBodyPart, SmimeKey smimeKey, String algorithmName) {
+ try {
+ SMIMESignedGenerator generator = getGenerator(smimeKey, algorithmName);
+ MimeMultipart signedMimeMultipart = generator.generate(MimeUtil.canonicalize(mimeBodyPart));
+ MimeBodyPart signedMimeBodyPart = new MimeBodyPart();
+ signedMimeBodyPart.setContent(signedMimeMultipart);
+ return signedMimeBodyPart;
+
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+
+ }
+
+ private static SMIMESignedGenerator getGenerator(SmimeKey smimeKey, String algorithmName)
+ throws CertificateEncodingException, OperatorCreationException {
+ SMIMESignedGenerator generator = new SMIMESignedGenerator();
+ generator.addCertificates(getCertificateStore(smimeKey));
+ generator.addSignerInfoGenerator(getInfoGenerator(smimeKey, algorithmName));
+ return generator;
+ }
+
+ private static SignerInfoGenerator getInfoGenerator(SmimeKey smimeKey, String algorithmName)
+ throws OperatorCreationException, CertificateEncodingException {
+ JcaSimpleSignerInfoGeneratorBuilder builder = new JcaSimpleSignerInfoGeneratorBuilder();
+ builder.setSignedAttributeGenerator(new AttributeTable(getSignedAttributes(smimeKey)));
+ builder.setProvider(BouncyCastleProvider.PROVIDER_NAME);
+
+ PrivateKey privateKey = smimeKey.getPrivateKey();
+ X509Certificate certificate = smimeKey.getCertificate();
+ return builder.build(algorithmName, privateKey, certificate);
+ }
+
+ private static ASN1EncodableVector getSignedAttributes(SmimeKey smimeKey) {
+ ASN1EncodableVector signedAttributes = new ASN1EncodableVector();
+ IssuerAndSerialNumber issuerAndSerialNumber = getIssuerAndSerialNumber(smimeKey);
+ signedAttributes.add(new SMIMEEncryptionKeyPreferenceAttribute(issuerAndSerialNumber));
+ signedAttributes.add(new SMIMECapabilitiesAttribute(getCapabilityVector()));
+ return signedAttributes;
+ }
+
+ private static SMIMECapabilityVector getCapabilityVector() {
+ SMIMECapabilityVector capabilityVector = new SMIMECapabilityVector();
+ capabilityVector.addCapability(SMIMECapability.dES_EDE3_CBC);
+ capabilityVector.addCapability(SMIMECapability.rC2_CBC, 128);
+ capabilityVector.addCapability(SMIMECapability.dES_CBC);
+ return capabilityVector;
+ }
+
+ private static IssuerAndSerialNumber getIssuerAndSerialNumber(SmimeKey smimeKey) {
+ X509Certificate certificate = smimeKey.getCertificate();
+ BigInteger serialNumber = certificate.getSerialNumber();
+ X500Name issuerName = new X500Name(certificate.getIssuerX500Principal().getName());
+ return new IssuerAndSerialNumber(issuerName, serialNumber);
+ }
+
+ private static JcaCertStore getCertificateStore(SmimeKey smimeKey) throws CertificateEncodingException {
+ Certificate[] certificateChain = smimeKey.getCertificateChain();
+ X509Certificate certificate = smimeKey.getCertificate();
+
+ final List certificateList;
+ if (certificateChain != null && certificateChain.length > 0) {
+ certificateList = Arrays.asList(certificateChain);
+ } else {
+ certificateList = new ArrayList<>();
+ certificateList.add(certificate);
+ }
+ return new JcaCertStore(certificateList);
+ }
+
+ /**
+ * Signs a MIME message and yields a new S/MIME signed MIME message.
+ *
+ * @param session The {@link Session} that is used in conjunction with the
+ * original {@link MimeMessage}.
+ * @param mimeMessage The original {@link MimeMessage} or {@link SMTPMessage} to be signed.
+ * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to
+ * sign the original message with.
+ * @return The new S/MIME signed {@link MimeMessage} or {@link SMTPMessage}.
+ */
+ public static T sign(Session session, T mimeMessage, SmimeKey smimeKey) {
+ return sign(session, mimeMessage, smimeKey, defaultSignatureAlgorithmName);
+ }
+
+ /**
+ * Signs a MIME message and yields a new S/MIME signed MIME message.
+ *
+ * @param session The {@link Session} that is used in conjunction with the
+ * original {@link MimeMessage}.
+ * @param mimeMessage The original {@link MimeMessage} or {@link SMTPMessage} to be signed.
+ * @param smimeKey The {@link SmimeKey} used to obtain the {@link PrivateKey} to
+ * sign the original message with.
+ * @param algorithmName The name of the signature algorithm to use. Must be an algorithm
+ * supported by the Bouncy Castle security provider.
+ * @return The new S/MIME signed {@link MimeMessage} or {@link SMTPMessage}.
+ */
+ public static T sign(Session session, T mimeMessage, SmimeKey smimeKey, String algorithmName) {
+ //noinspection unchecked
+ return (mimeMessage instanceof SMTPMessage)
+ ? sign(mimeMessage, (T) new SMTPMessage(session), smimeKey, algorithmName)
+ : sign(mimeMessage, (T) new MimeMessage(session), smimeKey, algorithmName);
+ }
+
+ private static T sign(T mimeMessage, T signedMessage, SmimeKey smimeKey, String algorithmName) {
+ try {
+ copyHeaderLines(mimeMessage, signedMessage);
+ copyContent(sign(extractMimeBodyPart(mimeMessage), smimeKey, algorithmName), signedMessage);
+ return signedMessage;
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ private static MimeBodyPart extractMimeBodyPart(MimeMessage mimeMessage) throws IOException, MessagingException {
+ Object content = mimeMessage.getContent();
+ UpdatableMimeBodyPart updateableMimeBodyPart = new UpdatableMimeBodyPart();
+ if (content instanceof Multipart) {
+ updateableMimeBodyPart.setContent((Multipart) content);
+ } else {
+ updateableMimeBodyPart.setContent(content, mimeMessage.getDataHandler().getContentType());
+ }
+ updateableMimeBodyPart.updateHeaders();
+ return updateableMimeBodyPart;
+ }
+
+ /**
+ * Checks the signature on an S/MIME signed MIME multipart.
+ *
+ * @param mimeMultipart The {@link MimeMultipart} to be checked.
+ * @return {@code true} if the multipart is correctly signed, {@code false}
+ * otherwise.
+ */
+ public static boolean checkSignature(MimeMultipart mimeMultipart) {
+ try {
+ return checkSignature(new SMIMESigned(mimeMultipart));
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ /**
+ * Checks the signature on an S/MIME signed MIME part (i.e. MIME message).
+ *
+ * @param mimePart The {@link MimePart} to be checked.
+ * @return {@code true} if the part is correctly signed, {@code false}
+ * otherwise.
+ */
+ public static boolean checkSignature(MimePart mimePart) {
+ try {
+ if (mimePart.isMimeType("multipart/signed")) {
+ return checkSignature(new SMIMESigned((MimeMultipart) mimePart.getContent()));
+ } else if (mimePart.isMimeType("application/pkcs7-mime") || mimePart.isMimeType("application/x-pkcs7-mime")) {
+ return checkSignature(new SMIMESigned(mimePart));
+ } else {
+ throw new SmimeException("Message not signed");
+ }
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ /**
+ * Checks a SMIMESigned to make sure that the signature matches.
+ */
+ private static boolean checkSignature(SMIMESigned smimeSigned) {
+ try {
+ boolean returnValue = true;
+
+ @SuppressWarnings("rawtypes")
+ Store certificates = smimeSigned.getCertificates();
+ Iterator signerInformations = smimeSigned.getSignerInfos().getSigners().iterator();
+
+ while (returnValue && signerInformations.hasNext()) {
+ SignerInformation signerInformation = signerInformations.next();
+ X509Certificate certificate = getCertificate(certificates, signerInformation.getSID());
+ SignerInformationVerifier verifier = getVerifier(certificate);
+ if (!signerInformation.verify(verifier)) {
+ returnValue = false;
+ }
+ }
+ return returnValue;
+
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ /**
+ * @param mimeMultipart The {@link MimeMultipart} to be checked.
+ * @return The subject / address to which the certificate was issued to. Email clients may use this to show
+ * {@code "Signed by: "}
+ */
+ public static String getSignedByAddress(MimeMultipart mimeMultipart) {
+ try {
+ return getSignedByAddress(new SMIMESigned(mimeMultipart));
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ /**
+ * @param mimePart The {@link MimePart} to be checked.
+ * @return The subject / address to which the certificate was issued to. Email clients may use this to show
+ * {@code "Signed by: "}
+ */
+ public static String getSignedByAddress(MimePart mimePart) {
+ try {
+ if (mimePart.isMimeType("multipart/signed")) {
+ return getSignedByAddress(new SMIMESigned((MimeMultipart) mimePart.getContent()));
+ } else if (mimePart.isMimeType("application/pkcs7-mime") || mimePart.isMimeType("application/x-pkcs7-mime")) {
+ return getSignedByAddress(new SMIMESigned(mimePart));
+ } else {
+ throw new SmimeException("Message not signed");
+ }
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ /**
+ * Returns the subject / address to which the certificate was issued to. Email clients may use this to show
+ * {@code "Signed by: "}
+ */
+ private static String getSignedByAddress(SMIMESigned smimeSigned) {
+ try {
+ @SuppressWarnings("rawtypes")
+ Store certificates = smimeSigned.getCertificates();
+
+ SignerInformation signerInformation = smimeSigned.getSignerInfos().getSigners().iterator().next();
+ X509Certificate certificate = getCertificate(certificates, signerInformation.getSID());
+ SignerInformationVerifier verifier = getVerifier(certificate);
+ X500Name x500name = verifier.getAssociatedCertificate().getSubject();
+ RDN cn = x500name.getRDNs(BCStyle.CN)[0];
+ return IETFUtils.valueToString(cn.getFirst().getValue());
+
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ private static X509Certificate getCertificate(@SuppressWarnings("rawtypes") Store certificates,
+ SignerId signerId) throws CertificateException {
+ @SuppressWarnings({"unchecked"})
+ X509CertificateHolder certificateHolder = (X509CertificateHolder) certificates.getMatches(signerId).iterator().next();
+ JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
+ certificateConverter.setProvider(BouncyCastleProvider.PROVIDER_NAME);
+ return certificateConverter.getCertificate(certificateHolder);
+ }
+
+ private static SignerInformationVerifier getVerifier(X509Certificate certificate) throws OperatorCreationException {
+ JcaSimpleSignerInfoVerifierBuilder builder = new JcaSimpleSignerInfoVerifierBuilder();
+ builder.setProvider(BouncyCastleProvider.PROVIDER_NAME);
+ return builder.build(certificate);
+ }
+
+ /**
+ * Returns the signed MIME body part of an S/MIME signed MIME multipart.
+ *
+ * @param mimeMultipart The {@link MimeMultipart} to be stripped off.
+ * @return The signed {@link MimeBodyPart} contained in the
+ * {@link MimeMultipart}.
+ */
+ public static MimeBodyPart getSignedContent(MimeMultipart mimeMultipart) {
+ try {
+ return new SMIMESigned(mimeMultipart).getContent();
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ /**
+ * Returns the signed MIME body part of an S/MIME signed MIME part (i.e. MIME
+ * message).
+ *
+ * @param mimePart The {@link MimePart} to be stripped off.
+ * @return The signed {@link MimeBodyPart} contained in the {@link MimePart}
+ * .
+ */
+ public static MimeBodyPart getSignedContent(MimePart mimePart) {
+ try {
+ if (mimePart.isMimeType("multipart/signed")) {
+ return new SMIMESigned((MimeMultipart) mimePart.getContent()).getContent();
+ } else if (mimePart.isMimeType("application/pkcs7-mime") || mimePart.isMimeType("application/x-pkcs7-mime")) {
+ return new SMIMESigned(mimePart).getContent();
+ } else {
+ throw new SmimeException("Message not signed");
+ }
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ /**
+ * Returns the S/MIME state of a MIME multipart.
+ *
+ * @param mimeMultipart The {@link MimeMultipart} to be checked.
+ * @return the {@link SmimeState} of the {@link MimeMultipart}.
+ */
+ public static SmimeState getStatus(MimeMultipart mimeMultipart) {
+ try {
+ return getStatus(new ContentType(mimeMultipart.getContentType()));
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ /**
+ * Returns the S/MIME state of a MIME part (i.e. MIME message).
+ *
+ * @param mimePart The {@link MimePart} to be checked.
+ * @return the {@link SmimeState} of the {@link MimePart}.
+ */
+ public static SmimeState getStatus(MimePart mimePart) {
+ try {
+ return getStatus(new ContentType(mimePart.getContentType()));
+ } catch (Exception e) {
+ throw handledException(e);
+ }
+ }
+
+ private static SmimeState getStatus(ContentType contentType) {
+ if (isSmimeSignatureContentType(contentType)) {
+ return SmimeState.SIGNED;
+ } else if (isSignatureSmimeType(contentType)) {
+ return SmimeState.SIGNED_ENVELOPED;
+ } else if (isSmimeEncryptionContenttype(contentType)) {
+ return SmimeState.ENCRYPTED;
+ } else {
+ return SmimeState.NEITHER;
+ }
+ }
+
+ private static boolean isSmimeEncryptionContenttype(ContentType contentType) {
+ String baseContentType = contentType.getBaseType();
+ return baseContentType.equalsIgnoreCase("application/pkcs7-mime")
+ || baseContentType.equalsIgnoreCase("application/x-pkcs7-mime");
+ }
+
+ private static boolean isSmimeSignatureContentType(ContentType contentType) {
+ String baseContentType = contentType.getBaseType();
+ String protocol = contentType.getParameter("protocol");
+ return baseContentType.equalsIgnoreCase("multipart/signed")
+ && protocol != null && isSmimeSignatureProtocoll(protocol);
+ }
+
+ private static boolean isSignatureSmimeType(ContentType contentType) {
+ String baseContentType = contentType.getBaseType();
+ return baseContentType.equalsIgnoreCase("application/x-pkcs7-mime")
+ && "signed-data".equals(contentType.getParameter("smime-type"));
+ }
+
+ private static boolean isSmimeSignatureProtocoll(String protocol) {
+ return protocol.equalsIgnoreCase("application/pkcs7-signature")
+ || protocol.equalsIgnoreCase("application/x-pkcs7-signature");
+ }
+
+ private static SmimeException handledException(Exception e) {
+ if (e instanceof SmimeException) {
+ return (SmimeException) e;
+ }
+ return new SmimeException(e.getMessage(), e);
+ }
}
diff --git a/src/test/java/org/simplejavamail/utils/mail/smime/SmimeUtilTest.java b/src/test/java/org/simplejavamail/utils/mail/smime/SmimeUtilTest.java
new file mode 100644
index 0000000..1d34764
--- /dev/null
+++ b/src/test/java/org/simplejavamail/utils/mail/smime/SmimeUtilTest.java
@@ -0,0 +1,125 @@
+package org.simplejavamail.utils.mail.smime;
+
+import jakarta.mail.MessagingException;
+import jakarta.mail.Session;
+import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeMessage;
+import jakarta.mail.internet.MimeMultipart;
+import org.bouncycastle.cms.CMSAlgorithm;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Security;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class SmimeUtilTest {
+
+ private static final String SignatureAlgorithmRsa = "SHA256withRSA";
+ private static final String SignatureAlgorithmRsaPss = "SHA256WITHRSAANDMGF1";
+ private SmimeKeyStore alicesKeyStore;
+ private SmimeKeyStore bobsKeyStore;
+ private Session mailSession;
+
+ @BeforeAll
+ void setup() throws MessagingException, KeyStoreException, NoSuchProviderException, CertificateException, IOException, NoSuchAlgorithmException {
+ Security.addProvider(new BouncyCastleProvider());
+ InputStream alicesKeystoreStream = this.getClass().getClassLoader().getResourceAsStream("alice.p12");
+ this.alicesKeyStore = new SmimeKeyStore(alicesKeystoreStream, "alice".toCharArray());
+ InputStream bobsKeystoreStream = this.getClass().getClassLoader().getResourceAsStream("bob.p12");
+ this.bobsKeyStore = new SmimeKeyStore(bobsKeystoreStream, "bob".toCharArray());
+
+ Properties sessionProps = System.getProperties(); // new Properties(); // Fake properties for a fake session
+ this.mailSession = Session.getDefaultInstance(sessionProps);
+ }
+
+ private MimeMessage createTestMessage(String from, String to) throws MessagingException {
+ MimeMessage testMessage = new MimeMessage(this.mailSession);
+ testMessage.setFrom(new InternetAddress(from));
+ testMessage.setRecipient(MimeMessage.RecipientType.TO, new InternetAddress(to));
+ testMessage.setSubject("This is a test email");
+ testMessage.setContent("This is some test content for the test email's body", "text/plain; charset=utf-8");
+ return testMessage;
+ }
+
+ @Test
+ void SuccessfullySignAndValidate() throws MessagingException, IOException {
+ MimeMessage testMessage = createTestMessage("alice@testcorp.com", "alice@testcorp.com");
+ SmimeKey alicesKey = this.alicesKeyStore.getPrivateKey("alice", "alice".toCharArray());
+ MimeMessage signedMessage = SmimeUtil.sign(this.mailSession, testMessage, alicesKey);
+ MimeMultipart multipartContent = (MimeMultipart) signedMessage.getContent();
+ assertEquals(SmimeState.SIGNED, SmimeUtil.getStatus(multipartContent));
+ boolean isSignatureValid = SmimeUtil.checkSignature(multipartContent);
+ assertTrue(isSignatureValid);
+ }
+
+ @Test
+ void SuccessfullyEnvelopeAndDecryptDefault() throws MessagingException {
+ MimeMessage testMessage = createTestMessage("alice@testcorp.com", "alice@testcorp.com");
+ SmimeKey alicesKey = this.alicesKeyStore.getPrivateKey("alice", "alice".toCharArray());
+ X509Certificate alicesCert = alicesKey.getCertificate();
+ MimeMessage encryptedMessage = SmimeUtil.encrypt(this.mailSession,
+ SmimeUtil.sign(this.mailSession, testMessage, alicesKey),
+ alicesCert);
+ assertEquals(SmimeState.ENCRYPTED, SmimeUtil.getStatus((encryptedMessage)));
+ MimeMessage decryptedMessage = SmimeUtil.decrypt(this.mailSession, encryptedMessage, alicesKey);
+ boolean isSignatureValid = SmimeUtil.checkSignature(decryptedMessage);
+ assertTrue(isSignatureValid);
+ }
+
+ @Test
+ void SuccessfullyEnvelopeAndDecrypt() throws MessagingException {
+ MimeMessage testMessage = createTestMessage("alice@testcorp.com", "alice@testcorp.com");
+ SmimeKey alicesKey = this.alicesKeyStore.getPrivateKey("alice", "alice".toCharArray());
+ X509Certificate alicesCert = alicesKey.getCertificate();
+ MimeMessage encryptedMessage = SmimeUtil.encrypt(this.mailSession,
+ SmimeUtil.sign(this.mailSession, testMessage, alicesKey, SignatureAlgorithmRsaPss),
+ alicesCert, KeyEncapsulationAlgorithm.RSA_OAEP_SHA256, CMSAlgorithm.AES256_CBC);
+ assertEquals(SmimeState.ENCRYPTED, SmimeUtil.getStatus((encryptedMessage)));
+ MimeMessage decryptedMessage = SmimeUtil.decrypt(this.mailSession, encryptedMessage, alicesKey);
+ boolean isSignatureValid = SmimeUtil.checkSignature(decryptedMessage);
+ assertTrue(isSignatureValid);
+ }
+
+ @Test
+ void AliceToBoEnvelopeAndDecrypt() throws MessagingException {
+ MimeMessage testMessage = createTestMessage("alice@testcorp.com", "bob@testcorp.com");
+ SmimeKey alicesKey = this.alicesKeyStore.getPrivateKey("alice", "alice".toCharArray());
+ SmimeKey bobsKey = this.bobsKeyStore.getPrivateKey("bob", "bob".toCharArray());
+ X509Certificate bobsCert = bobsKey.getCertificate();
+ MimeMessage encryptedMessage = SmimeUtil.encrypt(this.mailSession,
+ SmimeUtil.sign(this.mailSession, testMessage, alicesKey, SignatureAlgorithmRsaPss),
+ bobsCert, KeyEncapsulationAlgorithm.RSA_OAEP_SHA512, CMSAlgorithm.AES256_GCM);
+ assertEquals(SmimeState.ENCRYPTED, SmimeUtil.getStatus((encryptedMessage)));
+ MimeMessage decryptedMessage = SmimeUtil.decrypt(this.mailSession, encryptedMessage, bobsKey);
+ boolean isSignatureValid = SmimeUtil.checkSignature(decryptedMessage);
+ assertTrue(isSignatureValid);
+ }
+
+ @Test
+ void BobToAliceEnvelopeAndDecrypt() throws MessagingException {
+ MimeMessage testMessage = createTestMessage("bob@testcorp.com", "alice@testcorp.com");
+ SmimeKey bobsKey = this.bobsKeyStore.getPrivateKey("bob", "bob".toCharArray());
+ SmimeKey alicesKey = this.alicesKeyStore.getPrivateKey("alice", "alice".toCharArray());
+ X509Certificate alicesCert = alicesKey.getCertificate();
+ MimeMessage encryptedMessage = SmimeUtil.encrypt(this.mailSession,
+ SmimeUtil.sign(this.mailSession, testMessage, bobsKey, SignatureAlgorithmRsaPss),
+ alicesCert, KeyEncapsulationAlgorithm.RSA_OAEP_SHA384, CMSAlgorithm.AES192_CCM);
+ assertEquals(SmimeState.ENCRYPTED, SmimeUtil.getStatus((encryptedMessage)));
+ MimeMessage decryptedMessage = SmimeUtil.decrypt(this.mailSession, encryptedMessage, alicesKey);
+ boolean isSignatureValid = SmimeUtil.checkSignature(decryptedMessage);
+ assertTrue(isSignatureValid);
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/alice-certgen-rsa.sh b/src/test/resources/alice-certgen-rsa.sh
new file mode 100755
index 0000000..b7a2063
--- /dev/null
+++ b/src/test/resources/alice-certgen-rsa.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# This script can be used to generate a self-signed test-certificate for the fictional principal "Alice".
+# The certificate is issued on the basis of a standard RSA key-pair.
+
+### Set the openssl version to use.
+openssl_bin="/usr/local/opt/openssl@1.1/bin/openssl"
+account_name="alice"
+priv_key_name="${account_name}.priv"
+certificate_config_filename="${account_name}.cnf"
+validity_days=1825 # Five years, so the tests won't fail too soon.
+
+echo "Generating private RSA key"
+$openssl_bin genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:4096 -pkeyopt rsa_keygen_pubexp:65537 -out ${priv_key_name}.rsakey
+### Save the private key without password protection
+$openssl_bin rsa -in ${priv_key_name}.rsakey -out ${priv_key_name}.nopass.rsakey
+
+echo "Generating self-signed certificate..."
+$openssl_bin req -outform PEM -out ${account_name}.pem -key ${priv_key_name}.nopass.rsakey -keyform PEM -x509 -nodes -batch -days $validity_days -config $certificate_config_filename -pkeyopt rsa_keygen_bits:2048 -sha256
+
+echo "Generating .p12 file with certificate and private key..."
+$openssl_bin pkcs12 -export -in ${account_name}.pem -inkey ${priv_key_name}.nopass.rsakey -out ${account_name}.p12
diff --git a/src/test/resources/alice.cnf b/src/test/resources/alice.cnf
new file mode 100644
index 0000000..e7ba56a
--- /dev/null
+++ b/src/test/resources/alice.cnf
@@ -0,0 +1,34 @@
+[ req ]
+default_bits = 4096
+distinguished_name = req_distinguished_name
+x509_extensions = x509_ext
+string_mask = utf8only
+
+[ req_distinguished_name ]
+countryName = Country Name (2 letter code)
+countryName_default = AA
+countryName_min = 2
+countryName_max = 2
+stateOrProvinceName = State or Province Name (full name)
+stateOrProvinceName_default = Testprovince
+localityName = Locality Name (eg, city)
+localityName_default = Testtown
+0.organizationName = Organization Name (eg, company)
+0.organizationName_default = Testcorp
+commonName = Common Name
+commonName_default = Alice
+commonName_max = 64
+emailAddress = Email Address
+emailAddress_default = alice@testcorp.com
+emailAddress_max = 64
+
+# Section x509_ext is used when generating a self-signed certificate. I.e., openssl req -x509 ...
+[ x509_ext ]
+
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid,issuer
+
+basicConstraints = CA:FALSE
+keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
+extendedKeyUsage = clientAuth, emailProtection
+#subjectAltName = email:copy
\ No newline at end of file
diff --git a/src/test/resources/alice.p12 b/src/test/resources/alice.p12
new file mode 100644
index 0000000..e0e73b7
Binary files /dev/null and b/src/test/resources/alice.p12 differ
diff --git a/src/test/resources/bob-certgen-rsa.sh b/src/test/resources/bob-certgen-rsa.sh
new file mode 100755
index 0000000..91cbb49
--- /dev/null
+++ b/src/test/resources/bob-certgen-rsa.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# This script can be used to generate a self-signed test-certificate for the fictional principal "Bob".
+# The certificate is issued on the basis of a RSASSA-PSS key-pair.
+
+### Set the openssl version to use. Must be OpenSSL 1.1 for RSASSA-PSS support
+openssl_bin="/usr/local/opt/openssl@1.1/bin/openssl"
+account_name="bob"
+priv_key_name="${account_name}.priv"
+certificate_config_filename="${account_name}.cnf"
+validity_days=1825 # Five years, so the tests won't fail too soon.
+
+echo "Generating private RSASSA-PSS key"
+$openssl_bin genpkey -algorithm rsa-pss -pkeyopt rsa_keygen_bits:4096 -pkeyopt rsa_keygen_pubexp:65537 -pkeyopt rsa_pss_keygen_md:sha256 -pkeyopt rsa_pss_keygen_mgf1_md:sha256 -pkeyopt rsa_pss_keygen_saltlen:32 -out ${priv_key_name}.rsapsskey
+### Save the private key without password protection
+$openssl_bin rsa -in ${priv_key_name}.rsapsskey -out ${priv_key_name}.nopass.rsapsskey
+
+echo "Generating self-signed certificate..."
+$openssl_bin req -outform PEM -out ${account_name}.pem -key ${priv_key_name}.nopass.rsapsskey -keyform PEM -x509 -nodes -batch -days $validity_days -config $certificate_config_filename -pkeyopt rsa_keygen_bits:4096 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:32 -sigopt rsa_mgf1_md:sha256 -sha256
+
+echo "Generating .p12 file with certificate and private key..."
+$openssl_bin pkcs12 -export -in ${account_name}.pem -inkey ${priv_key_name}.nopass.rsapsskey -out ${account_name}.p12
diff --git a/src/test/resources/bob.cnf b/src/test/resources/bob.cnf
new file mode 100644
index 0000000..9a7fd64
--- /dev/null
+++ b/src/test/resources/bob.cnf
@@ -0,0 +1,34 @@
+[ req ]
+default_bits = 4096
+distinguished_name = req_distinguished_name
+x509_extensions = x509_ext
+string_mask = utf8only
+
+[ req_distinguished_name ]
+countryName = Country Name (2 letter code)
+countryName_default = BB
+countryName_min = 2
+countryName_max = 2
+stateOrProvinceName = State or Province Name (full name)
+stateOrProvinceName_default = Testprovince
+localityName = Locality Name (eg, city)
+localityName_default = Testtown
+0.organizationName = Organization Name (eg, company)
+0.organizationName_default = Testcorp
+commonName = Common Name
+commonName_default = Bob
+commonName_max = 64
+emailAddress = Email Address
+emailAddress_default = bob@testcorp.com
+emailAddress_max = 64
+
+# Section x509_ext is used when generating a self-signed certificate. I.e., openssl req -x509 ...
+[ x509_ext ]
+
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid,issuer
+
+basicConstraints = CA:FALSE
+keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
+extendedKeyUsage = clientAuth, emailProtection
+#subjectAltName = email:copy
\ No newline at end of file
diff --git a/src/test/resources/bob.p12 b/src/test/resources/bob.p12
new file mode 100644
index 0000000..030791a
Binary files /dev/null and b/src/test/resources/bob.p12 differ