summaryrefslogtreecommitdiffstats
path: root/dom/media/webrtc/RTCCertificate.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/media/webrtc/RTCCertificate.cpp435
1 files changed, 435 insertions, 0 deletions
diff --git a/dom/media/webrtc/RTCCertificate.cpp b/dom/media/webrtc/RTCCertificate.cpp
new file mode 100644
index 0000000000..e419a0bee6
--- /dev/null
+++ b/dom/media/webrtc/RTCCertificate.cpp
@@ -0,0 +1,435 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/RTCCertificate.h"
+
+#include <cstdio>
+#include <cstring>
+#include <memory>
+#include <new>
+#include <utility>
+#include "ErrorList.h"
+#include "MainThreadUtils.h"
+#include "cert.h"
+#include "cryptohi.h"
+#include "js/StructuredClone.h"
+#include "js/TypeDecls.h"
+#include "js/Value.h"
+#include "keyhi.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/MacroForEach.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/CryptoBuffer.h"
+#include "mozilla/dom/CryptoKey.h"
+#include "mozilla/dom/KeyAlgorithmBinding.h"
+#include "mozilla/dom/KeyAlgorithmProxy.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/RTCCertificateBinding.h"
+#include "mozilla/dom/StructuredCloneHolder.h"
+#include "mozilla/dom/SubtleCryptoBinding.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/dom/WebCryptoCommon.h"
+#include "mozilla/dom/WebCryptoTask.h"
+#include "mozilla/fallible.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsLiteralString.h"
+#include "nsStringFlags.h"
+#include "nsStringFwd.h"
+#include "nsTLiteralString.h"
+#include "pk11pub.h"
+#include "plarena.h"
+#include "secasn1.h"
+#include "secasn1t.h"
+#include "seccomon.h"
+#include "secmodt.h"
+#include "secoid.h"
+#include "secoidt.h"
+#include "transport/dtlsidentity.h"
+#include "xpcpublic.h"
+
+namespace mozilla::dom {
+
+#define RTCCERTIFICATE_SC_VERSION 0x00000001
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(RTCCertificate, mGlobal)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(RTCCertificate)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(RTCCertificate)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RTCCertificate)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+// Note: explicit casts necessary to avoid
+// warning C4307: '*' : integral constant overflow
+#define ONE_DAY \
+ PRTime(PR_USEC_PER_SEC) * PRTime(60) /*sec*/ \
+ * PRTime(60) /*min*/ * PRTime(24) /*hours*/
+#define EXPIRATION_DEFAULT ONE_DAY* PRTime(30)
+#define EXPIRATION_SLACK ONE_DAY
+#define EXPIRATION_MAX ONE_DAY* PRTime(365) /*year*/
+
+const size_t RTCCertificateCommonNameLength = 16;
+const size_t RTCCertificateMinRsaSize = 1024;
+
+class GenerateRTCCertificateTask : public GenerateAsymmetricKeyTask {
+ public:
+ GenerateRTCCertificateTask(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const ObjectOrString& aAlgorithm,
+ const Sequence<nsString>& aKeyUsages,
+ PRTime aExpires)
+ : GenerateAsymmetricKeyTask(aGlobal, aCx, aAlgorithm, true, aKeyUsages),
+ mExpires(aExpires),
+ mAuthType(ssl_kea_null),
+ mCertificate(nullptr),
+ mSignatureAlg(SEC_OID_UNKNOWN) {
+ if (NS_FAILED(mEarlyRv)) {
+ // webrtc-pc says to throw NotSupportedError if we have passed "an
+ // algorithm that the user agent cannot or will not use to generate a
+ // certificate". This catches these cases.
+ mEarlyRv = NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+ }
+
+ private:
+ PRTime mExpires;
+ SSLKEAType mAuthType;
+ UniqueCERTCertificate mCertificate;
+ SECOidTag mSignatureAlg;
+
+ static CERTName* GenerateRandomName(PK11SlotInfo* aSlot) {
+ uint8_t randomName[RTCCertificateCommonNameLength];
+ SECStatus rv =
+ PK11_GenerateRandomOnSlot(aSlot, randomName, sizeof(randomName));
+ if (rv != SECSuccess) {
+ return nullptr;
+ }
+
+ char buf[sizeof(randomName) * 2 + 4];
+ strncpy(buf, "CN=", 4);
+ for (size_t i = 0; i < sizeof(randomName); ++i) {
+ snprintf(&buf[i * 2 + 3], 3, "%.2x", randomName[i]);
+ }
+ buf[sizeof(buf) - 1] = '\0';
+
+ return CERT_AsciiToName(buf);
+ }
+
+ nsresult GenerateCertificate() {
+ UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+ MOZ_ASSERT(slot.get());
+
+ UniqueCERTName subjectName(GenerateRandomName(slot.get()));
+ if (!subjectName) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ UniqueSECKEYPublicKey publicKey(mKeyPair->mPublicKey->GetPublicKey());
+ UniqueCERTSubjectPublicKeyInfo spki(
+ SECKEY_CreateSubjectPublicKeyInfo(publicKey.get()));
+ if (!spki) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ UniqueCERTCertificateRequest certreq(
+ CERT_CreateCertificateRequest(subjectName.get(), spki.get(), nullptr));
+ if (!certreq) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ PRTime now = PR_Now();
+ PRTime notBefore = now - EXPIRATION_SLACK;
+ mExpires += now;
+
+ UniqueCERTValidity validity(CERT_CreateValidity(notBefore, mExpires));
+ if (!validity) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ unsigned long serial;
+ // Note: This serial in principle could collide, but it's unlikely, and we
+ // don't expect anyone to be validating certificates anyway.
+ SECStatus rv = PK11_GenerateRandomOnSlot(
+ slot.get(), reinterpret_cast<unsigned char*>(&serial), sizeof(serial));
+ if (rv != SECSuccess) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ CERTCertificate* cert = CERT_CreateCertificate(
+ serial, subjectName.get(), validity.get(), certreq.get());
+ if (!cert) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+ mCertificate.reset(cert);
+ return NS_OK;
+ }
+
+ nsresult SignCertificate() {
+ MOZ_ASSERT(mSignatureAlg != SEC_OID_UNKNOWN);
+ PLArenaPool* arena = mCertificate->arena;
+
+ SECStatus rv = SECOID_SetAlgorithmID(arena, &mCertificate->signature,
+ mSignatureAlg, nullptr);
+ if (rv != SECSuccess) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ // Set version to X509v3.
+ *(mCertificate->version.data) = SEC_CERTIFICATE_VERSION_3;
+ mCertificate->version.len = 1;
+
+ SECItem innerDER = {siBuffer, nullptr, 0};
+ if (!SEC_ASN1EncodeItem(arena, &innerDER, mCertificate.get(),
+ SEC_ASN1_GET(CERT_CertificateTemplate))) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ SECItem* signedCert = PORT_ArenaZNew(arena, SECItem);
+ if (!signedCert) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ UniqueSECKEYPrivateKey privateKey(mKeyPair->mPrivateKey->GetPrivateKey());
+ rv = SEC_DerSignData(arena, signedCert, innerDER.data, innerDER.len,
+ privateKey.get(), mSignatureAlg);
+ if (rv != SECSuccess) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+ mCertificate->derCert = *signedCert;
+ return NS_OK;
+ }
+
+ nsresult BeforeCrypto() override {
+ if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_RSASSA_PKCS1)) {
+ // Double check that size is OK.
+ auto sz = static_cast<size_t>(mRsaParams.keySizeInBits);
+ if (sz < RTCCertificateMinRsaSize) {
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+
+ KeyAlgorithmProxy& alg = mKeyPair->mPublicKey->Algorithm();
+ if (alg.mType != KeyAlgorithmProxy::RSA ||
+ !alg.mRsa.mHash.mName.EqualsLiteral(WEBCRYPTO_ALG_SHA256)) {
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+
+ mSignatureAlg = SEC_OID_PKCS1_SHA256_WITH_RSA_ENCRYPTION;
+ mAuthType = ssl_kea_rsa;
+
+ } else if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_ECDSA)) {
+ // We only support good curves in WebCrypto.
+ // If that ever changes, check that a good one was chosen.
+
+ mSignatureAlg = SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE;
+ mAuthType = ssl_kea_ecdh;
+ } else {
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+ return NS_OK;
+ }
+
+ nsresult DoCrypto() override {
+ nsresult rv = GenerateAsymmetricKeyTask::DoCrypto();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GenerateCertificate();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SignCertificate();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ virtual void Resolve() override {
+ // Make copies of the private key and certificate, otherwise, when this
+ // object is deleted, the structures they reference will be deleted too.
+ UniqueSECKEYPrivateKey key = mKeyPair->mPrivateKey->GetPrivateKey();
+ CERTCertificate* cert = CERT_DupCertificate(mCertificate.get());
+ RefPtr<RTCCertificate> result =
+ new RTCCertificate(mResultPromise->GetParentObject(), key.release(),
+ cert, mAuthType, mExpires);
+ mResultPromise->MaybeResolve(result);
+ }
+};
+
+static PRTime ReadExpires(JSContext* aCx, const ObjectOrString& aOptions,
+ ErrorResult& aRv) {
+ // This conversion might fail, but we don't really care; use the default.
+ // If this isn't an object, or it doesn't coerce into the right type,
+ // then we won't get the |expires| value. Either will be caught later.
+ RTCCertificateExpiration expiration;
+ if (!aOptions.IsObject()) {
+ return EXPIRATION_DEFAULT;
+ }
+ JS::Rooted<JS::Value> value(aCx, JS::ObjectValue(*aOptions.GetAsObject()));
+ if (!expiration.Init(aCx, value)) {
+ aRv.NoteJSContextException(aCx);
+ return 0;
+ }
+
+ if (!expiration.mExpires.WasPassed()) {
+ return EXPIRATION_DEFAULT;
+ }
+ static const uint64_t max =
+ static_cast<uint64_t>(EXPIRATION_MAX / PR_USEC_PER_MSEC);
+ if (expiration.mExpires.Value() > max) {
+ return EXPIRATION_MAX;
+ }
+ return static_cast<PRTime>(expiration.mExpires.Value() * PR_USEC_PER_MSEC);
+}
+
+already_AddRefed<Promise> RTCCertificate::GenerateCertificate(
+ const GlobalObject& aGlobal, const ObjectOrString& aOptions,
+ ErrorResult& aRv, JS::Compartment* aCompartment) {
+ nsIGlobalObject* global = xpc::NativeGlobal(aGlobal.Get());
+ RefPtr<Promise> p = Promise::Create(global, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ Sequence<nsString> usages;
+ if (!usages.AppendElement(u"sign"_ns, fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return nullptr;
+ }
+
+ PRTime expires = ReadExpires(aGlobal.Context(), aOptions, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ RefPtr<WebCryptoTask> task = new GenerateRTCCertificateTask(
+ global, aGlobal.Context(), aOptions, usages, expires);
+ task->DispatchWithPromise(p);
+ return p.forget();
+}
+
+RTCCertificate::RTCCertificate(nsIGlobalObject* aGlobal)
+ : mGlobal(aGlobal),
+ mPrivateKey(nullptr),
+ mCertificate(nullptr),
+ mAuthType(ssl_kea_null),
+ mExpires(0) {}
+
+RTCCertificate::RTCCertificate(nsIGlobalObject* aGlobal,
+ SECKEYPrivateKey* aPrivateKey,
+ CERTCertificate* aCertificate,
+ SSLKEAType aAuthType, PRTime aExpires)
+ : mGlobal(aGlobal),
+ mPrivateKey(aPrivateKey),
+ mCertificate(aCertificate),
+ mAuthType(aAuthType),
+ mExpires(aExpires) {}
+
+RefPtr<DtlsIdentity> RTCCertificate::CreateDtlsIdentity() const {
+ if (!mPrivateKey || !mCertificate) {
+ return nullptr;
+ }
+ UniqueSECKEYPrivateKey key(SECKEY_CopyPrivateKey(mPrivateKey.get()));
+ UniqueCERTCertificate cert(CERT_DupCertificate(mCertificate.get()));
+ RefPtr<DtlsIdentity> id =
+ new DtlsIdentity(std::move(key), std::move(cert), mAuthType);
+ return id;
+}
+
+JSObject* RTCCertificate::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return RTCCertificate_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+bool RTCCertificate::WritePrivateKey(JSStructuredCloneWriter* aWriter) const {
+ JsonWebKey jwk;
+ nsresult rv = CryptoKey::PrivateKeyToJwk(mPrivateKey.get(), jwk);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ nsString json;
+ if (!jwk.ToJSON(json)) {
+ return false;
+ }
+ return StructuredCloneHolder::WriteString(aWriter, json);
+}
+
+bool RTCCertificate::WriteCertificate(JSStructuredCloneWriter* aWriter) const {
+ UniqueCERTCertificateList certs(CERT_CertListFromCert(mCertificate.get()));
+ if (!certs || certs->len <= 0) {
+ return false;
+ }
+ if (!JS_WriteUint32Pair(aWriter, certs->certs[0].len, 0)) {
+ return false;
+ }
+ return JS_WriteBytes(aWriter, certs->certs[0].data, certs->certs[0].len);
+}
+
+bool RTCCertificate::WriteStructuredClone(
+ JSContext* aCx, JSStructuredCloneWriter* aWriter) const {
+ if (!mPrivateKey || !mCertificate) {
+ return false;
+ }
+
+ return JS_WriteUint32Pair(aWriter, RTCCERTIFICATE_SC_VERSION, mAuthType) &&
+ JS_WriteUint32Pair(aWriter, (mExpires >> 32) & 0xffffffff,
+ mExpires & 0xffffffff) &&
+ WritePrivateKey(aWriter) && WriteCertificate(aWriter);
+}
+
+bool RTCCertificate::ReadPrivateKey(JSStructuredCloneReader* aReader) {
+ nsString json;
+ if (!StructuredCloneHolder::ReadString(aReader, json)) {
+ return false;
+ }
+ JsonWebKey jwk;
+ if (!jwk.Init(json)) {
+ return false;
+ }
+ mPrivateKey = CryptoKey::PrivateKeyFromJwk(jwk);
+ return !!mPrivateKey;
+}
+
+bool RTCCertificate::ReadCertificate(JSStructuredCloneReader* aReader) {
+ CryptoBuffer cert;
+ if (!ReadBuffer(aReader, cert) || cert.Length() == 0) {
+ return false;
+ }
+
+ SECItem der = {siBuffer, cert.Elements(),
+ static_cast<unsigned int>(cert.Length())};
+ mCertificate.reset(CERT_NewTempCertificate(CERT_GetDefaultCertDB(), &der,
+ nullptr, true, true));
+ return !!mCertificate;
+}
+
+// static
+already_AddRefed<RTCCertificate> RTCCertificate::ReadStructuredClone(
+ JSContext* aCx, nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader) {
+ if (!NS_IsMainThread()) {
+ // These objects are mainthread-only.
+ return nullptr;
+ }
+ uint32_t version, authType;
+ if (!JS_ReadUint32Pair(aReader, &version, &authType) ||
+ version != RTCCERTIFICATE_SC_VERSION) {
+ return nullptr;
+ }
+ RefPtr<RTCCertificate> cert = new RTCCertificate(aGlobal);
+ cert->mAuthType = static_cast<SSLKEAType>(authType);
+
+ uint32_t high, low;
+ if (!JS_ReadUint32Pair(aReader, &high, &low)) {
+ return nullptr;
+ }
+ cert->mExpires = static_cast<PRTime>(high) << 32 | low;
+
+ if (!cert->ReadPrivateKey(aReader) || !cert->ReadCertificate(aReader)) {
+ return nullptr;
+ }
+
+ return cert.forget();
+}
+
+} // namespace mozilla::dom