| // 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/compliance_checker.h" |
| |
| #include "absl/base/nullability.h" |
| #include "absl/status/status.h" |
| #include "absl/status/status_macros.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_format.h" |
| #include "openssl/asn1.h" |
| #include "openssl/base.h" |
| #include "openssl/ec.h" |
| #include "openssl/ec_key.h" |
| #include "openssl/evp.h" |
| #include "openssl/nid.h" |
| #include "openssl/obj.h" |
| #include "openssl/rsa.h" |
| #include "openssl/x509.h" |
| |
| namespace credentio { |
| namespace { |
| |
| constexpr int kMinRsaModulusBits = 2048; |
| |
| absl::Status IsValidCurve(const EVP_PKEY* absl_nonnull evp_key) { |
| EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(evp_key); |
| if (ec_key == nullptr) { |
| // This should never happen. Return an internal error to indicate a bug. |
| return absl::InternalError("EVP key is not an EC key"); |
| } |
| const EC_GROUP* ec_group = EC_KEY_get0_group(ec_key); |
| if (ec_group == nullptr) { |
| // This should never happen. Return an internal error to indicate a bug. |
| return absl::InvalidArgumentError("EC key does not have an EC group"); |
| } |
| int curve_name = EC_GROUP_get_curve_name(ec_group); |
| switch (curve_name) { |
| case NID_X9_62_prime256v1: |
| case NID_secp384r1: |
| case NID_secp521r1: |
| return absl::OkStatus(); |
| default: |
| return absl::InvalidArgumentError(absl::StrCat( |
| "EC curve does not fulfill C2PA requirements: ", curve_name)); |
| } |
| } |
| |
| absl::Status IsValidModulus(const EVP_PKEY* absl_nonnull evp_key) { |
| RSA* rsa = EVP_PKEY_get0_RSA(evp_key); |
| if (rsa == nullptr) { |
| // This should never happen. Return an internal error to indicate a bug. |
| return absl::InternalError("EVP key is not an RSA key"); |
| } |
| if (unsigned int length = RSA_bits(rsa); length < kMinRsaModulusBits) { |
| return absl::InvalidArgumentError(absl::StrFormat( |
| "RSA modulus does not fulfill C2PA requirements: The modulus length n " |
| "= %d bits (minimum required is %d bits)", |
| length, kMinRsaModulusBits)); |
| } |
| return absl::OkStatus(); |
| } |
| |
| absl::Status IsValidTbsCertificate(const X509* absl_nonnull x509) { |
| const ASN1_BIT_STRING *issuer_uid, *subject_uid; |
| X509_get0_uids(x509, &issuer_uid, &subject_uid); |
| if (issuer_uid != nullptr || subject_uid != nullptr) { |
| return absl::InvalidArgumentError( |
| "Certificate contains issuerUniqueID or subjectUniqueID fields"); |
| } |
| return absl::OkStatus(); |
| } |
| |
| } // namespace |
| |
| // This method checks the requirements in |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_general_requirements. |
| absl::Status ComplianceChecker::IsValidC2paCertificate(const X509* x509, |
| bool is_leaf) { |
| if (x509 == nullptr) { |
| return absl::InvalidArgumentError("Certificate is null"); |
| } |
| |
| if (is_leaf) { |
| int bc_critical = -1; |
| bssl::UniquePtr<BASIC_CONSTRAINTS> basic_constraints( |
| static_cast<BASIC_CONSTRAINTS*>(X509_get_ext_d2i( |
| x509, NID_basic_constraints, &bc_critical, nullptr))); |
| if (basic_constraints == nullptr) { |
| if (bc_critical != -1) { |
| return absl::InvalidArgumentError( |
| "Malformed or invalid Basic Constraints extension in the " |
| "end-entity certificate."); |
| } |
| } else if (basic_constraints->ca) { |
| return absl::InvalidArgumentError( |
| "The cA boolean flag in the Basic Constraints extension of the " |
| "end-entity certificate MUST be set to FALSE."); |
| } |
| |
| int ku_critical = -1; |
| bssl::UniquePtr<ASN1_BIT_STRING> key_usage(static_cast<ASN1_BIT_STRING*>( |
| X509_get_ext_d2i(x509, NID_key_usage, &ku_critical, nullptr))); |
| if (key_usage == nullptr) { |
| if (ku_critical != -1) { |
| return absl::InvalidArgumentError( |
| "Malformed or invalid Key Usage extension in the end-entity " |
| "certificate."); |
| } |
| } else if (ASN1_BIT_STRING_get_bit(key_usage.get(), 5) != 0) { |
| return absl::InvalidArgumentError( |
| "The keyCertSign bit in the Key Usage extension of the " |
| "end-entity certificate MUST be set to FALSE."); |
| } |
| } |
| EVP_PKEY* pubkey = X509_get0_pubkey(x509); |
| if (pubkey == nullptr) { |
| return absl::InvalidArgumentError("Certificate does not have a public key"); |
| } |
| |
| // * The algorithm requirements in the spec must be satisfied. |
| // A subset of these checks are performed when COSE verifier is constructing |
| // the `CoseSign1` structure so we do not need to repeat them here. |
| switch (EVP_PKEY_id(pubkey)) { |
| case EVP_PKEY_EC: // id_ecPublicKey |
| // Quoting |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_general_requirements, |
| // if the `algorithm` field of the `algorithm` field of the certificate’s |
| // `subjectPublicKeyInfo` is `id-ecPublicKey`, the `parameters` field |
| // shall be one of the following named curves from RFC 5480, |
| // section 2.1.1.1: "prime256v1", "secp384r1", "secp521r1". |
| ABSL_RETURN_IF_ERROR(IsValidCurve(pubkey)); |
| break; |
| case EVP_PKEY_RSA: // rsaEncryption |
| case EVP_PKEY_RSA_PSS: // rsaPSS |
| // Quoting |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_general_requirements, |
| // if the `algorithm` field of the `algorithm` field of the certificate’s |
| // `subjectPublicKeyInfo` is `rsaEncryption` or `rsaPSS`, the modulus |
| // field of the parameters field shall have a length of at least 2048 |
| // bits. |
| ABSL_RETURN_IF_ERROR(IsValidModulus(pubkey)); |
| // BoringSSL does not support id-RSASSA-PSS key encoding, and |
| // `RSA_get0_pss_params` will always return nullptr. So we cannot perform |
| // the following checks required by the C2PA spec for RSA-PSS: |
| // * `hashAlgorithm` and `maskGenAlgorithm` are present in the PSS params |
| // and they have the same value. |
| // * The value is one of the following: `id-sha256`, id-sha385` or |
| // `id-sha512`. |
| // |
| // In the case of a malformed key where the `hashAlgorithm` and |
| // `maskGenAlgorithm` have different values, the signature verification |
| // will fail. `CoseSign1Verifier::Verify` will also return an error if the |
| // signing algorithm is RSA-PSS but the hash algorithm is not SHA-256, |
| // SHA-358 or SHA-512. So we do not need to check these two items here. |
| break; |
| } |
| |
| // * The version must be equal to v3. |
| if (X509_get_version(x509) != X509_VERSION_3) { |
| return absl::InvalidArgumentError("Certificate version is not v3"); |
| } |
| |
| // * The `issuerUniqueID` and `subjectUniqueID` optional fields of the |
| // `TBSCertificate` sequence shall not be present, as per RFC 5280, |
| // section 4.1.2.8. |
| ABSL_RETURN_IF_ERROR(IsValidTbsCertificate(x509)); |
| |
| return absl::OkStatus(); |
| } |
| |
| } // namespace credentio |