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