blob: 48ff8a566a8ba3f41f3e15c3553010bca49f24e7 [file]
// Copyright 2026 Google LLC
//
// 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
//
// https://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.
//
#include "crypto/default/cms/verify_signature.h"
#include <stdint.h>
#include <memory>
#include <string>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "crypto/default/cms/certificates.h"
#include "crypto/default/cms/cms_error_code.h"
#include "crypto/default/cms/cms_parser.h"
#include "crypto/default/cms/cms_test_helper.h"
#include "crypto/default/cms/oids.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "openssl/base.h"
#include "openssl/bytestring.h"
#include "openssl/mem.h"
#include "openssl/nid.h"
#include "openssl/obj.h"
#include "openssl/obj_mac.h"
#include "openssl/safestack.h"
#include "openssl/stack.h"
namespace credentio_cms {
namespace {
using ::absl_testing::IsOk;
using ::absl_testing::StatusIs;
ByteString ByteStringFromString(const std::string& value) {
ByteString bs;
CBS_init(bs.cbs_ptr(), reinterpret_cast<const unsigned char*>(value.data()),
value.length());
return bs;
}
std::string ByteStringToString(const ByteString& bs) {
return std::string(reinterpret_cast<const char*>(CBS_data(bs.cbs_ptr())),
CBS_len(bs.cbs_ptr()));
}
class VerifySignatureTest : public ::testing::Test {
protected:
void ParseOpaqueMessage(const char* filename) {
std::string message = GetFileContents(filename);
cms_object_der_ = GetCms(message);
std::string error_message;
EXPECT_EQ(ErrorCode::OK,
ParseCms(cms_object_der_.data(), cms_object_der_.length(), &cms_,
&error_message))
<< "Failed to parse: " << filename << " error: " << error_message;
}
void ParseMessage(const char* filename) {
std::string message = GetFileContents(filename);
// The first mime part in the message, in the tests this is the clear text.
first_part_ = GetFirstMimePart(message);
cms_object_der_ = GetCms(message);
std::string error_message;
EXPECT_EQ(ErrorCode::OK,
ParseCms(cms_object_der_.data(), cms_object_der_.length(), &cms_,
&error_message))
<< error_message;
}
// Pick the first certificate matching the signer restrictions, or null if
// none was found.
X509* GetFirstMatchingCert(const Content& content, const SignerInfo& signer) {
certificate_stack_.reset(GetAllCertificates(content));
if (certificate_stack_ == nullptr) {
return nullptr;
}
std::vector<X509*> matches =
GetSignerCertificates(certificate_stack_.get(), signer);
if (matches.empty()) {
return nullptr;
}
return matches[0];
}
std::string first_part_;
std::string cms_object_der_;
Content cms_;
bssl::UniquePtr<STACK_OF(X509)> certificate_stack_;
};
class VerifyOpaqueSignaturesTest
: public VerifySignatureTest,
public ::testing::WithParamInterface<const char*> {};
TEST_P(VerifyOpaqueSignaturesTest, CheckSignature) {
ParseOpaqueMessage(GetParam());
ASSERT_EQ(1, cms_.signers.size());
ASSERT_EQ(1, cms_.content.size());
for (const auto& signer : cms_.signers) {
X509* certificate = GetFirstMatchingCert(cms_, signer);
ASSERT_FALSE(certificate == nullptr)
<< "Failed to get the certificate for: " << GetParam()
<< " and Issuer: " << CBSToName(signer.issuer_name);
auto status = VerifySignature(cms_, signer, cms_.content, *certificate);
EXPECT_TRUE(status.ok()) << status.status();
// Sanity check: change the first bit and check that the signature fails.
std::string data = ByteStringToString(cms_.content[0]);
data[0] ^= 0x80;
ByteString modified = ByteStringFromString(data);
EXPECT_FALSE(VerifySignature(cms_, signer, {modified}, *certificate).ok());
}
}
INSTANTIATE_TEST_SUITE_P(EmbeddedContent, VerifyOpaqueSignaturesTest,
testing::Values("signed-openssl-opaque.msg",
"signed-openssl-ecdsa.msg"));
class VerifyDetachedSignaturesTest
: public VerifySignatureTest,
public ::testing::WithParamInterface<const char*> {};
TEST_P(VerifyDetachedSignaturesTest, CheckSignatureWithExternalContent) {
ParseMessage(GetParam());
ASSERT_EQ(1, cms_.signers.size());
EXPECT_EQ(0, cms_.content.size());
ByteString data = ByteStringFromString(first_part_);
for (const auto& signer : cms_.signers) {
X509* certificate = GetFirstMatchingCert(cms_, signer);
ASSERT_FALSE(certificate == nullptr);
auto status = VerifySignature(cms_, signer, {data}, *certificate);
EXPECT_TRUE(status.ok()) << status.status();
}
}
INSTANTIATE_TEST_SUITE_P(ExternalContent, VerifyDetachedSignaturesTest,
testing::Values("signed-openssl-detached.msg",
"signed-outlook-mac-2011.msg",
"signed-thunderbird.msg",
"signed-openssl-pss.msg",
"signed-openssl-sha256.msg"));
class VerifyFailedSignaturesTest
: public VerifySignatureTest,
public ::testing::WithParamInterface<const char*> {};
TEST_P(VerifyFailedSignaturesTest, CheckSignatureWithExternalContent) {
ParseMessage(GetParam());
ASSERT_EQ(1, cms_.signers.size());
EXPECT_EQ(0, cms_.content.size());
ByteString data = ByteStringFromString(first_part_);
for (const auto& signer : cms_.signers) {
X509* certificate = GetFirstMatchingCert(cms_, signer);
ASSERT_FALSE(certificate == nullptr);
auto status = VerifySignature(cms_, signer, {data}, *certificate);
EXPECT_FALSE(status.ok()) << status.status();
}
}
INSTANTIATE_TEST_SUITE_P(
ExternalContent, VerifyFailedSignaturesTest,
testing::Values("signed-type-mismatch.msg",
"signed-unknown-hash-algorithm.msg",
"signed-unknown-signature-algorithm.msg"));
TEST_F(VerifySignatureTest, MissingSignedAttributes) {
// The signed attributes are needed if the the envelope content type is not
// data.
ParseMessage("signed-openssl-detached.msg");
auto& signer = cms_.signers[0];
// Clear the signed attributes.
CBS_init(signer.raw_signed_attributes.cbs_ptr(), nullptr, 0);
CBS_init(cms_.content_type.cbs_ptr(), kSignedDataOid, sizeof(kSignedDataOid));
ByteString data = ByteStringFromString(first_part_);
X509* certificate = GetFirstMatchingCert(cms_, signer);
ASSERT_FALSE(certificate == nullptr);
auto status = VerifySignature(cms_, signer, {data}, *certificate);
EXPECT_FALSE(status.ok()) << status.status();
}
TEST_F(VerifySignatureTest, MissingSignerDigest) {
ParseMessage("signed-openssl-detached.msg");
auto& signer = cms_.signers[0];
// Clear the signed message digest attribute.
CBS_init(signer.message_digest.cbs_ptr(), nullptr, 0);
ByteString data = ByteStringFromString(first_part_);
X509* certificate = GetFirstMatchingCert(cms_, signer);
ASSERT_FALSE(certificate == nullptr);
auto status = VerifySignature(cms_, signer, {data}, *certificate);
EXPECT_FALSE(status.ok()) << status.status();
}
TEST_F(VerifySignatureTest, MissingContentTypeSigned) {
ParseMessage("signed-openssl-detached.msg");
auto& signer = cms_.signers[0];
// Clear the signed content_type_signed attribute.
CBS_init(signer.content_type_signed.cbs_ptr(), nullptr, 0);
ByteString data = ByteStringFromString(first_part_);
X509* certificate = GetFirstMatchingCert(cms_, signer);
ASSERT_FALSE(certificate == nullptr);
auto status = VerifySignature(cms_, signer, {data}, *certificate);
EXPECT_FALSE(status.ok()) << status.status();
}
TEST_F(VerifySignatureTest, CertificateWithBrokenPubKey) {
ParseMessage("signed-openssl-detached.msg");
ByteString data = ByteStringFromString(first_part_);
// user1_bad_public_key has a public key with an unknown OID.
bssl::UniquePtr<X509> certificate(
PemToCertificate(GetFileContents("user1_bad_public_key.pem")));
auto status = VerifySignature(cms_, cms_.signers[0], {data}, *certificate);
EXPECT_FALSE(status.ok()) << status.status();
}
TEST_F(VerifySignatureTest, BadSignatureDigestAlgorithm) {
ParseMessage("signed-openssl-detached.msg");
auto& signer = cms_.signers[0];
// Use an OID known to OpenSSL but that is not a valid hash or signature
// algorithm with an implied hash function.
// 1.2.840.113549.3.7 DES-EDE3-CBC
auto oid = NidToDerString(NID_des_ede3_cbc);
signer.signature_algorithm.algorithm_oid = ByteStringFromString(oid);
signer.digest_algorithm.algorithm_oid = ByteStringFromString(oid);
ByteString data = ByteStringFromString(first_part_);
X509* certificate = GetFirstMatchingCert(cms_, signer);
ASSERT_FALSE(certificate == nullptr);
auto status = VerifySignature(cms_, signer, {data}, *certificate);
EXPECT_FALSE(status.ok()) << status.status();
}
TEST_F(VerifySignatureTest, BadHashDigestAlgorithm) {
ParseMessage("signed-openssl-sha256.msg");
auto& signer = cms_.signers[0];
// Use a signature OID that provides an explicit hashing algorithm.
auto sig_oid = NidToDerString(NID_sha256WithRSAEncryption);
signer.signature_algorithm.algorithm_oid = ByteStringFromString(sig_oid);
ByteString data = ByteStringFromString(first_part_);
X509* certificate = GetFirstMatchingCert(cms_, signer);
ASSERT_FALSE(certificate == nullptr);
auto status = VerifySignature(cms_, signer, {data}, *certificate);
// Make sure that the signature algorithm is correct.
EXPECT_TRUE(status.ok()) << status.status();
// Use an OID known to OpenSSL but that is not a valid hash algorithm.
// 1.2.840.113549.3.7 DES-EDE3-CBC
auto digest_oid = NidToDerString(NID_des_ede3_cbc);
signer.digest_algorithm.algorithm_oid = ByteStringFromString(digest_oid);
status = VerifySignature(cms_, signer, {data}, *certificate);
EXPECT_FALSE(status.ok()) << status.status();
}
TEST_F(VerifySignatureTest, WrongSignedHashLength) {
ParseMessage("signed-openssl-sha256.msg");
auto& signer = cms_.signers[0];
// Shrink the digest length by one.
CBS_init(signer.message_digest.cbs_ptr(),
CBS_data(signer.message_digest.cbs_ptr()),
CBS_len(signer.message_digest.cbs_ptr()) - 1);
ByteString data = ByteStringFromString(first_part_);
X509* certificate = GetFirstMatchingCert(cms_, signer);
ASSERT_FALSE(certificate == nullptr);
auto status = VerifySignature(cms_, signer, {data}, *certificate);
EXPECT_FALSE(status.ok()) << status.status();
}
TEST_F(VerifySignatureTest, MissingPSSParameters) {
ParseMessage("signed-openssl-pss.msg");
auto& signer = cms_.signers[0];
CBS_init(signer.signature_algorithm.parameter.cbs_ptr(), nullptr, 0);
ByteString data = ByteStringFromString(first_part_);
X509* certificate = GetFirstMatchingCert(cms_, signer);
ASSERT_FALSE(certificate == nullptr);
auto status = VerifySignature(cms_, signer, {data}, *certificate);
EXPECT_FALSE(status.ok()) << status.status();
}
TEST_F(VerifySignatureTest, CheckSignatureInfo) {
ParseMessage("signed-openssl-pss.msg");
auto& signer = cms_.signers[0];
ByteString data = ByteStringFromString(first_part_);
X509* certificate = GetFirstMatchingCert(cms_, signer);
ASSERT_FALSE(certificate == nullptr);
auto status = VerifySignature(cms_, signer, {data}, *certificate);
ASSERT_TRUE(status.ok()) << status.status();
EXPECT_EQ(NID_rsassaPss, status.value().signature_algorithm_id);
EXPECT_EQ(NID_sha256, status.value().digest_algorithm_nid);
}
TEST_F(VerifySignatureTest, MismatchedHashDigestAlgorithmEcdsa) {
ParseOpaqueMessage("signed-openssl-ecdsa.msg");
auto& signer = cms_.signers[0];
// Use a signature OID that provides an explicit hashing algorithm and use the
// right hash algorithm to make sure the test case is valid.
auto sig_oid = NidToDerString(NID_ecdsa_with_SHA256);
signer.signature_algorithm.algorithm_oid = ByteStringFromString(sig_oid);
auto good_digest_oid = NidToDerString(NID_sha256);
signer.digest_algorithm.algorithm_oid = ByteStringFromString(good_digest_oid);
ByteString data = ByteStringFromString(first_part_);
X509* certificate = GetFirstMatchingCert(cms_, signer);
ASSERT_FALSE(certificate == nullptr);
// Make sure signature algorithm is correct.
EXPECT_THAT(VerifySignature(cms_, signer, cms_.content, *certificate),
IsOk());
// Set hash algorithm to a supported value that is different from the one
// implied by the signature algorithm.
auto bad_digest_oid = NidToDerString(NID_sha384);
signer.digest_algorithm.algorithm_oid = ByteStringFromString(bad_digest_oid);
EXPECT_THAT(
VerifySignature(cms_, signer, {data}, *certificate),
StatusIs(
absl::StatusCode::kInvalidArgument,
R"(The digest algorithm does not match the value derived from the signature algorithm: digest_nid=673, signature_digest_nid=672)"));
}
} // namespace
} // namespace credentio_cms