From b0cb2b8933a37ccb9b0d1de542feb38fa0da7268 Mon Sep 17 00:00:00 2001 From: andre Date: Tue, 17 Feb 2026 10:08:44 +0100 Subject: [PATCH] add certificate pinning support for Android --- README.md | 9 ++- .../webidmetaplugin/WebIdMetaPluginModule.kt | 57 ++++++++++++++++++- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 88ab75c..c66b625 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,6 @@ > > This repository contains a proof-of-concept React Native plugin and an example mobile application intended solely for technical evaluation. > -> Only the iOS integration has been verified to work at this time. -> The Android bridge and Android example setup exist but have not been tested and are expected to require additional adjustments before they can be considered functional. -> > This repository is **experimental**, **not formally approved**, **not officially released**, **not published as an npm package**, and **not endorsed by WebID for > production use**. > @@ -38,12 +35,14 @@ cp .env.example .env URL=https://test.webid-solutions.de USERNAME=your_username API_KEY=your_api_key -CERT_BASE64=On ANdroid it need to be SHA-PINS -# SHA_PINS=sha256/AAAA...,sha256/BBBB... +CERT_BASE64= ``` Edit the .env file and add your username and API key. +ℹ️ The provided CERT_BASE64 value is preconfigured for the test system and is valid until 2026-02-21. +In most cases, this value does not need to be modified if you intend to test against the test environment. + ### Install Dependencies From the repository root: diff --git a/android/src/main/java/com/webidmetaplugin/WebIdMetaPluginModule.kt b/android/src/main/java/com/webidmetaplugin/WebIdMetaPluginModule.kt index 723cd9e..c8386e4 100644 --- a/android/src/main/java/com/webidmetaplugin/WebIdMetaPluginModule.kt +++ b/android/src/main/java/com/webidmetaplugin/WebIdMetaPluginModule.kt @@ -24,6 +24,11 @@ import java.net.URI import com.facebook.react.bridge.UiThreadUtil import android.util.Log import com.google.gson.Gson +import java.io.ByteArrayInputStream +import java.security.MessageDigest +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import android.util.Base64 private const val TAG = "WebIdMetaPlugin" @@ -53,6 +58,53 @@ class WebIdMetaPluginModule( listenerCount = (listenerCount - count).coerceAtLeast(0) } + private fun toPinningShaKey(inputRaw: String): String { + val input = inputRaw.trim() + + if (input.startsWith("sha256/") || input.startsWith("sha1/")) return input + + val justValue = input.substringAfter("=", input).trim() + + val base64Cert = justValue + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace("\\s+".toRegex(), "") + + require(base64Cert.isNotEmpty()) { "Certificate input is empty after normalization" } + + val certBytes = try { + Base64.decode(base64Cert, Base64.DEFAULT) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Certificate is not valid Base64 (DER/PEM).", e) + } + + val x509 = try { + val cf = CertificateFactory.getInstance("X.509") + cf.generateCertificate(ByteArrayInputStream(certBytes)) as X509Certificate + } catch (e: Exception) { + throw IllegalArgumentException("Could not parse X.509 certificate bytes.", e) + } + + val spkiBytes = x509.publicKey.encoded + val digest = MessageDigest.getInstance("SHA-256").digest(spkiBytes) + val pinB64 = Base64.encodeToString(digest, Base64.NO_WRAP) + + return "sha256/$pinB64" + } + + private fun toPinningShaKeys(inputs: List): Array = + inputs + .map { it.trim() } + .filter { it.isNotEmpty() } + .map { toPinningShaKey(it) } + .toTypedArray() + + private fun shortPreview(s: String): String { + val t = s.trim() + if (t.length <= 12) return t + return "${t.take(6)}...${t.takeLast(6)}" + } + @ReactMethod fun createMetaPlugin( uri: String, @@ -69,10 +121,13 @@ class WebIdMetaPluginModule( val shaPinsList = shaPins.toArrayList().map { it.toString() } val pluginsList = plugins.toArrayList().map { it.toString() } + val pinningShaKeys = toPinningShaKeys(shaPinsList) + Log.i(TAG, "createMetaPlugin pins(mapped) len=${pinningShaKeys.size} values=${pinningShaKeys.map { shortPreview(it) }}") + try { val environment = WebIdSdkEnvironment( URI.create(cleanUri), - *shaPinsList.toTypedArray() + *pinningShaKeys ) val selectedPlugins = ArrayList()