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.
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.
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.
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);
}
}
}
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>
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);
}
…
Das Problem besteht darin, dass es die Bibliotheken nicht im .lib-Format gibt, sondern im .a-Format (für Linux), obwohl der Download explizit für Windows angeboten wird
Ich möchte ein C++-Programm erstellen, das HTTP(S)-Zugriffe durchführen und dafür möchte ich die cURL-Bibliothek verwenden.
ich habe cURL von der offiziellen Webseite als zip-File heruntergeladen und entpackt. Jetzt möchte ich cURL zu einem Projekt hinzufügen, aber alle Beschreibungen, die ich recherchiert habe, funktionieren nicht.
Ich verwende Visual Studio 2022 auf einem Windows 11 Laptop.
Wer kann mir Tipps geben?
Vielen herzlichen Dank für deine Hilfe!
Ich habe nur den Konstruktor geändert, damit läuft das kleine Programm sowohl mit RSA- als auch mit ECDsa-Zertifikat. Kleine Ursache, große Wirkung!
byte[] rawCert = File.ReadAllBytes(certpath);
X509Certificate2 certificate = new X509Certificate2(rawCert, "David", X509KeyStorageFlags.PersistKeySet);
Danke für die Antwort und sorry für die fehlenden Informationen.
Hier der Code, der mit einem RSA-Zertifikat funktioniert:
using System;
using System.IO;
using System.Net.Http;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
namespace ConsoleApp2
{
class Program
{
static String retvalue;
static void Main(string[] args)
{
Console.WriteLine("https://localhost/mbbsim/test4.php?params=show mit SSL und Zertifikat aufgerufen");
retvalue = new String(GetValue());
Console.WriteLine(retvalue);
Console.ReadKey();
}
static string GetValue()
{
var retstring = "";
//string certpath = "C:\\temp\\CAItest\\signed.p12";
string certpath = "C:\\Austausch\\SSL\\XXXXXX_Client_EXAM.p12";
Console.WriteLine(certpath);
X509Certificate2 certificate = new X509Certificate2(certpath, "David", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
Console.WriteLine("neues X509certificate2 erzeugt");
// Fehler in der Server-Zertifikatskette ignorieren
var handler = new HttpClientHandler();
Console.WriteLine("neuen HttpClientHandler angelegt");
handler.ClientCertificates.Add(certificate);
Console.WriteLine("certificate hinzugefügt");
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
Console.WriteLine("ClientCertificateOption.Automatic definiert");
handler.ServerCertificateCustomValidationCallback =
(httpRequestMessage, cert, cetChain, policyErrors) =>
{
return true;
};
Console.WriteLine("Callback eingerichtet");
try
{
var client = new HttpClient(handler);
Console.WriteLine("neues HttpClient Objekt erzeugt");
var webRequest = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mbbsim/test4.php?params=show");
var response = client.Send(webRequest);
var reader = new StreamReader(response.Content.ReadAsStream());
retstring = reader.ReadToEnd();
}
catch (System.Net.Http.HttpRequestException e)
{
// Win32Exception: Die Anmeldeinformationen, die dem Paket übergeben wurden, wurden nicht erkannt
retstring = "Fehler " + e.Message;
if (e.InnerException != null)
Console.WriteLine("Inner exception: {0}", e.InnerException);
}
return retstring;
}
}
}
Bei Verwendung des hier auskommentierten EC-Zertifikats "signed.p12" kommt es zur Exception mit der Info, dass die Anmeldeinformationen nicht erkannt wurden