Certificate pinning zabranjuje HTTP klijentu u vašoj .NET MAUI aplikaciji da prihvati TLS certifikat koji nije unaprijed poznat, čak i ako ga je potpisalo sistemski pouzdano tijelo za izdavanje certifikata. Rezultat je da napadač koji kontrolira mrežni promet — ili koji je instalirao vlastiti root CA na uređaj korisnika — ne može dešifrirati HTTPS komunikaciju između aplikacije i API servera.
Implementacija u .NET MAUI svodi se na postavljanje ServerCertificateCustomValidationCallback na HttpClientHandler i usporedbu SPKI hasha certifikata poslužitelja s unaprijed poznatim vrijednostima. Cijela ta logika registrira se jednom u MauiProgram.cs i primjenjuje na svaki HTTP zahtjev bez ikakve promjene u ostatku koda.
Pinning certifikata nasuprot pinningu javnog ključa
Postoje dva temeljna pristupa pinningu:
- Pinning kompletnog certifikata — uspoređuje cijeli certifikat (ili njegov SHA-256 otisak) s poznatom vrijednošću. Jednostavan za razumjeti, ali ima kratak rok trajanja: svaki put kada poslužitelj izda novi certifikat — čak i s istim javnim ključem — pin postaje nevažeći, što znači prisilno ažuriranje aplikacije.
- SPKI pinning (public key pinning) — uspoređuje SHA-256 hash SubjectPublicKeyInfo bloka koji je dio certifikata i koji sadržava samo javni ključ bez podataka o valjanosti. Sve dok poslužiteljev administrator zadržava isti par ključeva i obnavlja certifikat s njim, pin ostaje valjan bez ikakve promjene u aplikaciji.
Za mobilne aplikacije SPKI pinning je gotovo uvijek bolji izbor. Jedini razlog za odabir pinninga kompletnog certifikata jest ako imate strogi zahtjev da aplikacija prestane raditi čim se promijeni i sam certifikat — ne samo ključ. U svim ostalim slučajevima SPKI pristup smanjuje operativni teret upravljanja certifikatima bez žrtvovanja sigurnosti.
Izračun SPKI hasha
Prije pisanja ijednog retka C# koda potrebno je znati što pinati. SPKI hash dobiva se izravno s TLS certifikata koji poslužuje vaš API. Najbrži način je OpenSSL naredbeni redak koji se može pokrenuti s bilo kojeg razvojnog ili CI/CD računala:
openssl s_client -connect api.primjer.hr:443 </dev/null 2>/dev/null \
| openssl x509 -pubkey -noout \
| openssl pkey -pubin -outform der \
| openssl dgst -sha256 -binary \
| openssl base64Naredba se spaja na poslužitelj, izvlači javni ključ, kodira ga u DER format, izračuna SHA-256 hash i ispisuje Base64 reprezentaciju. Taj niz znakova je vaš SPKI pin. Pokrenite je i za produkcijski i za staging certifikat i zabilježite oba rezultata.
Ako radite s certifikatom koji imate lokalno kao datoteku, možete izračunati isti hash u C# kodu pomoću sljedeće pomoćne funkcije:
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
static string ComputeSpkiPin(X509Certificate2 cert)
{
AsymmetricAlgorithm? key =
cert.GetRSAPublicKey() as AsymmetricAlgorithm ??
(AsymmetricAlgorithm?)cert.GetECDsaPublicKey();
if (key is null)
throw new NotSupportedException("Nepodržana vrsta ključa.");
using (key)
{
byte[] spki = key.ExportSubjectPublicKeyInfo();
byte[] hash = SHA256.HashData(spki);
return Convert.ToBase64String(hash);
}
}ExportSubjectPublicKeyInfo() vraća DER-kodirani SubjectPublicKeyInfo niz koji je identičan onome koji OpenSSL koristi u gornjoj naredbi. Izlaz iz C# koda i izlaz OpenSSL naredbe trebaju biti identični Base64 stringovi za isti certifikat — to je korisna provjera ispravnosti prije nego što pin uvrstite u aplikaciju.
Implementacija s HttpClientHandler i DI
Sva logika pinninga smještena je u jednu statičnu klasu koja kreira konfigurirani HttpClientHandler. Pinove pohranite kao konstante neposredno u kodu ili ih učitajte iz konfiguracije pri pokretanju aplikacije — bitno je da su dostupni prije prvog HTTP zahtjeva:
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
public static class CertificatePinning
{
private static readonly HashSet<string> Pins =
new(StringComparer.Ordinal)
{
"PRIMARNI_BASE64_SPKI_HASH=",
"BACKUP_BASE64_SPKI_HASH=",
};
public static HttpClientHandler CreateHandler() =>
new()
{
ServerCertificateCustomValidationCallback = ValidatePin
};
private static bool ValidatePin(
HttpRequestMessage _,
X509Certificate2? cert,
X509Chain? __,
SslPolicyErrors errors)
{
if (cert is null) return false;
return Pins.Contains(ComputeSpkiPin(cert));
}
private static string ComputeSpkiPin(X509Certificate2 cert)
{
AsymmetricAlgorithm? key =
cert.GetRSAPublicKey() as AsymmetricAlgorithm ??
(AsymmetricAlgorithm?)cert.GetECDsaPublicKey();
if (key is null) return string.Empty;
using (key)
{
byte[] spki = key.ExportSubjectPublicKeyInfo();
byte[] hash = SHA256.HashData(spki);
return Convert.ToBase64String(hash);
}
}
}Registracija u MauiProgram.cs koristi IHttpClientFactory koji je ugrađen u .NET dependency injection sustav. ConfigurePrimaryHttpMessageHandler prima factory koji se poziva svaki put kada IHttpClientFactory kreira novu instancu handlera:
builder.Services
.AddHttpClient("PinnedApi", client =>
{
client.BaseAddress = new Uri("https://api.primjer.hr/");
client.Timeout = TimeSpan.FromSeconds(30);
})
.ConfigurePrimaryHttpMessageHandler(CertificatePinning.CreateHandler);U servisu koji komunicira s API-jem ubrizgajte IHttpClientFactory i kreirajte klijenta s imenovanom konfiguracijom:
public class ApiService(IHttpClientFactory factory)
{
private readonly HttpClient _client = factory.CreateClient("PinnedApi");
public Task<string> GetDataAsync() =>
_client.GetStringAsync("/v1/data");
}Ako koristite typed clients umjesto imenovanih, obrazac ostaje isti — samo se AddHttpClient parametrizira tipom servisa:
builder.Services
.AddHttpClient<ApiService>(client =>
{
client.BaseAddress = new Uri("https://api.primjer.hr/");
})
.ConfigurePrimaryHttpMessageHandler(CertificatePinning.CreateHandler);Android Network Security Config
Android 7.0 (API razina 24) i noviji podržavaju deklarativni pristup pinningu kroz XML datoteku Network Security Config. Ovo je izvorni mehanizam koji ne zahtijeva C# kod i koji djeluje na razini operacijskog sustava, pokrivajući sav mrežni promet aplikacije uključujući WebView i eventualne native biblioteke koje ne koriste .NET HTTP stack.
Kreirajte datoteku Platforms/Android/Resources/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.primjer.hr</domain>
<pin-set expiration="2027-06-01">
<pin digest="SHA-256">PRIMARNI_BASE64_SPKI_HASH=</pin>
<pin digest="SHA-256">BACKUP_BASE64_SPKI_HASH=</pin>
</pin-set>
</domain-config>
</network-security-config>Zatim u Platforms/Android/AndroidManifest.xml dodajte referencu unutar <application> taga:
<application
android:networkSecurityConfig="@xml/network_security_config"
android:label="@string/app_name">
</application>Pinovi u XML datoteci moraju biti isti Base64 SPKI hashovi dobiveni OpenSSL naredbom — format je identičan onome koji se koristi u C# kodu. Jedna vrijednost vrijedi za oba pristupa.
Prednosti i ograničenja XML pristupa
XML pristup pokriva sve HTTP biblioteke koje koriste Android networking stack, što ga čini sveobuhvatnijim od HttpClientHandler pristupa za aplikacije s WebViewom ili native bibliotekama. Atribut expiration je opcionalan, ali preporučen: nakon isteka datuma Android ignorira pin i dopušta sve valjane TLS certifikate, što sprječava trajni ispad aplikacije u slučaju da rotacija certifikata nije provedena na vrijeme. Za čiste .NET MAUI aplikacije bez WebViewa dovoljan je isključivo HttpClientHandler pristup koji pokriva i iOS.
Rotacija certifikata i produkcijske zamke
Najveća opasnost pinninga nije implementacija, već upravljanje životnim ciklusom pinova. Pogrešno upravljanje rezultira time da nijedan korisnik ne može koristiti aplikaciju dok ne ažurira na novu verziju — i to se događa iznenada, točno u trenutku rotacije certifikata na poslužitelju.
Procedura sigurne rotacije
- Generirajte novi par ključeva na poslužitelju i pribavite novi certifikat, ali ga još ne aktivirajte.
- Izračunajte SPKI hash novog ključa i dodajte ga kao backup pin u aplikaciju zajedno s postojećim primarnim pinom.
- Objavite novu verziju aplikacije s oba pina i pričekajte da je dostatan broj korisnika preuzme.
- Rotirajte certifikat na poslužitelju — korisnici s novom verzijom nastavljaju raditi jer imaju backup pin koji odgovara novom certifikatu.
- Uklonite stari pin iz sljedeće verzije aplikacije.
Korisnici koji nikada ne ažuriraju aplikaciju izgubit će pristup u koraku 4. To je svjesna sigurnosna odluka: pinning pretpostavlja da ažurirana verzija aplikacije sadržava ispravne pinove, a zastarjela verzija ne zaslužuje povjerenje.
Logiranje grešaka pinninga
Kada ServerCertificateCustomValidationCallback vrati false, .NET baca HttpRequestException s unutarnjim AuthenticationException. Uhvatite tu iznimku posebno od ostalih mrežnih grešaka i zabilježite je u vaš sustav za praćenje:
try
{
var data = await _client.GetStringAsync("/v1/data");
}
catch (HttpRequestException ex)
when (ex.InnerException is AuthenticationException)
{
_logger.LogError(ex, "TLS provjera neuspješna za {Url}", _client.BaseAddress);
throw new SecurityException("Veza s poslužiteljem nije sigurna.", ex);
}Nagli porast tih grešaka u produkciji ukazuje na moguće MITM napade, ali isto tako može značiti da je rotacija certifikata provedena bez prethodnog dodavanja backup pina. Postavljanje alertinga na ovu iznimku standardna je praksa u produkcijskim okruženjima.
Često postavljana pitanja
Zašto ServerCertificateCustomValidationCallback ne radi ispravno na iOS simulatoru?
Na iOS simulatoru .NET koristi drugačiji networking sloj nego na fizičkom uređaju. Uvijek testirajte certificate pinning na fizičkom iOS uređaju — simulator može propuštati ili ignorirati TLS provjere koje su aktivne u produkciji.
Mogu li koristiti self-signed certifikat za testiranje pinninga u razvoju?
Da. Generirajte self-signed certifikat, izračunajte njegov SPKI hash i dodajte ga u debug konfiguraciju. Koristite uvjetnu kompilaciju (#if DEBUG) kako biste osigurali da se razvojni pin ne uključi u release build.
Što ako API koristi certifikat koji potpisuje posredni CA?
Pinning možete primijeniti na certifikat krajnjeg poslužitelja (leaf), posredni CA ili root CA. Za API koji kontrolirate preporučeno je piniranje na leaf certifikat ili na javni ključ koji izravno kontrolirate. Piniranje na root CA pruža najmanji stupanj zaštite jer pokriva sve certifikate koje taj CA potpisuje.
Utječe li certificate pinning na performanse aplikacije?
Usporedba hasha odvija se samo jednom po TLS rukovanje (handshake). Uz HTTP/2 i IHttpClientFactory koji reciklira handler instance, overhead je zanemariv za tipičan mobilni API promet. Nikako ne blokirajte validaciju s asinkronim operacijama — callback mora biti sinkron.
Trebam li koristiti i HttpClientHandler i Android Network Security Config?
Nije nužno, ali kombinacija pruža dublju obranu. HttpClientHandler pokriva sve platforme u jednom C# kodu; Android Network Security Config pokriva i WebView i native biblioteke na Androidu. Za aplikacije bez WebViewa dovoljno je koristiti samo HttpClientHandler pristup.




