| // 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 <stddef.h> | |
| #include <stdint.h> | |
| #include <memory> | |
| #include <string> | |
| #include <vector> | |
| #include "absl/status/status.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/str_format.h" | |
| #include "absl/strings/string_view.h" | |
| #include "crypto/default/cms/cms_parser.h" | |
| #include "crypto/default/cms/oids.h" | |
| #include "openssl/asn1.h" | |
| #include "openssl/base.h" | |
| #include "openssl/bytestring.h" | |
| #include "openssl/crypto.h" | |
| #include "openssl/digest.h" | |
| #include "openssl/err.h" | |
| #include "openssl/evp.h" | |
| #include "openssl/mem.h" | |
| #include "openssl/nid.h" | |
| #include "openssl/obj.h" | |
| #include "openssl/obj_mac.h" | |
| #include "openssl/objects.h" | |
| #include "openssl/rsa.h" | |
| #include "openssl/x509.h" | |
| using absl::StatusCode; | |
| namespace credentio_cms { | |
| namespace { | |
| // Append the OpenSSL error strings to the status message. | |
| absl::Status OpenSslError(absl::StatusCode code, | |
| absl::string_view error_message) { | |
| std::string message(error_message); | |
| const char *file, *data; | |
| int line, flags; | |
| while (uint32_t err = ERR_get_error_line_data(&file, &line, &data, &flags)) { | |
| if (file) { | |
| absl::StrAppendFormat(&message, "\n%s:%d ", file, line); | |
| } else { | |
| absl::StrAppend(&message, "\n"); | |
| } | |
| if (const char* reason = ERR_reason_error_string(err)) { | |
| absl::StrAppend(&message, reason); | |
| } else { | |
| absl::StrAppend(&message, err); | |
| } | |
| if (data && (flags & ERR_TXT_STRING)) { | |
| absl::StrAppend(&message, " - ", data); | |
| } | |
| } | |
| return absl::Status(code, message); | |
| } | |
| // For PSS signatures, verify the parameters and apply them to 'pkey_ctx'. | |
| // - 'certificate' is needed for the extra checks needed when a PSS | |
| // certificate is used. | |
| // - 'md' is the message digest algorithm used on the content (or signed | |
| // attributes), it will be used to initialize the masking function. | |
| absl::Status SetPssParameters(const SignerInfo& signer, const X509& certificate, | |
| int content_digest_nid, const EVP_MD* md, | |
| EVP_PKEY_CTX* pkey_ctx) { | |
| // See https://tools.ietf.org/html/rfc4056 for the checks performed. | |
| const unsigned char* p = | |
| CBS_data(signer.signature_algorithm.parameter.cbs_ptr()); | |
| if (p == nullptr || | |
| CBS_len(signer.signature_algorithm.parameter.cbs_ptr()) == 0) { | |
| return OpenSslError(absl::StatusCode::kInvalidArgument, | |
| "Missing PSS parameters"); | |
| } | |
| bssl::UniquePtr<RSA_PSS_PARAMS> pss_params(d2i_RSA_PSS_PARAMS( | |
| nullptr, &p, CBS_len(&(signer.signature_algorithm.parameter).cbs()))); | |
| if (pss_params == nullptr) { | |
| // http://tools.ietf.org/html/rfc4056#section-2.2 | |
| return OpenSslError(absl::StatusCode::kInvalidArgument, | |
| "Missing PSS parameters"); | |
| } | |
| if (pss_params->trailerField && | |
| ASN1_INTEGER_get(pss_params->trailerField) != 1) { | |
| return OpenSslError(absl::StatusCode::kUnimplemented, | |
| "Unsupported PSS trailer value."); | |
| } | |
| if (X509_get_signature_nid(&certificate) == NID_rsassaPss) { | |
| return OpenSslError( | |
| absl::StatusCode::kUnimplemented, | |
| "PSS signatures are not supported with RSASSA-PSS certificates."); | |
| } | |
| if (EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, RSA_PKCS1_PSS_PADDING) != 1) { | |
| return OpenSslError(absl::StatusCode::kInternal, | |
| "Failed to set PSS padding"); | |
| } | |
| if (pss_params->hashAlgorithm) { | |
| // http://tools.ietf.org/html/rfc4056#section-3 | |
| const ASN1_OBJECT* algorithm; | |
| X509_ALGOR_get0(&algorithm, /*out_param_type=*/nullptr, | |
| /*out_param_value=*/nullptr, pss_params->hashAlgorithm); | |
| if (OBJ_obj2nid(algorithm) != content_digest_nid) { | |
| return absl::Status( | |
| absl::StatusCode::kInvalidArgument, | |
| "The PSS Hash algorithm doesn't match the content hash algorithm."); | |
| } | |
| } | |
| if (pss_params->maskGenAlgorithm) { | |
| const ASN1_OBJECT* algorithm; | |
| X509_ALGOR_get0(&algorithm, /*out_param_type=*/nullptr, | |
| /*out_param_value=*/nullptr, pss_params->maskGenAlgorithm); | |
| if (OBJ_obj2nid(algorithm) != NID_mgf1) { | |
| return OpenSslError(absl::StatusCode::kUnimplemented, | |
| "Unsupported PSS mask generation algorithm."); | |
| } | |
| } | |
| if (EVP_PKEY_CTX_set_rsa_mgf1_md(pkey_ctx, md) != 1) { | |
| return OpenSslError(absl::StatusCode::kInvalidArgument, | |
| "Failed to set the PSS MGF1 hash"); | |
| } | |
| int salt_length = 20; | |
| if (pss_params->saltLength != nullptr) { | |
| salt_length = ASN1_INTEGER_get(pss_params->saltLength); | |
| } | |
| if (EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx, salt_length) != 1) { | |
| return OpenSslError(absl::StatusCode::kInvalidArgument, | |
| "Invalid PSS parameters"); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| // Check that the hash of the 'contents' matches the digest present in the | |
| // authenticated attributes. | |
| absl::Status VerifySignedAttributesHash(const SignerInfo& signer, | |
| const std::vector<ByteString>& contents, | |
| int digest_nid) { | |
| const EVP_MD* md = EVP_get_digestbynid(digest_nid); | |
| if (md == nullptr) { | |
| return OpenSslError(absl::StatusCode::kUnimplemented, | |
| "Failed to get the content hashing EVP_MD object."); | |
| } | |
| // When the authenticated attributes are present the signature is computed | |
| // over them so we need two checks: | |
| // 1) That the digest of the contents matches the attribute's | |
| // message_digest. | |
| // 2) That the signature of the authenticated attributes is valid. | |
| bssl::ScopedEVP_MD_CTX md_context; | |
| if (!EVP_DigestInit_ex(md_context.get(), md, nullptr)) { | |
| return OpenSslError(absl::StatusCode::kInternal, | |
| "EVP_DigestInit_ex failed."); | |
| } | |
| for (const auto& chunk : contents) { | |
| if (!EVP_DigestUpdate(md_context.get(), CBS_data(&(chunk).cbs()), | |
| CBS_len(chunk.cbs_ptr()))) { | |
| return OpenSslError(absl::StatusCode::kInternal, | |
| "EVP_DigestUpdate failed."); | |
| } | |
| } | |
| uint8_t md_value[EVP_MAX_MD_SIZE]; | |
| unsigned md_len; | |
| if (!EVP_DigestFinal_ex(md_context.get(), md_value, &md_len)) { | |
| return OpenSslError(absl::StatusCode::kInternal, | |
| "EVP_DigestFinal_ex failed."); | |
| } | |
| if (CBS_len(&(signer.message_digest).cbs()) != md_len || | |
| CRYPTO_memcmp(md_value, CBS_data(&(signer.message_digest).cbs()), | |
| CBS_len(&(signer.message_digest).cbs())) != 0) { | |
| return OpenSslError(absl::StatusCode::kInvalidArgument, | |
| "Hash attribute mismatch"); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| } // namespace | |
| absl::StatusOr<SignatureInfo> VerifySignature( | |
| const Content& cms_content, const SignerInfo& signer, | |
| const std::vector<ByteString>& contents, const X509& certificate) { | |
| ERR_clear_error(); // Clear OpenSSL's error queue for this thread. | |
| // Signed attributes checks, see | |
| // https://tools.ietf.org/html/rfc5652#section-5.6 | |
| if (!CompareOid(cms_content.content_type.cbs(), kDataOid, sizeof(kDataOid)) && | |
| CBS_len(signer.raw_signed_attributes.cbs_ptr()) == 0) { | |
| return absl::Status(absl::StatusCode::kInvalidArgument, | |
| "The signed attributes are needed when the " | |
| "encapsultated content type is not id-data."); | |
| } | |
| if (CBS_len(signer.raw_signed_attributes.cbs_ptr()) > 0) { | |
| if (CBS_len(signer.content_type_signed.cbs_ptr()) == 0 || | |
| !CompareOid(signer.content_type_signed.cbs(), | |
| cms_content.content_type.cbs())) { | |
| return absl::Status(absl::StatusCode::kInvalidArgument, | |
| "Mismatch between the signed attributes content type " | |
| "and the encapsultated content type."); | |
| } | |
| if (CBS_len(signer.message_digest.cbs_ptr()) == 0) { | |
| return absl::Status(absl::StatusCode::kInvalidArgument, | |
| "Missing message digest in the signed attributes."); | |
| } | |
| } | |
| bssl::UniquePtr<EVP_PKEY> key( | |
| X509_get_pubkey(const_cast<X509*>(&certificate))); | |
| if (key == nullptr) { | |
| return OpenSslError(absl::StatusCode::kInvalidArgument, | |
| "Failed to get the public key from the certificate."); | |
| } | |
| int digest_nid = OBJ_cbs2nid(&signer.digest_algorithm.algorithm_oid.cbs()); | |
| if (digest_nid == NID_undef) { | |
| return OpenSslError(absl::StatusCode::kUnimplemented, | |
| "Unknown digest algorithm."); | |
| } | |
| int signature_algorithm_nid = | |
| OBJ_cbs2nid(signer.signature_algorithm.algorithm_oid.cbs_ptr()); | |
| // Get the digest NID from the signature NID if possible, otherwise use the | |
| // signer's digest algorithm. | |
| int signature_digest_nid = NID_undef; | |
| if (OBJ_find_sigid_algs(signature_algorithm_nid, &signature_digest_nid, | |
| nullptr /* pkey nid */) != 1 || | |
| signature_digest_nid == NID_undef) { | |
| signature_digest_nid = digest_nid; | |
| } | |
| // Check that the signature algorithm is supported. | |
| switch (signature_algorithm_nid) { | |
| case NID_rsassaPss: | |
| case NID_rsaEncryption: | |
| case NID_sha224WithRSAEncryption: | |
| case NID_sha256WithRSAEncryption: | |
| case NID_sha384WithRSAEncryption: | |
| case NID_sha512WithRSAEncryption: | |
| case NID_X9_62_id_ecPublicKey: | |
| break; | |
| // ECDSA support is defined in RFC 5753 and includes instructions for | |
| // additional verification that `digest_nid` must match the value derived | |
| // from `signature_algorithm_nid`. (See RFC 5753 section 2.1.1.) Note | |
| // that the RFC permits the use of SHA-1 for ECDSA signatures, but we do | |
| // not, as at the time of writing it had long been obsolete. | |
| case NID_ecdsa_with_SHA224: | |
| case NID_ecdsa_with_SHA256: | |
| case NID_ecdsa_with_SHA384: | |
| case NID_ecdsa_with_SHA512: | |
| if (digest_nid != signature_digest_nid) { | |
| return absl::InvalidArgumentError(absl::StrFormat( | |
| R"(The digest algorithm does not match the value derived from the signature algorithm: digest_nid=%d, signature_digest_nid=%d)", | |
| digest_nid, signature_digest_nid)); | |
| } | |
| break; | |
| default: | |
| return OpenSslError( | |
| absl::StatusCode::kUnimplemented, | |
| absl::StrCat("Signature algorithm not implemented, NID: ", | |
| signature_algorithm_nid)); | |
| } | |
| const EVP_MD* signature_md = EVP_get_digestbynid(signature_digest_nid); | |
| if (signature_md == nullptr) { | |
| return OpenSslError(absl::StatusCode::kUnimplemented, | |
| "Failed to get the signature EVP_MD object."); | |
| } | |
| bssl::ScopedEVP_MD_CTX md_context; | |
| EVP_PKEY_CTX* pkey_ctx = nullptr; | |
| if (EVP_DigestVerifyInit(md_context.get(), &pkey_ctx, signature_md, | |
| nullptr /* engine */, key.get()) != 1) { | |
| return OpenSslError(absl::StatusCode::kInternal, | |
| "EVP_DigestVerifyInit failed."); | |
| } | |
| if (signature_algorithm_nid == NID_rsassaPss) { | |
| auto status = SetPssParameters(signer, certificate, digest_nid, | |
| signature_md, pkey_ctx); | |
| if (!status.ok()) { | |
| return status; | |
| } | |
| } | |
| if (CBS_len(signer.raw_signed_attributes.cbs_ptr()) > 0) { | |
| auto status = VerifySignedAttributesHash(signer, contents, digest_nid); | |
| if (!status.ok()) { | |
| return status; | |
| } | |
| // The signature is computed after replacing the implicit tag. | |
| // https://tools.ietf.org/html/rfc2315#section-9.3 | |
| uint8_t tag = 0x31; // SET | |
| if (EVP_DigestVerifyUpdate(md_context.get(), &tag, 1) != 1 || | |
| EVP_DigestVerifyUpdate( | |
| md_context.get(), | |
| reinterpret_cast<const char*>( | |
| CBS_data(signer.raw_signed_attributes.cbs_ptr()) + 1), | |
| CBS_len(signer.raw_signed_attributes.cbs_ptr()) - 1) != 1) { | |
| return OpenSslError(absl::StatusCode::kInternal, | |
| "EVP_DigestVerifyUpdate failed."); | |
| } | |
| } else { | |
| // When there are no authenticated attributes the signature is computed on | |
| // the contents. | |
| for (const auto& chunk : contents) { | |
| if (EVP_DigestVerifyUpdate(md_context.get(), CBS_data(chunk.cbs_ptr()), | |
| CBS_len(chunk.cbs_ptr())) != 1) { | |
| return OpenSslError(absl::StatusCode::kInternal, | |
| "EVP_DigestVerifyUpdate failed."); | |
| } | |
| } | |
| } | |
| if (EVP_DigestVerifyFinal(md_context.get(), | |
| const_cast<unsigned char*>( | |
| CBS_data(signer.signature_value.cbs_ptr())), | |
| CBS_len(signer.signature_value.cbs_ptr())) != 1) { | |
| return OpenSslError(absl::StatusCode::kInvalidArgument, | |
| "Invalid signature"); | |
| } | |
| SignatureInfo signature_info; | |
| signature_info.digest_algorithm_nid = digest_nid; | |
| signature_info.signature_algorithm_id = signature_algorithm_nid; | |
| return signature_info; | |
| } | |
| } // namespace credentio_cms |