Der Server (Apache / PHP) meldet: SSL_CLIENT_VERIFY: FAILED:unable to verify the first certificate
Im Apache ist das Root-CA-Zertifikat richtig eingetragen:
SSLCACertificateFile "conf/ssl.crt/testrootca-crt.pem"
Die Zertifikats-Kette besteht aus einem Client-Zertifikat und einem Intermediate-CA-Zertifikat. Das root-Zertifikat ist selbst erstellt und selbst signiert.
Bei einem vergleichbaren Java-Client sowie einem Python-Client kann der Server das erste Zertifikat erfolgreich verifizieren und PHP meldet: SSL_CLIENT_VERIFY: SUCCESS
Weder ChatGPT noch Claude Sonnet4 konnten eine brauchbare Lösung anbieten und bei Stack Overflow habe ich auch nichts gefunden. Wo habe ich was falsch gemacht?
string certpath = "C:\\Austausch\\SSLneu2\\client-chain.p12";
// client-chain.p12 enthält das Client-Zertifikat und den Client-Private-Key
// und das Intermediate-CA Zertifikat
byte[] rawCert = File.ReadAllBytes(certpath);
X509Certificate2Collection certificateCollection = X509CertificateLoader.LoadPkcs12Collection(
rawCert,
"xxxxx",
X509KeyStorageFlags.Exportable
);
// Collection damit füllen:
var handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.ClientCertificates.AddRange(certificateCollection);
// hier dann der HTTPS Aufruf:
using HttpClient client = new HttpClient(handler);
string url = "https://localhost/mbbsim/test4.php?params=dotnet"; // Beispiel-API
try
{
HttpResponseMessage response = await client.GetAsync(url);
response.EnsureSuccessStatusCode(); // Exception, wenn der Statuscode nicht erfolgreich ist
string responseBody = await response.Content.ReadAsStringAsync();
Console.WriteLine(responseBody);
}
…
Ich sehe prinzipiell keinen Fehler hier im C# Code.
Sofern Dein p12 den Private Key und das Intermediate beinhaltet, sollte das klappen. Mein Code sieht beim Laden ähnlich aus:
X509Certificate2 cert = new("my.p12", "passwort", X509KeyStorageFlags.EphemeralKeySet);
handler.ClientCertificates.Add(cert);
Restlicher Code bei mir ist funktional identisch.
Wie sieht denn Dein funktionierender PHP Code aus?
Du kannst das auch alles mit openssl oder curl verifizieren. So können wir aktuell nichts vergleichen, was Du eigentlich machst.
Hier das PHP Code-Snippet, das den Subject und den Issuer des Client-Zertifikats ausgibt sowie den Verify-Status ($_SERVER["SSL_CLIENT_VERIFY"]).
Der Verify-Status sollte SUCCESS enthalten.
<?php
$methode = $_SERVER['REQUEST_METHOD'];
$ausgabe = "";
if ($methode === 'GET') {
// The request is using the GET method
$ausgabe = "PHP Test1 GET: ";
if (!empty($_REQUEST["params"])) {
$ausgabe .= " ".$_REQUEST["params"]."<br>\n";
if (!empty($_SERVER["SSL_CLIENT_S_DN"])) $ausgabe .= " SSL_CLIENT_S_DN: ".$_SERVER["SSL_CLIENT_S_DN"]."<br>\n";
if (!empty($_SERVER["SSL_CLIENT_I_DN"])) $ausgabe .= " SSL_CLIENT_I_DN: ".$_SERVER["SSL_CLIENT_I_DN"]."<br>\n";
if (!empty($_SERVER["SSL_CLIENT_VERIFY"])) $ausgabe .= " SSL_CLIENT_VERIFY: ".$_SERVER["SSL_CLIENT_VERIFY"]."<br>\n";
}
}
?>
<html>
<body>
menno
<?=$ausgabe?><br>
</body>
</html>
Habs verwechselt, bitte zeig einen vergleichbaren Client
Bei einem vergleichbaren Java-Client sowie einem Python-Client kann der Server das erste Zertifikat erfolgreich verifizieren und PHP meldet: SSL_CLIENT_VERIFY: SUCCESS
Hier der vergleichbare Java-Client, der die gleiche P12-Datei mit Client-Zertifikat, Private Key und Intermediate-CA-Zertifikat nutzt wie das C#-Pendant.
Alles etwas umständlicher als in der C#-Welt.
Dieser Client verifiziert zusätzlich das Server-Zertifikat gegen das eigene root-Zertifikat.
import java.io.*;
import java.net.*;
import javax.net.ssl.*;
import java.util.*;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
public class Main {
public static void main(String[] args)
{
try {
String keyPassphrase = "xxxxx";
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
KeyStore keyStore = KeyStore.getInstance("PKCS12");
// p12-Datei mit Chain aus Client-Zertifikat mit Private Key und Intermediate-CA-Zertifikat
keyStore.load(new FileInputStream("C:\\Austausch\\SSLneu2\\client-chain.p12"), keyPassphrase.toCharArray());
// zusätzlich Server-Zertifikat mit eigenem CA-Zertifikat verifizieren
// 1. CA-Zertifikat aus PEM-Datei laden
CertificateFactory cf = CertificateFactory.getInstance("X.509");
FileInputStream fis = new FileInputStream("C:\\Austausch\\SSLneu2\\testrootca-crt.pem");
X509Certificate caCert = (X509Certificate) cf.generateCertificate(fis);
// 2. Neuen leeren KeyStore anlegen
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null); // leer initialisieren
ks.setCertificateEntry("myCA", caCert);
// 3. TrustManagerFactory mit unserem KeyStore initialisieren
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
// 4. Ersten TrustManager ziehen und akzeptierte Issuer ausgeben
X509TrustManager tm = (X509TrustManager) tmf.getTrustManagers()[0];
for (X509Certificate cert : tm.getAcceptedIssuers()) {
System.out.println("Akzeptierte CA: " + cert.getSubjectDN());
}
keyManagerFactory.init(keyStore, keyPassphrase.toCharArray());
KeyManager[] kms = keyManagerFactory.getKeyManagers();
// Install the trust manager
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(kms, tmf.getTrustManagers(), new java.security.SecureRandom()); // nur das eigene CA Zertifikat akzeptieren
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostName, SSLSession session) {
return true;
}
});
URL url;
url = new URL("https://localhost/mbbsim/test4.php?params=Java"); // PHP Testmock
String charset = "UTF-8";
SSLParameters sslParameters = new SSLParameters();
List sniHostNames = new ArrayList(1);
sniHostNames.add(new SNIHostName(url.getHost()));
sslParameters.setServerNames(sniHostNames);
SSLSocketFactory wrappedSSLSocketFactory = new SSLSocketFactoryWrapper(sslContext.getSocketFactory(), sslParameters);
HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
connection.setRequestProperty("Accept-Charset", charset);
InputStream response = connection.getInputStream();
try (Scanner scanner = new Scanner(response)) {
String responseBody = scanner.useDelimiter("\\A").next();
System.out.println(responseBody);
}
}
catch (java.net.MalformedURLException e)
{
System.out.println(e);
}
catch (java.security.NoSuchAlgorithmException e)
{
System.out.println(e);
}
catch (java.security.KeyManagementException e)
{
System.out.println(e);
}
catch (Exception e)
{
System.out.println("Fehler"+e);
}
}
}
Verwende mal curl oder openssl zur Verifizierung Deines Endpunkts und des Cert-Files.
Dein Java Code ist fundamental anders als Dein C# Code inkl. anderer funktionsweise.
Der c# Client bekommt nur eine Verbindung zum Server, wenn in Apache
SSLVerifyClient optional_no_ca
definiert ist. Bei SSLVerifyClient optional oder SSLVerifyClient required wird die Verbindung abgelehnt.
Wenn der C#-Client auf TLS1.2 (statt 1.3) umgestellt wird, meldet der PHP-Server statt FAILED jetzt GENEROUS.
Bei Java und Python ist es unerheblich, welche TLS-Version eingestellt ist, es wird immer SUCCESS gemeldet.
Ich habe die Verbindungsdaten sowohl vom C#-Client als auch vom Java-Client und vom Python-Client mit Wireshark verglichen (jeweils mit TLS1.2). Bei allen Clients wurden beim Server 2 Zertifikate empfangen, nämlich das Client-Zertifikat und das Intermediate-Zertifikat. Daher ist für mich nicht verständlich, warum sich C# anders verhält als Python oder Java.
Problem gelöst!
Nachdem ich das eigene Root-Zertifikat mit der Management-Konsole unter "Vertrauenswürdige Stammzertifizierungsstellen" und das Intermediate-Zertifikat unter "Zwischenzertifizierungsstellen" eingetragen habe, klappt alles wie geplant.