From 5f35fd7e1b1b02c8001f338a4d1e4abca26b6011 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 1 Nov 2026 14:58:56 +0100 Subject: [PATCH 1/2] fix: report certificate expiration and fail library load on init errors Native certificates carry their expiration time in milliseconds, or RTCCertificatePEM.EXPIRES_UNKNOWN, passed as a jlong. JNI_OnLoad keeps the context local until it is initialized, cleans it up on failure and returns JNI_ERR; the VM is set apart from the context for that. --- .../networking/peer-connection-config.md | 2 +- .../jni-voithos/include/JavaUtils.h | 6 ++ .../jni-voithos/src/JavaUtils.cpp | 18 +++++- .../src/main/cpp/include/WebRTCContext.h | 5 +- .../main/cpp/include/rtc/RTCCertificatePEM.h | 16 ++++- .../src/main/cpp/src/JNI_RTCDtlsTransport.cpp | 5 +- webrtc-jni/src/main/cpp/src/JNI_WebRTC.cpp | 62 ++++++++++++++----- webrtc-jni/src/main/cpp/src/WebRTCContext.cpp | 20 +++++- .../main/cpp/src/api/JavaCallbackThread.cpp | 4 +- .../src/main/cpp/src/api/RTCConfiguration.cpp | 2 +- .../main/cpp/src/rtc/RTCCertificatePEM.cpp | 49 ++++++++++++++- .../dev/onvoid/webrtc/RTCCertificatePEM.java | 21 ++++++- .../webrtc/RTCConfigurationFieldsTests.java | 45 ++++++++++++++ .../onvoid/webrtc/RTCDtlsTransportTests.java | 11 ++++ 14 files changed, 231 insertions(+), 35 deletions(-) diff --git a/docs/guide/networking/peer-connection-config.md b/docs/guide/networking/peer-connection-config.md index 50259d49f..ade5e4bc5 100644 --- a/docs/guide/networking/peer-connection-config.md +++ b/docs/guide/networking/peer-connection-config.md @@ -69,7 +69,7 @@ config.cryptoOptions = crypto; `cryptexPolicy` encrypts the RTP header extensions and CSRCs as a whole (RFC 9335) where the peer supports it (`NEGOTIATE`) or always (`REQUIRE`). -`certificates` takes the DTLS certificate a peer connection authenticates with, as an `RTCCertificatePEM`; without one, WebRTC generates a certificate per connection. A certificate that does not parse makes `createPeerConnection()` and `setConfiguration()` throw. +`certificates` takes the DTLS certificate a peer connection authenticates with, as an `RTCCertificatePEM`; without one, WebRTC generates a certificate per connection. A certificate that does not parse makes `createPeerConnection()` and `setConfiguration()` throw. The expiration time passed to its constructor is informational: WebRTC takes it from the certificate. The certificates that `getConfiguration()` and `RTCDtlsTransport.getRemoteCertificates()` return carry that time in `getExpires()`, in milliseconds since the Unix epoch, or `RTCCertificatePEM.EXPIRES_UNKNOWN` (-1) if it cannot be read. ## Signaling diff --git a/webrtc-jni/src/main/cpp/dependencies/jni-voithos/include/JavaUtils.h b/webrtc-jni/src/main/cpp/dependencies/jni-voithos/include/JavaUtils.h index aead881df..64ca5f218 100644 --- a/webrtc-jni/src/main/cpp/dependencies/jni-voithos/include/JavaUtils.h +++ b/webrtc-jni/src/main/cpp/dependencies/jni-voithos/include/JavaUtils.h @@ -46,6 +46,12 @@ // Uses JNI directly, so reporting an allocation failure needs no C++ allocation. void ThrowJavaException(JNIEnv * env, const char * className, const char * message) noexcept; void ThrowCxxJavaException(JNIEnv * env); +// The VM the library is loaded into. JNI_OnLoad sets it before anything else, +// independently of the library's JavaContext, which is published only once it +// is fully initialized: its initialization already needs the VM, through the +// JNI references and AttachCurrentThread. +void SetJavaVM(JavaVM * vm); +JavaVM * GetJavaVM(); JNIEnv * AttachCurrentThread(); bool ExceptionCheck(JNIEnv * env); jclass FindClass(JNIEnv * env, const char * name); diff --git a/webrtc-jni/src/main/cpp/dependencies/jni-voithos/src/JavaUtils.cpp b/webrtc-jni/src/main/cpp/dependencies/jni-voithos/src/JavaUtils.cpp index f0d6734f7..92a3b17a9 100644 --- a/webrtc-jni/src/main/cpp/dependencies/jni-voithos/src/JavaUtils.cpp +++ b/webrtc-jni/src/main/cpp/dependencies/jni-voithos/src/JavaUtils.cpp @@ -16,9 +16,25 @@ #include "JavaThreadEnv.h" #include "JavaWrappedException.h" +#include #include #include +namespace +{ + std::atomic javaVM { nullptr }; +} + +void SetJavaVM(JavaVM * vm) +{ + javaVM.store(vm); +} + +JavaVM * GetJavaVM() +{ + return javaVM.load(); +} + bool ExceptionCheck(JNIEnv * env) { if (env->ExceptionCheck()) { @@ -162,7 +178,7 @@ jfieldID GetFieldID(JNIEnv * env, jclass cls, const std::string & fieldName, con JNIEnv * AttachCurrentThread() { - thread_local std::unique_ptr threadEnv(new jni::JavaThreadEnv(javaContext->getVM())); + thread_local std::unique_ptr threadEnv(new jni::JavaThreadEnv(GetJavaVM())); return threadEnv->getEnv(); } diff --git a/webrtc-jni/src/main/cpp/include/WebRTCContext.h b/webrtc-jni/src/main/cpp/include/WebRTCContext.h index 9b7d4050a..f937b6e27 100644 --- a/webrtc-jni/src/main/cpp/include/WebRTCContext.h +++ b/webrtc-jni/src/main/cpp/include/WebRTCContext.h @@ -33,7 +33,9 @@ namespace jni { public: WebRTCContext(JavaVM * vm); - ~WebRTCContext() = default; + // Releases what initialize() set up and destroy() has not, also + // after an initialization that failed part way. + ~WebRTCContext(); void initialize(JNIEnv * env) override; void initializeClassLoader(JNIEnv* env, const char * loaderName) override; @@ -51,6 +53,7 @@ namespace jni void initializePowerManagement(); private: + bool sslInitialized; std::mutex aMutex; std::mutex vMutex; std::unique_ptr audioDevManager; diff --git a/webrtc-jni/src/main/cpp/include/rtc/RTCCertificatePEM.h b/webrtc-jni/src/main/cpp/include/rtc/RTCCertificatePEM.h index deb183252..8661226a8 100644 --- a/webrtc-jni/src/main/cpp/include/rtc/RTCCertificatePEM.h +++ b/webrtc-jni/src/main/cpp/include/rtc/RTCCertificatePEM.h @@ -21,6 +21,7 @@ #include "JavaRef.h" #include "rtc_base/rtc_certificate.h" +#include "rtc_base/ssl_certificate.h" #include @@ -40,7 +41,20 @@ namespace jni jfieldID certificate; }; - JavaLocalRef toJava(JNIEnv * env, const webrtc::RTCCertificatePEM & certificate); + // The value of RTCCertificatePEM.EXPIRES_UNKNOWN, for a certificate whose + // expiration time cannot be read. + constexpr jlong kExpiresUnknown = -1; + + // The expiration time of the certificate in milliseconds relative to the + // Unix epoch, or kExpiresUnknown. + jlong ExpiresMillis(const webrtc::SSLCertificate & certificate); + + // A local certificate, with its private key. + JavaLocalRef toJava(JNIEnv * env, const webrtc::RTCCertificate & certificate); + // A remote certificate, whose private key is not known. + JavaLocalRef toJava(JNIEnv * env, const webrtc::SSLCertificate & certificate); + // `expires` in milliseconds relative to the Unix epoch, or kExpiresUnknown. + JavaLocalRef toJava(JNIEnv * env, const webrtc::RTCCertificatePEM & certificate, jlong expires); webrtc::RTCCertificatePEM toNative(JNIEnv * env, const JavaRef & certificate); } } diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp index 5c239516b..1eef6904c 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp @@ -76,10 +76,7 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCDtlsTransport_getRemoteCerti jni::JavaArrayList certificates(env, count); for (size_t i = 0; i < count; i++) { - const webrtc::SSLCertificate & certificate = certChain->Get(i); - const webrtc::RTCCertificatePEM pem("", certificate.ToPEMString()); - - certificates.add(jni::RTCCertificatePEM::toJava(env, pem)); + certificates.add(jni::RTCCertificatePEM::toJava(env, certChain->Get(i))); } return certificates.listObject().release(); diff --git a/webrtc-jni/src/main/cpp/src/JNI_WebRTC.cpp b/webrtc-jni/src/main/cpp/src/JNI_WebRTC.cpp index 54707fe2b..b604c178c 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_WebRTC.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_WebRTC.cpp @@ -23,6 +23,9 @@ #include +#include +#include + jni::JavaContext * javaContext = nullptr; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM * vm, void * reserved) @@ -30,30 +33,56 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM * vm, void * reserved) JNIEnv * env = nullptr; if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { - return -1; + return JNI_ERR; } - // libwebrtc initializes its logging configuration on the first log line, - // with everything from LS_INFO upwards written to stderr, and the - // configuration can be set only once. Initialize it here, before anything - // can log, so nothing is written unless the application asks for it through - // Logging.logToDebug or a log sink. - webrtc::LoggingConfig loggingConfig; - loggingConfig.set_log_thread(true); - loggingConfig.set_min_severity(webrtc::LS_NONE); - loggingConfig.set_debug_severity(webrtc::LS_NONE); - webrtc::InitializeLogging(std::move(loggingConfig)); - - javaContext = new jni::WebRTCContext(vm); + // Before anything that creates a JNI reference: those find the env of their + // thread through the VM. + SetJavaVM(vm); + // No C++ exception may leave JNI_OnLoad. On failure the context is + // destroyed here, which releases what its initialization had set up, the + // translated exception stays pending and JNI_ERR makes the library load + // fail: the JVM throws the pending exception from System.loadLibrary and + // unloads the library. The context is published only once it is complete, + // so no native call can see a partially initialized one. try { - javaContext->initialize(env); + // libwebrtc initializes its logging configuration on the first log + // line, with everything from LS_INFO upwards written to stderr, and the + // configuration can be set only once. Initialize it here, before + // anything can log, so nothing is written unless the application asks + // for it through Logging.logToDebug or a log sink. + webrtc::LoggingConfig loggingConfig; + loggingConfig.set_log_thread(true); + loggingConfig.set_min_severity(webrtc::LS_NONE); + loggingConfig.set_debug_severity(webrtc::LS_NONE); + webrtc::InitializeLogging(std::move(loggingConfig)); + + auto context = std::make_unique(vm); + context->initialize(env); + + // Not every JNI call of the initialization is followed by a check, a + // Java exception still pending means it failed as well. + if (env->ExceptionCheck()) { + return JNI_ERR; + } + + javaContext = context.release(); + + return JNI_VERSION_1_6; } catch (...) { - ThrowCxxJavaException(env); + // Translating can fail as well (creating the Java error needs its + // class): the load fails then without a pending exception, the JVM + // reports the unsupported JNI version. + try { + ThrowCxxJavaException(env); + } + catch (...) { + } } - return JNI_VERSION_1_6; + return JNI_ERR; } JNIEXPORT void JNICALL JNI_OnUnload(JavaVM * vm, void * reserved) @@ -73,5 +102,6 @@ JNIEXPORT void JNICALL JNI_OnUnload(JavaVM * vm, void * reserved) } delete javaContext; + javaContext = nullptr; } } diff --git a/webrtc-jni/src/main/cpp/src/WebRTCContext.cpp b/webrtc-jni/src/main/cpp/src/WebRTCContext.cpp index 700d2b5b7..d7917a969 100644 --- a/webrtc-jni/src/main/cpp/src/WebRTCContext.cpp +++ b/webrtc-jni/src/main/cpp/src/WebRTCContext.cpp @@ -59,18 +59,28 @@ namespace jni WebRTCContext::WebRTCContext(JavaVM * vm) : JavaContext(vm), webrtcEnv(webrtc::CreateEnvironment()), + sslInitialized(false), audioDevManager(nullptr), videoDevManager(nullptr), powerManagement(nullptr) { } + WebRTCContext::~WebRTCContext() + { + if (sslInitialized) { + webrtc::CleanupSSL(); + } + } + void WebRTCContext::initialize(JNIEnv * env) { if (!webrtc::InitializeSSL()) { throw Exception("Initialize SSL failed"); } - + + sslInitialized = true; + JavaEnums::add(env, PKG_LOG"Logging$Severity"); JavaEnums::add(env, PKG_MEDIA"MediaType"); JavaEnums::add(env, PKG"RTCDataChannelState"); @@ -142,8 +152,12 @@ namespace jni void WebRTCContext::destroy(JNIEnv * env) { - if (!webrtc::CleanupSSL()) { - env->Throw(jni::JavaError(env, "Cleanup SSL failed")); + if (sslInitialized) { + sslInitialized = false; + + if (!webrtc::CleanupSSL()) { + env->Throw(jni::JavaError(env, "Cleanup SSL failed")); + } } audioDevManager = nullptr; diff --git a/webrtc-jni/src/main/cpp/src/api/JavaCallbackThread.cpp b/webrtc-jni/src/main/cpp/src/api/JavaCallbackThread.cpp index d05930171..fbf5da8a4 100644 --- a/webrtc-jni/src/main/cpp/src/api/JavaCallbackThread.cpp +++ b/webrtc-jni/src/main/cpp/src/api/JavaCallbackThread.cpp @@ -15,7 +15,7 @@ */ #include "api/JavaCallbackThread.h" -#include "JavaContext.h" +#include "JavaUtils.h" #include "rtc_base/logging.h" @@ -38,7 +38,7 @@ namespace jni void Run(Queue * queue) { - JavaVM * vm = javaContext->getVM(); + JavaVM * vm = GetJavaVM(); JNIEnv * env = nullptr; JavaVMAttachArgs args; diff --git a/webrtc-jni/src/main/cpp/src/api/RTCConfiguration.cpp b/webrtc-jni/src/main/cpp/src/api/RTCConfiguration.cpp index 742abec30..9a7bf308d 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCConfiguration.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCConfiguration.cpp @@ -82,7 +82,7 @@ namespace jni JavaArrayList certificateList(env, certificates.size()); for (auto & certificate : certificates) { - certificateList.add(jni::RTCCertificatePEM::toJava(env, certificate->ToPEM())); + certificateList.add(jni::RTCCertificatePEM::toJava(env, *certificate)); } auto type = JavaEnums::toJava(env, nativeType.type); diff --git a/webrtc-jni/src/main/cpp/src/rtc/RTCCertificatePEM.cpp b/webrtc-jni/src/main/cpp/src/rtc/RTCCertificatePEM.cpp index fe5f11485..1f63c5204 100644 --- a/webrtc-jni/src/main/cpp/src/rtc/RTCCertificatePEM.cpp +++ b/webrtc-jni/src/main/cpp/src/rtc/RTCCertificatePEM.cpp @@ -23,18 +23,63 @@ #include "rtc_base/ref_count.h" #include "rtc_base/rtc_certificate_generator.h" +#include +#include + namespace jni { namespace RTCCertificatePEM { - JavaLocalRef toJava(JNIEnv * env, const webrtc::RTCCertificatePEM & certificate) + jlong ExpiresMillis(const webrtc::SSLCertificate & certificate) + { + constexpr int64_t kMillisPerSecond = 1000; + constexpr int64_t kMax = std::numeric_limits::max(); + constexpr int64_t kMin = std::numeric_limits::min(); + + // Seconds relative to the Unix epoch, or -1 when the certificate + // cannot be parsed. RTCCertificate::Expires() would report that as + // 0, which is indistinguishable from an actual time. + const int64_t seconds = certificate.CertificateExpirationTime(); + + if (seconds == -1) { + return kExpiresUnknown; + } + + // No X.509 time comes near the limits, saturate rather than overflow. + if (seconds > kMax / kMillisPerSecond) { + return static_cast(kMax); + } + if (seconds < kMin / kMillisPerSecond) { + return static_cast(kMin); + } + + return static_cast(seconds * kMillisPerSecond); + } + + JavaLocalRef toJava(JNIEnv * env, const webrtc::RTCCertificate & certificate) + { + return toJava(env, certificate.ToPEM(), ExpiresMillis(certificate.GetSSLCertificate())); + } + + JavaLocalRef toJava(JNIEnv * env, const webrtc::SSLCertificate & certificate) + { + const webrtc::RTCCertificatePEM pem("", certificate.ToPEMString()); + + return toJava(env, pem, ExpiresMillis(certificate)); + } + + JavaLocalRef toJava(JNIEnv * env, const webrtc::RTCCertificatePEM & certificate, jlong expires) { const auto javaClass = JavaClasses::get(env); + // The arguments of the variadic NewObject are not converted to the + // types of the constructor's signature, the JVM reads the long + // parameter with va_arg(jlong): it must be passed as a jlong, an int + // literal there is undefined behavior. jobject obj = env->NewObject(javaClass->cls, javaClass->ctor, JavaString::toJava(env, certificate.private_key()).get(), JavaString::toJava(env, certificate.certificate()).get(), - 0 + expires ); return JavaLocalRef(env, obj); diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCCertificatePEM.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCCertificatePEM.java index 96ae3599f..3ed64f523 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCCertificatePEM.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCCertificatePEM.java @@ -25,9 +25,16 @@ */ public class RTCCertificatePEM { + /** + * The value of {@link #getExpires()} for a certificate whose expiration + * time cannot be read. No actual expiration time has this value: those + * are whole seconds, multiples of 1000 in milliseconds. + */ + public static final long EXPIRES_UNKNOWN = -1; + /** * Indicates the time in milliseconds relative to the Unix epoch after which - * the certificate will be considered invalid. + * the certificate will be considered invalid, or {@link #EXPIRES_UNKNOWN}. */ private final long expires; @@ -48,7 +55,11 @@ public class RTCCertificatePEM { * * @param privateKey Private key as PEM string. * @param certificate Certificate as PEM string. - * @param expires The expiration date of this certificate. + * @param expires The expiration time of this certificate in + * milliseconds relative to the Unix epoch, or + * {@link #EXPIRES_UNKNOWN}. It is informational only: + * WebRTC reads the expiration time from the PEM + * certificate itself. */ public RTCCertificatePEM(String privateKey, String certificate, long expires) { this.privateKey = privateKey; @@ -72,7 +83,11 @@ public String getCertificate() { /** * @return The expiration time in milliseconds of this certificate relative - * to the Unix epoch. + * to the Unix epoch, or {@link #EXPIRES_UNKNOWN} if it cannot be read from + * the certificate. Certificates returned by the native API, such as those of + * {@link RTCDtlsTransport#getRemoteCertificates()} and + * {@link RTCPeerConnection#getConfiguration()}, carry the time read from + * the certificate. */ public long getExpires() { return expires; diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCConfigurationFieldsTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCConfigurationFieldsTests.java index 4d977fcce..98f768aed 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCConfigurationFieldsTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCConfigurationFieldsTests.java @@ -41,6 +41,30 @@ class RTCConfigurationFieldsTests extends TestBase { private static final long TIMEOUT_SECONDS = 10; + // A self-signed P-256 certificate, valid until 2049-12-31T23:59:59Z. + private static final String KEY_PEM = + "-----BEGIN PRIVATE KEY-----\n" + + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgejZcBA1yI8ptZtCF\n" + + "RUjGQ/DDktwLeqN+7hjULCMqoQehRANCAAShfg85M6ss3gmaqGaxseKa7z9XLnV4\n" + + "shMtYBi33Hrk2jRpR/kJqoLVsCWYB2jX2SR1pN4I1Kx20/2XelXeKDQn\n" + + "-----END PRIVATE KEY-----\n"; + + private static final String CERTIFICATE_PEM = + "-----BEGIN CERTIFICATE-----\n" + + "MIIBizCCATGgAwIBAgIUP0ZmlGlDx3zZ0q/DgiL6mYFhHuUwCgYIKoZIzj0EAwIw\n" + + "GzEZMBcGA1UEAwwQd2VicnRjLWphdmEtdGVzdDAeFw0yNjAxMDEwMDAwMDBaFw00\n" + + "OTEyMzEyMzU5NTlaMBsxGTAXBgNVBAMMEHdlYnJ0Yy1qYXZhLXRlc3QwWTATBgcq\n" + + "hkjOPQIBBggqhkjOPQMBBwNCAAShfg85M6ss3gmaqGaxseKa7z9XLnV4shMtYBi3\n" + + "3Hrk2jRpR/kJqoLVsCWYB2jX2SR1pN4I1Kx20/2XelXeKDQno1MwUTAdBgNVHQ4E\n" + + "FgQUHug09bThs69Rvu3uQX7xJr4vtDAwHwYDVR0jBBgwFoAUHug09bThs69Rvu3u\n" + + "QX7xJr4vtDAwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiEAg9Kr\n" + + "mern5M4S475PECo3S8chGsOEYlexai+r04iXkbACIG4JHalZdx0sF2rhZYIJZl2R\n" + + "oqimaBTvYtQRfkg3Bvv3\n" + + "-----END CERTIFICATE-----\n"; + + // 2049-12-31T23:59:59Z in milliseconds. + private static final long CERTIFICATE_EXPIRES = 2524607999000L; + @Test void nativeDefaults() { @@ -168,6 +192,27 @@ void malformedCertificateThrows() { () -> factory.createPeerConnection(config, new Observer())); } + @Test + void certificateExpiresFromCertificate() { + // The expiration time given in Java is not passed on: the one read + // back is that of the certificate, in milliseconds. It used to be 0. + RTCConfiguration config = new RTCConfiguration(); + config.certificates.add(new RTCCertificatePEM(KEY_PEM, CERTIFICATE_PEM, + RTCCertificatePEM.EXPIRES_UNKNOWN)); + + RTCPeerConnection connection = factory.createPeerConnection(config, new Observer()); + + try { + List certificates = connection.getConfiguration().certificates; + + assertEquals(1, certificates.size()); + assertEquals(CERTIFICATE_EXPIRES, certificates.get(0).getExpires()); + } + finally { + connection.close(); + } + } + @Test void setConfigurationRejectsMalformedCertificate() { RTCPeerConnection connection = factory.createPeerConnection(new RTCConfiguration(), new Observer()); diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtlsTransportTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtlsTransportTests.java index ea5a946bd..44eacd09c 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtlsTransportTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtlsTransportTests.java @@ -159,8 +159,19 @@ void remoteCertificates() { assertFalse(certificates.isEmpty()); + long now = System.currentTimeMillis(); + for (RTCCertificatePEM certificate : certificates) { assertTrue(certificate.getCertificate().startsWith("-----BEGIN CERTIFICATE-----")); + + // WebRTC generates the certificate of each connection valid for + // 30 days. The time is read from the certificate in whole seconds + // and given in milliseconds; it used to be 0. + long expires = certificate.getExpires(); + + assertTrue(expires > now, "Expires " + expires + " is not after " + now); + assertTrue(expires <= now + TimeUnit.DAYS.toMillis(31), "Expires " + expires); + assertEquals(0, expires % 1000); } } From 3fad1cc6ad9572dac4c49b8de407ffc7f8b0e030 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 1 Nov 2026 15:36:30 +0100 Subject: [PATCH 2/2] test: generate the test certificate instead of committing its key --- .../webrtc/RTCConfigurationFieldsTests.java | 29 +--- .../dev/onvoid/webrtc/TestCertificates.java | 134 ++++++++++++++++++ 2 files changed, 138 insertions(+), 25 deletions(-) create mode 100644 webrtc/src/test/java/dev/onvoid/webrtc/TestCertificates.java diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCConfigurationFieldsTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCConfigurationFieldsTests.java index 98f768aed..f04446515 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCConfigurationFieldsTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCConfigurationFieldsTests.java @@ -41,28 +41,8 @@ class RTCConfigurationFieldsTests extends TestBase { private static final long TIMEOUT_SECONDS = 10; - // A self-signed P-256 certificate, valid until 2049-12-31T23:59:59Z. - private static final String KEY_PEM = - "-----BEGIN PRIVATE KEY-----\n" + - "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgejZcBA1yI8ptZtCF\n" + - "RUjGQ/DDktwLeqN+7hjULCMqoQehRANCAAShfg85M6ss3gmaqGaxseKa7z9XLnV4\n" + - "shMtYBi33Hrk2jRpR/kJqoLVsCWYB2jX2SR1pN4I1Kx20/2XelXeKDQn\n" + - "-----END PRIVATE KEY-----\n"; - - private static final String CERTIFICATE_PEM = - "-----BEGIN CERTIFICATE-----\n" + - "MIIBizCCATGgAwIBAgIUP0ZmlGlDx3zZ0q/DgiL6mYFhHuUwCgYIKoZIzj0EAwIw\n" + - "GzEZMBcGA1UEAwwQd2VicnRjLWphdmEtdGVzdDAeFw0yNjAxMDEwMDAwMDBaFw00\n" + - "OTEyMzEyMzU5NTlaMBsxGTAXBgNVBAMMEHdlYnJ0Yy1qYXZhLXRlc3QwWTATBgcq\n" + - "hkjOPQIBBggqhkjOPQMBBwNCAAShfg85M6ss3gmaqGaxseKa7z9XLnV4shMtYBi3\n" + - "3Hrk2jRpR/kJqoLVsCWYB2jX2SR1pN4I1Kx20/2XelXeKDQno1MwUTAdBgNVHQ4E\n" + - "FgQUHug09bThs69Rvu3uQX7xJr4vtDAwHwYDVR0jBBgwFoAUHug09bThs69Rvu3u\n" + - "QX7xJr4vtDAwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiEAg9Kr\n" + - "mern5M4S475PECo3S8chGsOEYlexai+r04iXkbACIG4JHalZdx0sF2rhZYIJZl2R\n" + - "oqimaBTvYtQRfkg3Bvv3\n" + - "-----END CERTIFICATE-----\n"; - - // 2049-12-31T23:59:59Z in milliseconds. + // 2049-12-31T23:59:59Z, as UTCTime and in milliseconds. + private static final String CERTIFICATE_NOT_AFTER = "491231235959Z"; private static final long CERTIFICATE_EXPIRES = 2524607999000L; @@ -193,12 +173,11 @@ void malformedCertificateThrows() { } @Test - void certificateExpiresFromCertificate() { + void certificateExpiresFromCertificate() throws Exception { // The expiration time given in Java is not passed on: the one read // back is that of the certificate, in milliseconds. It used to be 0. RTCConfiguration config = new RTCConfiguration(); - config.certificates.add(new RTCCertificatePEM(KEY_PEM, CERTIFICATE_PEM, - RTCCertificatePEM.EXPIRES_UNKNOWN)); + config.certificates.add(TestCertificates.selfSigned("260101000000Z", CERTIFICATE_NOT_AFTER)); RTCPeerConnection connection = factory.createPeerConnection(config, new Observer()); diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/TestCertificates.java b/webrtc/src/test/java/dev/onvoid/webrtc/TestCertificates.java new file mode 100644 index 000000000..98e35bacb --- /dev/null +++ b/webrtc/src/test/java/dev/onvoid/webrtc/TestCertificates.java @@ -0,0 +1,134 @@ +/* + * Copyright 2026 Alex Andres + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.onvoid.webrtc; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Signature; +import java.security.spec.ECGenParameterSpec; +import java.util.Base64; + +/** + * Creates self-signed certificates for tests, with a fresh key each time, so + * that no private key has to be kept in the repository. + */ +final class TestCertificates { + + // ecdsa-with-SHA256, 1.2.840.10045.4.3.2 + private static final byte[] ECDSA_WITH_SHA256 = { + 0x06, 0x08, 0x2A, (byte) 0x86, 0x48, (byte) 0xCE, 0x3D, 0x04, 0x03, 0x02 + }; + + // commonName, 2.5.4.3 + private static final byte[] COMMON_NAME = { 0x06, 0x03, 0x55, 0x04, 0x03 }; + + private static final int INTEGER = 0x02; + private static final int BIT_STRING = 0x03; + private static final int UTF8_STRING = 0x0C; + private static final int UTC_TIME = 0x17; + private static final int SEQUENCE = 0x30; + private static final int SET = 0x31; + private static final int VERSION = 0xA0; + + + private TestCertificates() { + } + + /** + * Creates a self-signed P-256 certificate. + * + * @param notBefore The start of the validity, as UTCTime, e.g. "260101000000Z". + * @param notAfter The end of the validity, as UTCTime, e.g. "491231235959Z". + * + * @return The certificate with its private key, both PEM encoded. + */ + static RTCCertificatePEM selfSigned(String notBefore, String notAfter) + throws GeneralSecurityException { + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + generator.initialize(new ECGenParameterSpec("secp256r1")); + + KeyPair keyPair = generator.generateKeyPair(); + + byte[] algorithm = der(SEQUENCE, ECDSA_WITH_SHA256); + byte[] name = der(SEQUENCE, der(SET, der(SEQUENCE, COMMON_NAME, + der(UTF8_STRING, ascii("webrtc-java-test"))))); + + byte[] tbsCertificate = der(SEQUENCE, + der(VERSION, der(INTEGER, new byte[] { 2 })), + der(INTEGER, new byte[] { 1 }), + algorithm, + name, + der(SEQUENCE, der(UTC_TIME, ascii(notBefore)), der(UTC_TIME, ascii(notAfter))), + name, + keyPair.getPublic().getEncoded()); + + Signature signer = Signature.getInstance("SHA256withECDSA"); + signer.initSign(keyPair.getPrivate()); + signer.update(tbsCertificate); + + byte[] certificate = der(SEQUENCE, tbsCertificate, algorithm, + der(BIT_STRING, new byte[] { 0 }, signer.sign())); + + return new RTCCertificatePEM( + pem("PRIVATE KEY", keyPair.getPrivate().getEncoded()), + pem("CERTIFICATE", certificate), + RTCCertificatePEM.EXPIRES_UNKNOWN); + } + + private static byte[] der(int tag, byte[]... contents) { + ByteArrayOutputStream value = new ByteArrayOutputStream(); + + for (byte[] content : contents) { + value.writeBytes(content); + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(tag); + + int length = value.size(); + + if (length < 0x80) { + out.write(length); + } + else if (length < 0x100) { + out.write(0x81); + out.write(length); + } + else { + out.write(0x82); + out.write(length >> 8); + out.write(length); + } + + out.writeBytes(value.toByteArray()); + + return out.toByteArray(); + } + + private static byte[] ascii(String value) { + return value.getBytes(StandardCharsets.US_ASCII); + } + + private static String pem(String type, byte[] der) { + String base64 = Base64.getMimeEncoder(64, new byte[] { '\n' }).encodeToString(der); + + return "-----BEGIN " + type + "-----\n" + base64 + "\n-----END " + type + "-----\n"; + } +}