| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/test/two_qwac_cert_binding_builder.h" |
| |
| #include "base/base64.h" |
| #include "base/base64url.h" |
| #include "base/json/json_string_value_serializer.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/boringssl/src/include/openssl/rsa.h" |
| |
| namespace net { |
| |
| TwoQwacCertBindingBuilder::TwoQwacCertBindingBuilder() |
| : cert_chain_(CertBuilder::CreateSimpleChain(2)) { |
| GetLeafBuilder()->SetCertificatePolicies({"0.4.0.194112.1.6"}); // QNCP-w-gen |
| GetLeafBuilder()->SetQwacQcStatements({bssl::der::Input(kEtsiQctWebOid)}); |
| GetLeafBuilder()->SetExtendedKeyUsages({bssl::der::Input(kIdKpTlsBinding)}); |
| GenerateKeyForSigAlg(); |
| // set bound_certs_ to two bogus values |
| bound_certs_ = {"one", "two"}; |
| } |
| |
| TwoQwacCertBindingBuilder::~TwoQwacCertBindingBuilder() = default; |
| |
| void TwoQwacCertBindingBuilder::GenerateKeyForSigAlg() { |
| switch (sig_alg_) { |
| case JwsSigAlg::kRsaPkcs1Sha256: |
| case JwsSigAlg::kRsaPssSha256: |
| cert_chain_[0]->GenerateRSAKey(); |
| break; |
| case JwsSigAlg::kEcdsaP256Sha256: |
| cert_chain_[0]->GenerateECKey(); |
| } |
| Invalidate(); |
| } |
| |
| std::string TwoQwacCertBindingBuilder::SigAlg() const { |
| switch (sig_alg_) { |
| case JwsSigAlg::kRsaPkcs1Sha256: |
| return "RS256"; |
| case JwsSigAlg::kRsaPssSha256: |
| return "PS256"; |
| case JwsSigAlg::kEcdsaP256Sha256: |
| return "ES256"; |
| } |
| return ""; |
| } |
| |
| std::string TwoQwacCertBindingBuilder::HashAlg() const { |
| switch (hash_alg_) { |
| case crypto::hash::kSha256: |
| return "S256"; |
| case crypto::hash::kSha384: |
| return "S384"; |
| case crypto::hash::kSha512: |
| return "S512"; |
| default: |
| return ""; |
| } |
| } |
| |
| base::ListValue TwoQwacCertBindingBuilder::GenerateX5cHeaderValue() { |
| base::ListValue x5c_list; |
| for (const auto& cert : cert_chain_) { |
| x5c_list.Append(base::Base64Encode(cert->GetDER())); |
| } |
| return x5c_list; |
| } |
| |
| base::DictValue TwoQwacCertBindingBuilder::GenerateSigDHeaderValue() { |
| base::DictValue sig_d = |
| base::Value::Dict() |
| .Set("mId", "http://uri.etsi.org/19182/ObjectIdByURIHash") |
| .Set("hashM", HashAlg()); |
| base::ListValue pars; |
| base::ListValue hash_v; |
| for (const auto& bound_cert : bound_certs_) { |
| // ETSI TS 119 182-1 clause 5.2.8.1: Each element of the "hashV" array |
| // shall contain the base64url-encoded digest value of the |
| // base64url-encoded data object. |
| std::string cert_b64; |
| base::Base64UrlEncode(bound_cert, base::Base64UrlEncodePolicy::OMIT_PADDING, |
| &cert_b64); |
| std::vector<uint8_t> cert_hash( |
| crypto::hash::DigestSizeForHashKind(hash_alg_)); |
| crypto::hash::Hash(hash_alg_, cert_b64, cert_hash); |
| std::string hash_b64; |
| base::Base64UrlEncode(cert_hash, base::Base64UrlEncodePolicy::OMIT_PADDING, |
| &hash_b64); |
| hash_v.Append(hash_b64); |
| pars.Append(""); |
| } |
| sig_d.Set("pars", std::move(pars)); |
| sig_d.Set("hashV", std::move(hash_v)); |
| return sig_d; |
| } |
| |
| void TwoQwacCertBindingBuilder::GenerateHeader() { |
| // Build the minimal JWS header needed for a 2-QWAC TLS certificate binding. |
| base::DictValue header = base::DictValue() |
| .Set("alg", SigAlg()) |
| .Set("cty", "TLS-Certificate-Binding-v1") |
| .Set("x5c", GenerateX5cHeaderValue()) |
| .Set("sigD", GenerateSigDHeaderValue()); |
| // Add/override values in the header |
| header.Merge(header_overrides_.Clone()); |
| |
| std::string header_string; |
| ASSERT_TRUE(JSONStringValueSerializer(&header_string).Serialize(header)); |
| header_b64_ = std::string(); |
| base::Base64UrlEncode( |
| header_string, base::Base64UrlEncodePolicy::OMIT_PADDING, &*header_b64_); |
| } |
| |
| void TwoQwacCertBindingBuilder::GenerateSignature() { |
| // All JWS signature algorithms that we support use SHA-256 as their digest. |
| const EVP_MD* digest = EVP_sha256(); |
| bssl::ScopedEVP_MD_CTX ctx; |
| EVP_PKEY_CTX* pkey_ctx; |
| EVP_PKEY* key = cert_chain_[0]->GetKey(); |
| ASSERT_TRUE( |
| EVP_DigestSignInit(ctx.get(), &pkey_ctx, EVP_sha256(), nullptr, key)); |
| if (sig_alg_ == JwsSigAlg::kRsaPssSha256) { |
| ASSERT_TRUE(EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, RSA_PKCS1_PSS_PADDING)); |
| ASSERT_TRUE(EVP_PKEY_CTX_set_rsa_mgf1_md(pkey_ctx, digest)); |
| ASSERT_TRUE(EVP_PKEY_CTX_set_rsa_pss_saltlen( |
| pkey_ctx, -1 /* match digest and salt length */)); |
| } |
| |
| // The JWS signing input is the the (base64url-encoded) header and payload |
| // concatenated and separated by a '.'. For a 2-QWAC cert binding, the |
| // payload is always empty. |
| const std::string& header = GetHeader(); |
| const std::array<uint8_t, 1> separator = {'.'}; |
| ASSERT_TRUE(EVP_DigestSignUpdate(ctx.get(), header.data(), header.size())); |
| ASSERT_TRUE( |
| EVP_DigestSignUpdate(ctx.get(), separator.data(), separator.size())); |
| size_t len = 0; |
| std::vector<uint8_t> sig; |
| ASSERT_TRUE(EVP_DigestSignFinal(ctx.get(), nullptr, &len)); |
| sig.resize(len); |
| ASSERT_TRUE(EVP_DigestSignFinal(ctx.get(), sig.data(), &len)); |
| sig.resize(len); |
| signature_b64_ = std::string(); |
| base::Base64UrlEncode(sig, base::Base64UrlEncodePolicy::OMIT_PADDING, |
| &*signature_b64_); |
| } |
| |
| } // namespace net |