Digitale Signaturen mit S/MIME Java
22. October 2025
Eine kurze Zusammenfassung wie man in Java (bouncy-castle) digitale Signaturen prüft.
Die grundlegende MIME-Struktur besteht aus einem Header und einem Body. Eine Übersicht über die Header findet sich in RFC 2076. Es gibt jedoch noch viele Weitere, wie beispielsweise RFC 2047. Der Body kann aus mehreren Nachrichten bestehen (Content-Type: multipart/…).
+-------------------------------------------+
| Header |
+-------------------------------------------+
| Body (Inhalt) |
+-------------------------------------------+
S/MIME
S/MIME wird für die Verschlüsselung und digitale Signaturen verwendet. Beginnen wir mit digitalen Signaturen. Es gibt zwei Arten, wie die digitale Signatur bei einem MIME Mail angewandt wird:
multipart/signed – “clear-signed” Nachricht
- Die Nachricht bleibt im Klartext lesbar, d. h. der Empfänger (oder auch ein Mailserver) kann den Text sehen, ohne ihn zu entschlüsseln.
- Die Signatur wird als separater MIME-Part angehängt.
Beispiel:
Content-Type: multipart/signed;
protocol="application/pkcs7-signature";
micalg=sha256;
boundary="----=_Part_1234_5678"
------=_Part_1234_5678
Content-Type: text/plain; charset=utf-8
Hallo Max,
das ist eine signierte Nachricht.
Viele Grüße,
Anna
------=_Part_1234_5678
Content-Type: application/pkcs7-signature; name="smime.p7s"
Content-Transfer-Encoding: base64
MIAGCSqGSIb3DQEHAqCAMIACAQExDjAMBggqhkiG9w0CBQUAMIIB...
------=_Part_1234_5678--
application/pkcs7-mime – “opaque signed” Nachricht
- Die Nachricht (Text und Signatur) ist in einem binären Container verpackt.
- Der Inhalt ist nicht direkt lesbar, erst nach dem Entpacken/Verifizieren.
Content-Type: application/pkcs7-mime;
smime-type=signed-data;
name="smime.p7m"
Content-Transfer-Encoding: base64
MIICdgYJKoZIhvcNAQcCoIICZzCCAmMCAQEx...
Die Certificate-Chain
Wie funktioniert jetzt aber die Validierung? Das E-Mail beinhaltet eine vollständige Kette:
1. End-Entity-Zertifikat (Signer-Zertifikat)
- Das Zertifikat des Absenders, der die E-Mail signiert.
- Enthält:
- den öffentlichen Schlüssel des Absenders
- den Subject Name (z. B. CN=Anna Example, E=anna@example.com)
- und die Signatur einer ausstellenden CA (nächste Stufe)
- Wird immer in der E-Mail mitgeschickt.
- Dieses Zertifikat wird verwendet, um die Signatur zu prüfen.
2. Intermediate CA (Zwischenzertifikate)
- Ein oder mehrere Zertifikate, die das Signer-Zertifikat ausgestellt haben.
- Mitgeschickt, damit der Empfänger die Chain aufbauen kann, auch wenn er diese CA noch nicht kennt.
3. Root CA (Vertrauensanker)
- Das oberste Zertifikat in der Kette.
- Selbstsigniert (Issuer = Subject).
- Muss nicht unbedingt mitgeschickt werden (Signatur ist ja schon einen Schritt vorher bekannt)
- Wichtig: nicht blind vertrauen
Wichtig ist, dass man die Rolle des CA versteht: Eine Certificate Authority (CA) ist eine vertrauenswürdige Instanz, die digitale Zertifikate ausstellt, überprüft und verwaltet. Diese Zertifikate bestätigen, dass ein bestimmter öffentlicher Schlüssel zu einer bestimmten Identität gehört.
┌────────────────────────────┐
│ Root CA │
│ "Trust Anchor" │
│ z. B. DigiCert Global Root│
└────────────┬───────────────┘
│
(signiert das Intermediate)
│
┌────────────▼───────────────┐
│ Intermediate CA │
│ z. B. DigiCert Secure │
│ Email CA │
└────────────┬───────────────┘
│
(signiert das End-Entity-Zertifikat)
│
┌────────────▼───────────────┐
│ End-Entity / Signer │
│ CN=Anna Example │
│ E=anna@example.com │
│ → Signiert die E-Mail │
└────────────┬───────────────┘
│
(signiert die Nachricht)
│
┌────────────▼───────────────┐
│ S/MIME-E-Mail │
│ Enthält: │
│ • Signaturdaten │
│ • Signer-Zertifikat │
│ • Intermediate(s) │
│ • (optional Root CA) │
└────────────────────────────┘
JDK
Einer der wichtigsten Klassen ist PKIXParameters.
Parameters used as input for the PKIX CertPathValidator algorithm.
Aber die Reise beginnt etwas früher: in Java werden etwas über 100 trusted Root CAs mitgeliefert (findet man unter $JAVA_HOME/lib/security/cacerts). Alternativ gibt es auch im Netz solche Listen: Common CA Database (CCADB). Die Standard Root CA lädt man sich mit (https://www.baeldung.com/java-list-trusted-certificates):
String cacertsPath = System.getProperty("java.home") + "/lib/security/cacerts";
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (FileInputStream in = new FileInputStream(cacertsPath)) {
keyStore.load(in, "changeit".toCharArray());
}
return new PKIXParameters(keyStore);
Wer nicht mit Pfaden arbeiten will, kann auch den Umweg über
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
List<TrustManager> trustManagers = Arrays.asList(trustManagerFactory.getTrustManagers());
List<X509Certificate> certificates = trustManagers.stream()
.filter(X509TrustManager.class::isInstance)
.map(X509TrustManager.class::cast)
.map(trustManager -> Arrays.asList(trustManager.getAcceptedIssuers()))
.flatMap(Collection::stream)
.collect(Collectors.toList());
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
for (X509Certificate certificate : certificates) {
keyStore.setCertificateEntry(certificate.getSubjectX500Principal().getName(), certificate);
}
PKIXParameters pkixParameters = new PKIXParameters(keyStore);
gehen.
Für die Validierung der Certificate-Chain gibt es folgende weitere Überprüfungen (siehe https://docs.spring.io/spring-security-saml/docs/current/reference/html/security.html#configuration-security-profiles-pkix):
| Mechanismus | Langname | Zweck | Typ | Netzwerkzugriff | Java-Property / API | Bemerkung |
|---|---|---|---|---|---|---|
| Revocation | Zertifikatssperrung | Prüfen, ob Zertifikat widerrufen ist | Konzept | optional | pkixParams.setRevocationEnabled(true) |
nutzt CRL oder OCSP |
| CRL | Certificate Revocation List | Liste gesperrter Zertifikate | Datei (statisch) | Ja (HTTP/LDAP) | com.sun.security.enableCRLDP |
groß, aber einfach |
| AIA | Authority Information Access | Quelle für Intermediate-CAs & OCSP-URLs | Zertifikatsfeld | Ja | com.sun.security.enableAIAcaIssuers |
lädt CA-Zertifikate automatisch nach |
| OCSP | Online Certificate Status Protocol | Online-Abfrage, ob Zertifikat gültig ist | Protokoll (HTTP) | Ja | ocsp.enable |
aktuell, aber datenschutzrelevant |
Man sieht also, dass viele Netzwerkzugriff brauchen. Abschalten kann man alles per:
pkixParameters.setRevocationEnabled(false);
java.security.Security.setProperty("com.sun.security.enableCRLDP", "false");
java.security.Security.setProperty("com.sun.security.enableAIAcaIssuers", "false");
java.security.Security.setProperty("ocsp.enable", "false");
Die Validierung
Die Validierung mit bouncy-castle ist sehr einfach. Den vollständigen Source findet man auf github - 99% des Codes dient aber nur der “Ausgabe” des Resultats - der Kern der Validierung:
SignedMailValidator signedMailValidator = new SignedMailValidator(message, pkixParameters);
for (SignerInformation signerInformation : signedMailValidator.getSignerInformationStore().getSigners()) {
SignedMailValidator.ValidationResult validationResult = signedMailValidator.getValidationResult(signerInformation);
PKIXCertPathReviewer certificatePathReviewer = validationResult.getCertPathReview();
boolean validSignature = validationResult.isValidSignature();
boolean validPath = certificatePathReviewer != null && certificatePathReviewer.isValidCertPath();
}
Das Signieren
Das Signieren geht ebenfalls einfach mit bouncy-castle. In der PDF einfach nach “Creating and Verifying an S/MIME Signed Multipart” suchen. Dort sieht man die Funktion createSignedMultipart.
private static ASN1EncodableVector generateSignedAttributes() {
ASN1EncodableVector signedAttrs = new ASN1EncodableVector();
SMIMECapabilityVector caps = new SMIMECapabilityVector();
caps.addCapability(SMIMECapability.aES128_CBC);
caps.addCapability(SMIMECapability.aES192_CBC);
caps.addCapability(SMIMECapability.aES256_CBC);
signedAttrs.add(new SMIMECapabilitiesAttribute(caps));
return signedAttrs;
}
public static MimeMultipart createSignedMultipart(PrivateKey signingKey, X509Certificate signingCert, MimeBodyPart message)
throws GeneralSecurityException, OperatorCreationException, SMIMEException, IOException {
List<X509Certificate> certList = new ArrayList<X509Certificate>();
certList.add(signingCert);
Store certs = new JcaCertStore(certList);
ASN1EncodableVector signedAttrs = generateSignedAttributes();
signedAttrs.add(new Attribute(CMSAttributes.signingTime, new DERSet(new Time(new Date()))));
SMIMESignedGenerator gen = new SMIMESignedGenerator();
gen.addSignerInfoGenerator(new JcaSimpleSignerInfoGeneratorBuilder()
.setProvider("BCFIPS")
.setSignedAttributeGenerator(new AttributeTable(signedAttrs))
.build("SHA384withRSAandMGF1", signingKey, signingCert));
gen.addCertificates(certs);
return gen.generate(message);
}