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/…).

plaintext
    +-------------------------------------------+
|  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:

plaintext
    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.
plaintext
    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.

plaintext
                             ┌────────────────────────────┐
                         │        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):

java
    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

java
    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:

java
    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:

java
    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.

java
    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);
}