sdk: expose RTCCertificate.getFingerprints() on Android described in https://w3c.github.io/webrtc-pc/#dom-rtccertificate-getfingerprints based on https://github.com/threema-ch/webrtc-build-docker/blob/master/patches/expose-certificate-fingerprint.patch Co-Authored-by: lgr@threema.ch Bug: webrtc:528992529 Change-Id: I19f5dc0854bfbe0646548414a1c1160cb0386b49 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/486621 Reviewed-by: ZoƩ Lepaul <zlep@webrtc.org> Reviewed-by: Guido Urdaneta <guidou@webrtc.org> Commit-Queue: Philipp Hancke <philipp.hancke@googlemail.com> Cr-Commit-Position: refs/heads/main@{#48194}
diff --git a/sdk/android/BUILD.gn b/sdk/android/BUILD.gn index f6c3ec1..8858c05 100644 --- a/sdk/android/BUILD.gn +++ b/sdk/android/BUILD.gn
@@ -302,6 +302,7 @@ "api/org/webrtc/CandidatePairChangeEvent.java", "api/org/webrtc/CryptoOptions.java", "api/org/webrtc/DataChannel.java", + "api/org/webrtc/DtlsFingerprint.java", "api/org/webrtc/DtmfSender.java", "api/org/webrtc/FecControllerFactoryFactoryInterface.java", "api/org/webrtc/FrameDecryptor.java", @@ -1567,6 +1568,7 @@ "api/org/webrtc/CandidatePairChangeEvent.java", "api/org/webrtc/CryptoOptions.java", "api/org/webrtc/DataChannel.java", + "api/org/webrtc/DtlsFingerprint.java", "api/org/webrtc/DtmfSender.java", "api/org/webrtc/IceCandidate.java", "api/org/webrtc/IceCandidateErrorEvent.java",
diff --git a/sdk/android/api/org/webrtc/DtlsFingerprint.java b/sdk/android/api/org/webrtc/DtlsFingerprint.java new file mode 100644 index 0000000..35cfdb0 --- /dev/null +++ b/sdk/android/api/org/webrtc/DtlsFingerprint.java
@@ -0,0 +1,33 @@ +/* + * Copyright 2026 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +package org.webrtc; + +import org.jni_zero.CalledByNative; + +/** + * Fingerprint of a certificate, mirroring the WebIDL RTCDtlsFingerprint + * dictionary. See https://w3c.github.io/webrtc-pc/#dom-rtcdtlsfingerprint + */ +public class DtlsFingerprint { + /** The hash function used to compute the fingerprint, e.g. "sha-256". */ + public final String algorithm; + /** + * The fingerprint value as colon-separated hexadecimal, using the syntax of + * the fingerprint in RFC 4572 Section 5, e.g. "AB:CD:...". + */ + public final String value; + + @CalledByNative + public DtlsFingerprint(String algorithm, String value) { + this.algorithm = algorithm; + this.value = value; + } +}
diff --git a/sdk/android/api/org/webrtc/RtcCertificatePem.java b/sdk/android/api/org/webrtc/RtcCertificatePem.java index 640934f..f031fc3 100644 --- a/sdk/android/api/org/webrtc/RtcCertificatePem.java +++ b/sdk/android/api/org/webrtc/RtcCertificatePem.java
@@ -10,6 +10,9 @@ package org.webrtc; +import androidx.annotation.Nullable; +import java.util.Collections; +import java.util.List; import org.jni_zero.NativeMethods; /** @@ -41,6 +44,21 @@ } /** + * Returns the fingerprints of the certificate, mirroring the WebIDL + * RTCCertificate.getFingerprints(). The fingerprints are derived from the PEM + * representation; an empty list is returned if they cannot be computed. + * Native WebRTC tracks a single fingerprint per certificate, so the list + * holds at most one entry. See + * https://w3c.github.io/webrtc-pc/#dom-rtccertificate-getfingerprints + */ + public List<DtlsFingerprint> getFingerprints() { + DtlsFingerprint fingerprint = + RtcCertificatePemJni.get().getFingerprint(privateKey, certificate); + return fingerprint == null ? Collections.emptyList() + : Collections.singletonList(fingerprint); + } + + /** * Generate a new RtcCertificatePem with the default settings of KeyType = ECDSA and * expires = 30 days. */ @@ -74,5 +92,7 @@ @NativeMethods interface Natives { RtcCertificatePem generateCertificate(PeerConnection.KeyType keyType, long expires); + @Nullable + DtlsFingerprint getFingerprint(String privateKey, String certificate); } }
diff --git a/sdk/android/instrumentationtests/src/org/webrtc/RtcCertificatePemTest.java b/sdk/android/instrumentationtests/src/org/webrtc/RtcCertificatePemTest.java index 4127bb2..bcabb17 100644 --- a/sdk/android/instrumentationtests/src/org/webrtc/RtcCertificatePemTest.java +++ b/sdk/android/instrumentationtests/src/org/webrtc/RtcCertificatePemTest.java
@@ -13,6 +13,7 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.filters.SmallTest; +import java.util.List; import org.junit.Before; import org.junit.Test; import org.webrtc.PeerConnection; @@ -67,4 +68,42 @@ assertThat(rtcCertificate.privateKey).isNotEmpty(); assertThat(rtcCertificate.certificate).isNotEmpty(); } + + @Test + @SmallTest + public void testGeneratedCertificateHasFingerprint() { + RtcCertificatePem rtcCertificate = RtcCertificatePem.generateCertificate(); + List<DtlsFingerprint> fingerprints = rtcCertificate.getFingerprints(); + assertThat(fingerprints).hasSize(1); + + DtlsFingerprint fingerprint = fingerprints.get(0); + // The algorithm is an IANA hash function textual name, e.g. "sha-256". + assertThat(fingerprint.algorithm).matches("[a-z0-9\\-]+"); + // The value is colon-separated hexadecimal per RFC 4572, e.g. "AB:CD:...". + assertThat(fingerprint.value).matches("([0-9A-F]{2}:)+[0-9A-F]{2}"); + } + + @Test + @SmallTest + public void testFingerprintSurvivesPemRoundTrip() { + RtcCertificatePem original = RtcCertificatePem.generateCertificate(); + RtcCertificatePem recreated = new RtcCertificatePem(original.privateKey, original.certificate); + + List<DtlsFingerprint> originalFingerprints = original.getFingerprints(); + List<DtlsFingerprint> recreatedFingerprints = recreated.getFingerprints(); + assertThat(originalFingerprints).hasSize(1); + assertThat(recreatedFingerprints).hasSize(1); + assertThat(originalFingerprints.get(0).algorithm) + .isEqualTo(recreatedFingerprints.get(0).algorithm); + assertThat(originalFingerprints.get(0).value).isEqualTo(recreatedFingerprints.get(0).value); + } + + @Test + @SmallTest + public void testCertificateFromInvalidPemHasNoFingerprint() { + // A certificate built from PEM strings that are not valid certificates has + // no computable fingerprint. + RtcCertificatePem rtcCertificate = new RtcCertificatePem("private", "certificate"); + assertThat(rtcCertificate.getFingerprints()).isEmpty(); + } }
diff --git a/sdk/android/src/jni/pc/rtc_certificate.cc b/sdk/android/src/jni/pc/rtc_certificate.cc index 72456c2..9d0361a 100644 --- a/sdk/android/src/jni/pc/rtc_certificate.cc +++ b/sdk/android/src/jni/pc/rtc_certificate.cc
@@ -13,11 +13,14 @@ #include <jni.h> #include <cstdint> +#include <memory> #include "api/scoped_refptr.h" #include "rtc_base/rtc_certificate.h" #include "rtc_base/rtc_certificate_generator.h" +#include "rtc_base/ssl_fingerprint.h" #include "rtc_base/ssl_identity.h" +#include "sdk/android/generated_peerconnection_jni/DtlsFingerprint_jni.h" #include "sdk/android/generated_peerconnection_jni/RtcCertificatePem_jni.h" #include "sdk/android/native_api/jni/java_types.h" #include "sdk/android/native_api/jni/scoped_java_ref.h" @@ -62,5 +65,28 @@ NativeToJavaString(jni, pem.certificate())); } +static jni_zero::ScopedJavaLocalRef<jobject> +JNI_RtcCertificatePem_GetFingerprint( + JNIEnv* jni, + const jni_zero::JavaRef<jstring>& j_private_key, + const jni_zero::JavaRef<jstring>& j_certificate) { + // The fingerprint is not stored on the PEM representation, so the native + // certificate is reconstructed from the PEM strings in order to compute it. + RTCCertificatePEM pem(JavaToNativeString(jni, j_private_key), + JavaToNativeString(jni, j_certificate)); + scoped_refptr<RTCCertificate> certificate = RTCCertificate::FromPEM(pem); + if (!certificate) { + return nullptr; + } + std::unique_ptr<SSLFingerprint> fingerprint = + SSLFingerprint::CreateFromCertificate(*certificate); + if (!fingerprint) { + return nullptr; + } + return Java_DtlsFingerprint_Constructor( + jni, NativeToJavaString(jni, fingerprint->algorithm), + NativeToJavaString(jni, fingerprint->GetRfc4572Fingerprint())); +} + } // namespace jni } // namespace webrtc