/* 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 "nsIX509CertDB.h" #include "CryptoTask.h" #include "QWACTrustDomain.h" #include "mozilla/dom/Promise.h" #include "mozpkix/pkix.h" #include "mozpkix/pkixder.h" #include "mozpkix/pkixnss.h" #include "mozpkix/pkixtypes.h" #include "mozpkix/pkixutil.h" #include "nsIX509Cert.h" #include "nsNSSCertificateDB.h" using namespace mozilla::pkix; using namespace mozilla::psm; using mozilla::dom::Promise; class VerifyQWACTask : public mozilla::CryptoTask { public: VerifyQWACTask(nsIX509CertDB::QWACType aType, nsIX509Cert* aCert, const nsACString& aHostname, const nsTArray>& aCollectedCerts, RefPtr& aPromise) : mType(aType), mCert(aCert), mHostname(aHostname), mCollectedCerts(aCollectedCerts.Clone()), mPromise(new nsMainThreadPtrHolder("VerifyQWACTask::mPromise", aPromise)), mVerified(false) {} private: virtual nsresult CalculateResult() override; virtual void CallCallback(nsresult rv) override; nsIX509CertDB::QWACType mType; RefPtr mCert; nsCString mHostname; nsTArray> mCollectedCerts; nsMainThreadPtrHandle mPromise; bool mVerified; }; // Does this certificate have the correct qcStatements ("qualified certificate // statements") to be a QWAC ("qualified website authentication certificate")? // ETSI EN 319 412-5 Clauses 4.2.1 and 4.2.3 state that a certificate issued in // compliance with Annex IV of Regulation (EU) No 910/2014 (i.e. a QWAC) has // 1) a QCStatement with statementId equal to id-etsi-qsc-QcCompliance and // an omitted statementInfo, and // 2) a QCStatement with statementId equal to id-etsi-qcs-QcType and a // statementInfo of length one that contains the id-etsi-qct-web // identifier. bool CertHasQWACSQCStatements(Input cert) { using namespace mozilla::pkix::der; // python DottedOIDToCode.py id-etsi-qcs-QcCompliance 0.4.0.1862.1.1 static const uint8_t id_etsi_qcs_QcCompliance[] = {0x04, 0x00, 0x8e, 0x46, 0x01, 0x01}; // python DottedOIDToCode.py id-etsi-qcs-QcType 0.4.0.1862.1.6 static const uint8_t id_etsi_qcs_QcType[] = {0x04, 0x00, 0x8e, 0x46, 0x01, 0x06}; // python DottedOIDToCode.py id-etsi-qct-web 0.4.0.1862.1.6.3 static const uint8_t id_etsi_qct_web[] = {0x04, 0x00, 0x8e, 0x46, 0x01, 0x06, 0x03}; BackCert backCert(cert, EndEntityOrCA::MustBeEndEntity, nullptr); if (backCert.Init() != Success) { return false; } const Input* qcStatementsInput(backCert.GetQCStatements()); if (!qcStatementsInput) { return false; } Reader qcStatements(*qcStatementsInput); // QCStatements ::= SEQUENCE OF QCStatement // QCStatement ::= SEQUENCE { // statementId QC-STATEMENT.&Id({SupportedStatements}), // statementInfo QC-STATEMENT.&Type // ({SupportedStatements}{@statementId}) OPTIONAL } // // SupportedStatements QC-STATEMENT ::= { qcStatement-1,...} bool foundQCComplianceStatement = false; bool foundQCTypeStatementWithWebType = false; mozilla::pkix::Result rv = NestedOf(qcStatements, SEQUENCE, SEQUENCE, EmptyAllowed::No, [&](Reader& qcStatementContents) { Reader statementId; mozilla::pkix::Result rv = ExpectTagAndGetValue( qcStatementContents, OIDTag, statementId); if (rv != Success) { return rv; } if (statementId.MatchRest(id_etsi_qcs_QcCompliance)) { foundQCComplianceStatement = true; return End(qcStatementContents); } if (statementId.MatchRest(id_etsi_qcs_QcType)) { Reader supportedStatementsContents; rv = ExpectTagAndGetValue(qcStatementContents, SEQUENCE, supportedStatementsContents); if (rv != Success) { return rv; } Reader supportedStatementId; rv = ExpectTagAndGetValue(supportedStatementsContents, OIDTag, supportedStatementId); if (supportedStatementId.MatchRest(id_etsi_qct_web)) { foundQCTypeStatementWithWebType = true; } rv = End(supportedStatementsContents); if (rv != Success) { return rv; } return End(qcStatementContents); } // Ignore the contents of unknown qcStatements. qcStatementContents.SkipToEnd(); return Success; }); if (rv != Success) { return false; } if (!qcStatements.AtEnd()) { return false; } return foundQCComplianceStatement && foundQCTypeStatementWithWebType; } // Helper function to determine if a certificate has a policy from the given // list of acceptable policies. bool CertHasPolicyFrom(Input cert, const nsTArray& policies) { using namespace mozilla::pkix::der; BackCert backCert(cert, EndEntityOrCA::MustBeEndEntity, nullptr); if (backCert.Init() != Success) { return false; } const Input* certificatePoliciesInput(backCert.GetCertificatePolicies()); if (!certificatePoliciesInput) { return false; } Reader certificatePolicies(*certificatePoliciesInput); // certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation // PolicyInformation ::= SEQUENCE { // policyIdentifier CertPolicyId, // ... // } // CertPolicyId ::= OBJECT IDENTIFIER bool foundPolicy = false; mozilla::pkix::Result rv = NestedOf(certificatePolicies, SEQUENCE, SEQUENCE, EmptyAllowed::No, [&](Reader& policyInformationContents) { Reader policyIdentifier; mozilla::pkix::Result rv = ExpectTagAndGetValue( policyInformationContents, OIDTag, policyIdentifier); if (rv != Success) { return rv; } for (const auto& policy : policies) { if (policyIdentifier.MatchRest(policy)) { foundPolicy = true; } } return Success; }); if (rv != Success) { return false; } if (!certificatePolicies.AtEnd()) { return false; } return foundPolicy; } // For 1-QWACs, ETSI TS 119 411-5 V2.1.1 clause 6.1.2 ("Validation of QWACs") // item 5 references clause 4.1.2, which references clause 4.1.1, which states // that such certificates must have either the QEVCP-w or QNCP-w policy as // specified in ETSI EN 319 411-2. bool CertHas1QWACPolicy(Input cert) { // QEVCP-w is itu-t(0) identified-organization(4) etsi(0) // qualified-certificate-policies(194112) policy-identifiers(1) qcp-web (4) // python DottedOIDToCode.py qevcp-w 0.4.0.194112.1.4 static const uint8_t qevcp_w[] = {0x04, 0x00, 0x8b, 0xec, 0x40, 0x01, 0x04}; // QNCP-w is itu-t(0) identified-organization(4) etsi(0) // qualified-certificate-policies(194112) policy-identifiers(1) qncp-web (5) // python DottedOIDToCode.py qncp-w 0.4.0.194112.1.5 static const uint8_t qncp_w[] = {0x04, 0x00, 0x8b, 0xec, 0x40, 0x01, 0x05}; return CertHasPolicyFrom(cert, {Input(qevcp_w), Input(qncp_w)}); } // For 2-QWACs, ETSI TS 119 411-5 V2.1.1 clause 6.1.2 ("Validation of QWACs") // item 5 references clause 4.2.2, which references clause 4.2.1, which states // that such certificates must have the QNCP-w-gen policy as specified in ETSI // EN 319 411-2. bool CertHas2QWACPolicy(Input cert) { // QEVCP-w-gen is itu-t(0) identified-organization(4) etsi(0) // qualified-certificate-policies(194112) policy-identifiers(1) // qncp-web-gen (6) // python DottedOIDToCode.py qevcp-w-gen 0.4.0.194112.1.6 static const uint8_t qevcp_w_gen[] = {0x04, 0x00, 0x8b, 0xec, 0x40, 0x01, 0x06}; return CertHasPolicyFrom(cert, {Input(qevcp_w_gen)}); } // ETSI TS 119 411-5 V2.1.1 states that "The 2-QWAC certificate shall be issued // in accordance with ETSI EN 319 412-4 [4] for the relevant certificate policy // as identified in clause 4.2.1 of the present document, except as described // below: // * the extKeyUsage value shall only assert the extendedKeyUsage purpose of // id-kp-tls-binding as specified in Annex A." // This is interpreted to mean the 2-QWAC certificate must have an // extendedKeyUsage extension and it must contain only id-kp-tls-binding, and // that there are no particular restrictions or requirements of the other // certificates in the chain with regard to EKU extensions. bool CertOnlyHasTLSBindingEKU(Input cert) { using namespace mozilla::pkix::der; // ETSI TS 119 411-5 V2.1.1 Annex A: // id-tlsBinding OBJECT IDENTIFIER ::= { itu-t(0) identified-organization(4) // etsi(0) id-qwacImplementation(194115) tls-binding (1) } // id-kp-tls-binding OBJECT IDENTIFIER ::= { id-tlsBinding // id-kp-tls-binding(0) } // python DottedOIDToCode.py id-kp-tls-binding 0.4.0.194115.1.0 static const uint8_t id_kp_tls_binding[] = {0x04, 0x00, 0x8b, 0xec, 0x43, 0x01, 0x00}; BackCert backCert(cert, EndEntityOrCA::MustBeEndEntity, nullptr); if (backCert.Init() != Success) { return false; } const Input* ekuInput(backCert.GetExtKeyUsage()); if (!ekuInput) { return false; } Reader eku(*ekuInput); // Normally, the extended key usage extension is defined like so: // ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId // KeyPurposeId ::= OBJECT IDENTIFIER // That is, it consists of a SEQUENCE of OBJECT IDENTIFIERs, where each OID // identifies a key purpose. However, for 2-QWACs, the EKU must consist of // exactly one key purpose ID of id-kp-tls-binding. mozilla::pkix::Result rv = Nested(eku, SEQUENCE, OIDTag, [&](Reader& r) { if (r.MatchRest(id_kp_tls_binding)) { return Success; } return mozilla::pkix::Result::ERROR_INADEQUATE_CERT_TYPE; }); if (rv != Success) { return false; } return eku.AtEnd(); } nsresult VerifyQWACTask::CalculateResult() { mozilla::psm::QWACTrustDomain trustDomain(mCollectedCerts); nsTArray certDER; nsresult rv = mCert->GetRawDER(certDER); if (NS_FAILED(rv)) { return rv; } Input cert; if (cert.Init(certDER.Elements(), certDER.Length()) != Success) { return NS_ERROR_FAILURE; } if (!CertHasQWACSQCStatements(cert)) { return NS_OK; } if (mType == nsIX509CertDB::QWACType::OneQWAC) { if (!CertHas1QWACPolicy(cert)) { return NS_OK; } } else if (mType == nsIX509CertDB::QWACType::TwoQWAC) { if (!CertHas2QWACPolicy(cert)) { return NS_OK; } if (!CertOnlyHasTLSBindingEKU(cert)) { return NS_OK; } } else { MOZ_ASSERT_UNREACHABLE("unhandled QWAC type"); return NS_ERROR_FAILURE; } if (BuildCertChain(trustDomain, cert, Now(), EndEntityOrCA::MustBeEndEntity, KeyUsage::noParticularKeyUsageRequired, KeyPurposeId::anyExtendedKeyUsage, CertPolicyId::anyPolicy, nullptr) != Success) { return NS_OK; } // For 1-QWACs, the hostname should have already been validated in the TLS // handshake. However, this operation is not expensive, and it ensures all // required checks have been done, in case 1-QWACs are ever re-used in a // different context. Input hostname; if (hostname.Init(mozilla::BitwiseCast( mHostname.BeginReading()), mHostname.Length()) != Success) { return NS_OK; } // According to ETSI EN 319 412-4 V1.4.1 section 4, certificates following // EVCP or QEVCP-w (which includes 1-QWACs) are subject to the CA/Browser // Forum's EV Guidelines, which incorporates the Baseline Requirements. // Certificates following QNCP-w-gen (which includes 2-QWACs) are subject to // the Baseline Requirements with respect to the subject alternative name // extension. if (CheckCertHostname(cert, hostname) != Success) { return NS_OK; } mVerified = true; return NS_OK; } void VerifyQWACTask::CallCallback(nsresult rv) { if (NS_FAILED(rv)) { mPromise->MaybeReject(rv); } else { mPromise->MaybeResolve(mVerified); } } NS_IMETHODIMP nsNSSCertificateDB::AsyncVerifyQWAC( QWACType aType, nsIX509Cert* aCert, const nsACString& aHostname, const nsTArray>& aCollectedCerts, JSContext* aCx, mozilla::dom::Promise** aPromise) { NS_ENSURE_ARG_POINTER(aCx); nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); if (!globalObject) { return NS_ERROR_UNEXPECTED; } mozilla::ErrorResult result; RefPtr promise = Promise::Create(globalObject, result); if (result.Failed()) { return result.StealNSResult(); } RefPtr task( new VerifyQWACTask(aType, aCert, aHostname, aCollectedCerts, promise)); nsresult rv = task->Dispatch(); if (NS_FAILED(rv)) { return rv; } promise.forget(aPromise); return NS_OK; }