| // 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 |