blob: 8d223e5d77ddf5dc9204443d811bd08f214efb13 [file] [log] [blame]
// 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/cert/two_qwac.h"
#include <stdint.h>
#include "base/base64url.h"
#include "base/containers/span.h"
#include "base/functional/callback.h"
#include "base/values.h"
#include "crypto/hash.h"
#include "net/test/cert_builder.h"
#include "net/test/two_qwac_cert_binding_builder.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/boringssl/src/include/openssl/ec_key.h"
#include "third_party/boringssl/src/include/openssl/rsa.h"
namespace net {
namespace {
TEST(ParseTlsCertificateBinding, MinimalValidBinding) {
// Build a header that has the minimally required set of parameters
TwoQwacCertBindingBuilder binding_builder;
std::string jws = binding_builder.GetJWS();
auto cert_binding = TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
}
TEST(ParseTlsCertificateBinding, MaximalValidBinding) {
TwoQwacCertBindingBuilder binding_builder;
// Set all of the optional fields in the header.
binding_builder.SetHeaderOverrides(
base::DictValue()
.Set("kid", base::Value::Dict()
.Set("random key", "random value")
.Set("kids can have", "whatever they want"))
.Set("x5t#S256", "base64urlhashA")
.Set("iat", 12345)
.Set("exp", 67.89)
.Set("crit", base::ListValue().Append("sigD"))
.Set("sigD",
base::DictValue().Set("ctys", base::Value::List()
.Append("content-type1")
.Append("content-type2"))));
std::string jws = binding_builder.GetJWS();
auto cert_binding = TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
}
TEST(ParseTlsCertificateBinding, RS256ValidSigAlg) {
TwoQwacCertBindingBuilder binding_builder;
binding_builder.SetHeaderOverrides(base::DictValue().Set("alg", "RS256"));
std::string jws = binding_builder.GetJWS();
auto cert_binding = TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
ASSERT_EQ(cert_binding->header().sig_alg, JwsSigAlg::kRsaPkcs1Sha256);
}
TEST(ParseTlsCertificateBinding, PS256ValidSigAlg) {
TwoQwacCertBindingBuilder binding_builder;
binding_builder.SetHeaderOverrides(base::DictValue().Set("alg", "PS256"));
std::string jws = binding_builder.GetJWS();
auto cert_binding = TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
ASSERT_EQ(cert_binding->header().sig_alg, JwsSigAlg::kRsaPssSha256);
}
TEST(ParseTlsCertificateBinding, InvalidSigAlg) {
TwoQwacCertBindingBuilder binding_builder;
binding_builder.SetHeaderOverrides(base::DictValue().Set("alg", "RSA1_5"));
std::string jws = binding_builder.GetJWS();
EXPECT_FALSE(jws.empty());
auto cert_binding = TwoQwacCertBinding::Parse(jws);
ASSERT_FALSE(cert_binding.has_value());
}
// Test failure when the JWS header isn't a JSON object.
TEST(ParseTlsCertificateBinding, JwsHeaderNotObject) {
std::string header = "[]";
std::string jws;
base::Base64UrlEncode(header, base::Base64UrlEncodePolicy::OMIT_PADDING,
&jws);
jws += "..";
EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
}
// Test failure when the JWS header isn't JSON.
TEST(ParseTlsCertificateBinding, JwsHeaderNotJson) {
std::string header = "AAA";
std::string jws;
base::Base64UrlEncode(header, base::Base64UrlEncodePolicy::OMIT_PADDING,
&jws);
jws += "..";
EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
}
// Test failure when the JWS header isn't valid base64url.
TEST(ParseTlsCertificateBinding, JwsHeaderNotBase64) {
// the header is encoded as "A", which is too short to be base64url.
std::string jws = "A..";
EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
}
// Test failure when the JWS payload is non-empty.
TEST(ParseTlsCertificateBinding, JwsPayloadNonEmpty) {
std::string header_b64 = TwoQwacCertBindingBuilder().GetHeader();
// Make a JWS consisting of a valid header, a payload (base64url-encoded as
// "AAAA") and an empty signature.
std::string jws = header_b64 + ".AAAA.";
EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
}
// Test failure when the JWS signature is not valid base64url.
TEST(ParseTlsCertificateBinding, JwsSignatureNotBase64) {
std::string header_b64 = TwoQwacCertBindingBuilder().GetHeader();
std::string jws = header_b64 + "..A";
EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
}
// Test failure when the JWS consists of 2 components instead of 3.
TEST(ParseTlsCertificateBinding, JwsHas2Components) {
std::string header_b64 = TwoQwacCertBindingBuilder().GetHeader();
std::string jws = header_b64 + ".AAAA";
EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
}
// Test failure when the JWS consists of 4 components instead of 3.
TEST(ParseTlsCertificateBinding, JwsHas4Components) {
std::string header_b64 = TwoQwacCertBindingBuilder().GetHeader();
std::string jws = header_b64 + "..AAAA.AAAA";
EXPECT_FALSE(TwoQwacCertBinding::Parse(jws).has_value());
}
TEST(ParseTlsCertificateBinding, InvalidFields) {
const struct {
std::string header_key;
base::Value value;
} kTests[] = {
{
// "alg" expects a string
"alg",
base::Value(1),
},
{
// "alg" expects a supported signature algorithm from the IANA
// registry. "none" is in the registry but we will never support it.
"alg",
base::Value("none"),
},
{
// "cty" expects a string
"cty",
base::Value(1),
},
{
// "cty" expects a specific value for its string
"cty",
base::Value("TLS-Certificate-Binding-v2"),
},
{
// "x5t#S256" expects a string
"x5t#S256",
base::Value(1),
},
{
// "x5c" expects a list
"x5c",
base::Value("wrong type"),
},
{
// "x5c" expects strings in its list
"x5c",
base::Value(base::ListValue().Append(1)),
},
{
// "x5c" expects base64 strings in its list. Test with a base64url
// (but not regular base64) string.
"x5c",
base::Value(base::ListValue().Append("M-_A")),
},
{
// "x5c" expects the base64 strings in its list to be valid X.509
// certificates. This string is valid base64, but is a (very)
// truncated X.509 certificate.
"x5c",
base::Value(base::ListValue().Append("MIID")),
},
{
// "iat" expects an int (when used for 2-QWACs). "iat" more generally
// (according to RFC 7519) can be a double, but we don't allow that,
// so explicitly check that doubles are rejected.
"iat",
base::Value(1.0),
},
{
// "exp" expects a numeric value
"exp",
base::Value("wrong type"),
},
{
// "crit", if present, can only contain "sigD"
"crit",
base::Value(base::ListValue().Append("sigD").Append("x5c")),
},
{
// "crit" expects a list
"crit",
base::Value("wrong type"),
},
{
// "sigD" expects an object
"sigD",
base::Value(base::ListValue()),
},
{
// The 2-QWAC TLS Certificate Binding JAdES profile only allows
// specific fields in the JWS header, and "x5u" is not one of them.
"x5u",
base::Value("X.509 URL"),
},
};
for (const auto& test : kTests) {
SCOPED_TRACE(test.header_key);
TwoQwacCertBindingBuilder binding_builder;
binding_builder.SetHeaderOverrides(
base::DictValue().Set(test.header_key, test.value.Clone()));
std::string jws = binding_builder.GetJWS();
auto cert_binding = TwoQwacCertBinding::Parse(jws);
ASSERT_FALSE(cert_binding.has_value());
}
}
TEST(ParseTlsCertificateBinding, SigDHeaderParam) {
const struct {
std::string name;
base::RepeatingCallback<void(base::DictValue*)> header_func;
bool valid;
} kTests[] = {
{
"wrong mId",
base::BindRepeating([](base::DictValue* sig_d) {
sig_d->Set("mId", "http://uri.etsi.org/19182/ObjectIdByURI");
}),
false,
},
{
"wrong mId type",
base::BindRepeating(
[](base::DictValue* sig_d) { sig_d->Set("mId", 1); }),
false,
},
{
"wrong pars type",
base::BindRepeating(
[](base::DictValue* sig_d) { sig_d->Set("pars", 1); }),
false,
},
{
"SHA-256 supported",
base::BindRepeating(
[](base::DictValue* sig_d) { sig_d->Set("hashM", "S256"); }),
true,
},
{
"SHA-384 supported",
base::BindRepeating(
[](base::DictValue* sig_d) { sig_d->Set("hashM", "S384"); }),
true,
},
{
"SHA-512 supported",
base::BindRepeating(
[](base::DictValue* sig_d) { sig_d->Set("hashM", "S512"); }),
true,
},
{
"Other hashM values not supported",
base::BindRepeating(
[](base::DictValue* sig_d) { sig_d->Set("hashM", "SHA1"); }),
false,
},
{
"wrong hashM type",
base::BindRepeating(
[](base::DictValue* sig_d) { sig_d->Set("hashM", 1); }),
false,
},
{
"wrong type in pars list",
base::BindRepeating([](base::DictValue* sig_d) {
// "pars" and "hashV" must have the same length.
sig_d->Set("pars", base::ListValue().Append(1));
sig_d->Set("hashV", base::ListValue().Append("fakehash"));
}),
false,
},
{
"disallowed base64 padding in hashV",
base::BindRepeating([](base::DictValue* sig_d) {
// "ctys" must have the same length as "pars" and "hashV".
sig_d->Set("pars", base::ListValue().Append("URL"));
// hashV list elements are base64url encoded with no padding
sig_d->Set("hashV", base::ListValue().Append("fakehashAA=="));
}),
false,
},
{
"bad base64url encoding in hashV",
base::BindRepeating([](base::DictValue* sig_d) {
// "ctys" must have the same length as "pars" and "hashV".
sig_d->Set("pars", base::ListValue().Append("URL"));
// a base64url input (with no padding) is malformed if its length
// mod 4 is 1.
sig_d->Set("hashV", base::ListValue().Append("fakehash1"));
}),
false,
},
{
"wrong type in hashV list",
base::BindRepeating([](base::DictValue* sig_d) {
// "ctys" must have the same length as "pars" and "hashV".
sig_d->Set("pars", base::ListValue().Append("URL"));
sig_d->Set("hashV", base::ListValue().Append(1));
}),
false,
},
{
"wrong hashV type",
base::BindRepeating(
[](base::DictValue* sig_d) { sig_d->Set("hashV", 1); }),
false,
},
{
"correct ctys type",
base::BindRepeating([](base::DictValue* sig_d) {
// "ctys" must have the same length as "pars" and "hashV".
sig_d->Set("pars", base::ListValue().Append("URL"));
sig_d->Set("hashV", base::ListValue().Append("fakehash"));
sig_d->Set("ctys", base::ListValue().Append("content type"));
}),
true,
},
{
"wrong ctys type",
base::BindRepeating(
[](base::DictValue* sig_d) { sig_d->Set("ctys", "wrong type"); }),
false,
},
{
"wrong type inside ctys list",
base::BindRepeating([](base::DictValue* sig_d) {
// "ctys" must have the same length as "pars" and "hashV".
sig_d->Set("pars", base::ListValue().Append("URL"));
sig_d->Set("hashV", base::ListValue().Append("fakehash"));
sig_d->Set("ctys", base::ListValue().Append(1));
}),
false,
},
{
"pars length mismatch",
base::BindRepeating([](base::DictValue* sig_d) {
// "ctys" must have the same length as "pars" and "hashV".
sig_d->Set("pars", base::ListValue().Append("URL").Append("URL 2"));
sig_d->Set("hashV", base::ListValue().Append("fakehash"));
sig_d->Set("ctys", base::ListValue().Append("content type"));
}),
false,
},
{
"hashV length mismatch",
base::BindRepeating([](base::DictValue* sig_d) {
// "ctys" must have the same length as "pars" and "hashV".
sig_d->Set("pars", base::ListValue().Append("URL"));
sig_d->Set("hashV",
base::ListValue().Append("fakehash").Append("hashfake"));
sig_d->Set("ctys", base::ListValue().Append("content type"));
}),
false,
},
{
"ctys length mismatch",
base::BindRepeating([](base::DictValue* sig_d) {
// "ctys" must have the same length as "pars" and "hashV".
sig_d->Set("pars", base::ListValue().Append("URL"));
sig_d->Set("hashV", base::ListValue().Append("fakehash"));
sig_d->Set("ctys", base::ListValue()
.Append("content type")
.Append("content type"));
}),
false,
},
{
"unknown member in sigD",
base::BindRepeating(
[](base::DictValue* sig_d) { sig_d->Set("spURI", "URL"); }),
false,
},
};
for (const auto& test : kTests) {
SCOPED_TRACE(test.name);
base::DictValue sig_d;
test.header_func.Run(&sig_d);
TwoQwacCertBindingBuilder binding_builder;
binding_builder.SetHeaderOverrides(
base::DictValue().Set("sigD", std::move(sig_d)));
std::string jws = binding_builder.GetJWS();
auto cert_binding = TwoQwacCertBinding::Parse(jws);
EXPECT_EQ(cert_binding.has_value(), test.valid);
}
}
TEST(VerifyTwoQwacCertBinding, ValidSignatureRS256) {
TwoQwacCertBindingBuilder binding_builder;
// Use RSA-PKCS1v1.5 for the TLS Certificate Binding.
binding_builder.SetJwsSigAlg(JwsSigAlg::kRsaPkcs1Sha256);
std::string jws = binding_builder.GetJWS();
std::optional<TwoQwacCertBinding> cert_binding =
TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
// Check that the JWS header has "alg": "RS256"
EXPECT_EQ(cert_binding->header().sig_alg, JwsSigAlg::kRsaPkcs1Sha256);
EXPECT_TRUE(cert_binding->VerifySignature());
}
TEST(VerifyTwoQwacCertBinding, ValidSignaturePS256) {
TwoQwacCertBindingBuilder binding_builder;
// Use RSA-PSS for the TLS Certificate Binding.
binding_builder.SetJwsSigAlg(JwsSigAlg::kRsaPssSha256);
std::string jws = binding_builder.GetJWS();
std::optional<TwoQwacCertBinding> cert_binding =
TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
// Check that the JWS header has "alg": "PS256"
EXPECT_EQ(cert_binding->header().sig_alg, JwsSigAlg::kRsaPssSha256);
EXPECT_TRUE(cert_binding->VerifySignature());
}
TEST(VerifyTwoQwacCertBinding, ValidSignatureES256) {
TwoQwacCertBindingBuilder binding_builder;
// Use ECDSA for the TLS Certificate Binding.
binding_builder.SetJwsSigAlg(JwsSigAlg::kEcdsaP256Sha256);
std::string jws = binding_builder.GetJWS();
std::optional<TwoQwacCertBinding> cert_binding =
TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
// Check that the JWS header has "alg": "ES256"
EXPECT_EQ(cert_binding->header().sig_alg, JwsSigAlg::kEcdsaP256Sha256);
EXPECT_TRUE(cert_binding->VerifySignature());
}
TEST(VerifyTwoQwacCertBinding, InvalidEcdsaCurve) {
TwoQwacCertBindingBuilder binding_builder;
// Set "ES256" as the JWS signature algorithm.
binding_builder.SetJwsSigAlg(JwsSigAlg::kEcdsaP256Sha256);
// Set the leaf cert to use a P-384 key.
bssl::UniquePtr<EC_KEY> ec_key(EC_KEY_new_by_curve_name(NID_secp384r1));
ASSERT_TRUE(EC_KEY_generate_key(ec_key.get()));
bssl::UniquePtr<EVP_PKEY> pkey(EVP_PKEY_new());
ASSERT_TRUE(EVP_PKEY_assign_EC_KEY(pkey.get(), ec_key.release()));
binding_builder.GetLeafBuilder()->SetKey(std::move(pkey));
std::string jws = binding_builder.GetJWS();
std::optional<TwoQwacCertBinding> cert_binding =
TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
// Check that the JWS header has "alg": "ES256"
EXPECT_EQ(cert_binding->header().sig_alg, JwsSigAlg::kEcdsaP256Sha256);
// Since the key uses the wrong curve, the signature verification should fail.
EXPECT_FALSE(cert_binding->VerifySignature());
}
TEST(VerifyTwoQwacCertBinding, InvalidSignature) {
TwoQwacCertBindingBuilder binding_builder;
const auto& header = binding_builder.GetHeader();
std::string signature = binding_builder.GetSignature();
// Build the JWS from the header and signature and confirm the signature is
// valid.
std::string jws = header + ".." + signature;
std::optional<TwoQwacCertBinding> cert_binding =
TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
EXPECT_TRUE(cert_binding->VerifySignature());
// Mess with the base64url-encoded signature to make it invalid.
if (signature[0] != 'A') {
signature[0] = 'A';
} else {
signature[0] = 'B';
}
// rebuild the JWS with the invalid signature, and check that the signature
// is invalid.
std::string jws_bad_sig = header + ".." + signature;
std::optional<TwoQwacCertBinding> cert_binding_bad_sig =
TwoQwacCertBinding::Parse(jws_bad_sig);
ASSERT_TRUE(cert_binding_bad_sig.has_value());
EXPECT_FALSE(cert_binding_bad_sig->VerifySignature());
}
TEST(TwoQwacCertBinding, BoundCertPresent) {
auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
std::string bound_cert = leaf->GetDER();
TwoQwacCertBindingBuilder binding_builder;
binding_builder.SetBoundCerts({bound_cert});
std::string jws = binding_builder.GetJWS();
std::optional<TwoQwacCertBinding> cert_binding =
TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
EXPECT_TRUE(cert_binding->BindsTlsCert(base::as_byte_span(bound_cert)));
}
TEST(TwoQwacCertBinding, MultipleBoundCerts) {
auto [leaf1, root1] = net::CertBuilder::CreateSimpleChain2();
std::string bound_cert1 = leaf1->GetDER();
auto [leaf2, root2] = net::CertBuilder::CreateSimpleChain2();
std::string bound_cert2 = leaf2->GetDER();
TwoQwacCertBindingBuilder binding_builder;
binding_builder.SetBoundCerts({bound_cert1, bound_cert2});
std::string jws = binding_builder.GetJWS();
std::optional<TwoQwacCertBinding> cert_binding =
TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
EXPECT_TRUE(cert_binding->BindsTlsCert(base::as_byte_span(bound_cert1)));
EXPECT_TRUE(cert_binding->BindsTlsCert(base::as_byte_span(bound_cert2)));
}
TEST(TwoQwacCertBinding, UnboundCertNotFound) {
auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
std::string bound_cert = leaf->GetDER();
TwoQwacCertBindingBuilder binding_builder;
binding_builder.SetBoundCerts({bound_cert});
std::string jws = binding_builder.GetJWS();
std::optional<TwoQwacCertBinding> cert_binding =
TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
auto [leaf2, root2] = net::CertBuilder::CreateSimpleChain2();
std::string unbound_cert = leaf2->GetDER();
EXPECT_FALSE(cert_binding->BindsTlsCert(base::as_byte_span(unbound_cert)));
}
TEST(TwoQwacCertBinding, BoundCertPresentSha384) {
auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
std::string bound_cert = leaf->GetDER();
TwoQwacCertBindingBuilder binding_builder;
binding_builder.SetBoundCerts({bound_cert});
binding_builder.SetHashAlg(crypto::hash::kSha384);
std::string jws = binding_builder.GetJWS();
std::optional<TwoQwacCertBinding> cert_binding =
TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
EXPECT_TRUE(cert_binding->BindsTlsCert(base::as_byte_span(bound_cert)));
}
TEST(TwoQwacCertBinding, BoundCertPresentSha512) {
auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
std::string bound_cert = leaf->GetDER();
TwoQwacCertBindingBuilder binding_builder;
binding_builder.SetBoundCerts({bound_cert});
binding_builder.SetHashAlg(crypto::hash::kSha512);
std::string jws = binding_builder.GetJWS();
std::optional<TwoQwacCertBinding> cert_binding =
TwoQwacCertBinding::Parse(jws);
ASSERT_TRUE(cert_binding.has_value());
EXPECT_TRUE(cert_binding->BindsTlsCert(base::as_byte_span(bound_cert)));
}
} // namespace
} // namespace net