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