| #!/bin/bash | |
| # 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. | |
| # | |
| # | |
| # Creates a private root CA and, issued by it, EC P-256 keys and certificates | |
| # for a C2PA claim signer and a Time Stamping Authority (TSA). The output is | |
| # intended for local testing of tools:asset_write and tools:c2pa_validate only. | |
| # | |
| # Usage: create_test_pki.sh [output_dir] | |
| # | |
| # output_dir defaults to ./test_pki and must not already contain keys. | |
| # | |
| # The certificates follow the C2PA certificate profile checked by the | |
| # validator: X.509 v3 with the digitalSignature key usage. The claim signer | |
| # certificate carries the C2PA claim signing EKU (1.3.6.1.4.1.62558.2.1) and no | |
| # anyExtendedKeyUsage. The TSA certificate carries a critical timeStamping EKU | |
| # as required by RFC 3161. | |
| # | |
| # Keep root_ca_key.pem private: anyone holding it can issue certificates that | |
| # validators configured with root_ca_cert.pem will trust. | |
| # | |
| # Requires OpenSSL 1.1.1 or later. | |
| set -euo pipefail | |
| readonly CURVE="P-256" | |
| readonly ROOT_CA_DAYS=3650 | |
| readonly LEAF_DAYS=365 | |
| readonly ORGANIZATION="Credentio Test" | |
| ####################################### | |
| # Writes the OpenSSL config with the certificate extensions for each profile. | |
| # Arguments: | |
| # Path of the config file to write. | |
| ####################################### | |
| write_openssl_config() { | |
| cat > "$1" <<'EOF' | |
| [ req ] | |
| distinguished_name = req_dn | |
| [ req_dn ] | |
| [ root_ca ] | |
| basicConstraints = critical, CA:TRUE | |
| keyUsage = critical, keyCertSign, cRLSign | |
| subjectKeyIdentifier = hash | |
| # C2PA claim signer: digitalSignature KU and the C2PA claim signing EKU. | |
| [ claim_signer ] | |
| basicConstraints = critical, CA:FALSE | |
| keyUsage = critical, digitalSignature | |
| extendedKeyUsage = 1.3.6.1.4.1.62558.2.1 | |
| subjectKeyIdentifier = hash | |
| authorityKeyIdentifier = keyid:always | |
| # RFC 3161 TSA: timeStamping must be the only EKU and marked critical. | |
| [ tsa ] | |
| basicConstraints = critical, CA:FALSE | |
| keyUsage = critical, digitalSignature | |
| extendedKeyUsage = critical, timeStamping | |
| subjectKeyIdentifier = hash | |
| authorityKeyIdentifier = keyid:always | |
| EOF | |
| } | |
| ####################################### | |
| # Generates an EC private key in PKCS#8 PEM format. | |
| # Arguments: | |
| # Output key path. | |
| ####################################### | |
| generate_key() { | |
| openssl genpkey -algorithm EC -pkeyopt "ec_paramgen_curve:${CURVE}" \ | |
| -out "$1" 2>/dev/null | |
| chmod 600 "$1" | |
| } | |
| ####################################### | |
| # Creates the self-signed root CA key and certificate. | |
| # Arguments: | |
| # Output directory. | |
| ####################################### | |
| create_root_ca() { | |
| local dir="$1" | |
| generate_key "${dir}/root_ca_key.pem" | |
| openssl req -x509 -new -key "${dir}/root_ca_key.pem" -sha256 \ | |
| -days "${ROOT_CA_DAYS}" \ | |
| -subj "/O=${ORGANIZATION}/CN=${ORGANIZATION} Root CA" \ | |
| -config "${dir}/openssl.cnf" -extensions root_ca \ | |
| -out "${dir}/root_ca_cert.pem" | |
| } | |
| ####################################### | |
| # Creates a key and a certificate issued by the root CA. | |
| # Arguments: | |
| # Output directory. | |
| # Name of the profile section in openssl.cnf; also the output file prefix. | |
| # Common name of the certificate subject. | |
| ####################################### | |
| issue_leaf() { | |
| local dir="$1" | |
| local profile="$2" | |
| local common_name="$3" | |
| generate_key "${dir}/${profile}_key.pem" | |
| openssl req -new -key "${dir}/${profile}_key.pem" \ | |
| -subj "/O=${ORGANIZATION}/CN=${common_name}" \ | |
| -config "${dir}/openssl.cnf" | | |
| openssl x509 -req -sha256 -days "${LEAF_DAYS}" \ | |
| -CA "${dir}/root_ca_cert.pem" -CAkey "${dir}/root_ca_key.pem" \ | |
| -set_serial "0x$(openssl rand -hex 16)" \ | |
| -extfile "${dir}/openssl.cnf" -extensions "${profile}" \ | |
| -out "${dir}/${profile}_cert.pem" 2>/dev/null | |
| } | |
| main() { | |
| if [[ $# -gt 1 ]]; then | |
| echo "Usage: $0 [output_dir]" >&2 | |
| exit 1 | |
| fi | |
| local dir="${1:-test_pki}" | |
| if ! command -v openssl >/dev/null; then | |
| echo "Error: openssl not found in PATH." >&2 | |
| exit 1 | |
| fi | |
| if compgen -G "${dir}/*_key.pem" >/dev/null; then | |
| echo "Error: ${dir} already contains keys; refusing to overwrite." >&2 | |
| exit 1 | |
| fi | |
| mkdir -p "${dir}" | |
| write_openssl_config "${dir}/openssl.cnf" | |
| create_root_ca "${dir}" | |
| issue_leaf "${dir}" claim_signer "${ORGANIZATION} Claim Signer" | |
| issue_leaf "${dir}" tsa "${ORGANIZATION} TSA" | |
| openssl verify -CAfile "${dir}/root_ca_cert.pem" \ | |
| "${dir}/claim_signer_cert.pem" "${dir}/tsa_cert.pem" >/dev/null | |
| local abs_dir | |
| abs_dir="$(cd "${dir}" && pwd)" | |
| cat <<EOF | |
| Created test PKI in ${abs_dir}: | |
| asset_write flags: | |
| --claim_signer_key=${abs_dir}/claim_signer_key.pem | |
| --claim_signer_cert=${abs_dir}/claim_signer_cert.pem | |
| --tsa_key=${abs_dir}/tsa_key.pem | |
| --tsa_cert=${abs_dir}/tsa_cert.pem | |
| c2pa_validate flags: | |
| --claim_signer_trust=${abs_dir}/root_ca_cert.pem | |
| --tsa_trust=${abs_dir}/root_ca_cert.pem | |
| These credentials are for local testing only. Keep root_ca_key.pem private. | |
| EOF | |
| } | |
| main "$@" |