From 2560e8144f38e82e697996aac371373c1e4c9910 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:37:48 +0000 Subject: [PATCH 1/3] Initial plan From bc08c314bd5d1f6b3aeebfaeb392cf3940dc99eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:45:18 +0000 Subject: [PATCH 2/3] feat: add automated certificate deployment workflow to dist branch Co-authored-by: Creeper19472 <38857196+Creeper19472@users.noreply.github.com> --- .github/workflows/deploy.yml | 90 +++++++++++++++++++++++++ .gitignore | 6 +- README.md | 20 ++++++ 8a5a09f0.0 => certs/intermediate_ca.pem | 0 e16db44c.0 => certs/root_ca.pem | 0 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deploy.yml rename 8a5a09f0.0 => certs/intermediate_ca.pem (100%) rename e16db44c.0 => certs/root_ca.pem (100%) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..86465d8 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,90 @@ +name: Deploy Certificates to dist + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout main branch + uses: actions/checkout@v4 + + - name: Clone certtools + run: git clone https://github.com/cfms-dev/certtools.git /tmp/certtools + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install certtools dependencies + run: pip install cryptography + + - name: Validate certificates using certtools + run: | + # Use the cryptography library (certtools dependency) to validate every + # PEM file in certs/ before they are rehashed and published to dist. + python - <<'EOF' + import glob, sys + from cryptography import x509 + + errors = [] + for pem_file in sorted(glob.glob("certs/*.pem")): + with open(pem_file, "rb") as f: + try: + cert = x509.load_pem_x509_certificate(f.read()) + print(f"OK {pem_file}: {cert.subject.rfc4514_string()}") + except Exception as e: + errors.append(f"ERR {pem_file}: {e}") + + if errors: + for msg in errors: + print(msg, file=sys.stderr) + sys.exit(1) + EOF + + - name: Generate certificate hash symlinks + run: | + # Use OpenSSL rehash to create .[0-9] symlinks for certificates + # and .r[0-9] symlinks for CRLs in the certs/ directory + openssl rehash certs/ + + - name: Collect certificate artifacts + run: | + mkdir -p /tmp/dist + + # Copy hash-named cert files (.[0-9]) and CRL files (.r[0-9]), + # resolving any symlinks so the dist branch contains real file content. + find certs/ -maxdepth 1 \( -type f -o -type l \) \ + | grep -E '[0-9a-f]{8}\.(r?[0-9])$' \ + | while IFS= read -r f; do + cp -L "$f" /tmp/dist/ + done + + echo "Certificate artifacts to deploy:" + ls -la /tmp/dist/ + + - name: Deploy to dist branch + run: | + cd /tmp/dist + + git init + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add . + git commit -m "Deploy certificates from ${GITHUB_SHA::7}" + + git remote add origin \ + "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" + + # Force-push to dist as an orphan branch (no shared history with main) + git push origin HEAD:dist --force + diff --git a/.gitignore b/.gitignore index fabb4b2..f5c313c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -.manifest.json \ No newline at end of file +.manifest.json + +# OpenSSL rehash-generated symlinks (produced by the deploy workflow; not source files) +certs/[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].[0-9] +certs/[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].r[0-9] \ No newline at end of file diff --git a/README.md b/README.md index eb8ec87..a3bdb05 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ Additionally, clients can be configured to accept Certificate Revocation Lists (CRLs) in PEM format, provided they follow a specific naming format when joining this repository. +## Repository Structure + +``` +certs/ — Source PEM certificate files (root CA, intermediate CAs, CRLs) +.github/ + workflows/ + deploy.yml — Automated deployment workflow +``` + ## Naming Convention The `c_rehash` tool provided by OpenSSL can be used to quickly generate certificate @@ -22,6 +31,17 @@ not available on your platform, the same effect can be achieved manually. For more information, please refer to OpenSSL's naming conventions on this topic. +## Deployment + +A GitHub Actions workflow (`.github/workflows/deploy.yml`) automatically triggers on +every push to `main`. It uses `openssl rehash` to produce hash-named certificate +files (`.[0-9]` for certificates, `.r[0-9]` for CRLs) from the source +PEM files in `certs/`, then force-pushes those artifacts to the `dist` branch as an +orphan branch with no shared history from `main`. + +Clients and servers that reference this repository as a Git submodule should point +to the `dist` branch, which exposes only the certificate artifacts. + ## CONTRIBUTION For security reasons, this code repository is unlikely to accept certificate diff --git a/8a5a09f0.0 b/certs/intermediate_ca.pem similarity index 100% rename from 8a5a09f0.0 rename to certs/intermediate_ca.pem diff --git a/e16db44c.0 b/certs/root_ca.pem similarity index 100% rename from e16db44c.0 rename to certs/root_ca.pem From f0d2c5825577bf87b6d0a9f79225a936f4eaada5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:01:57 +0000 Subject: [PATCH 3/3] feat: add 21-day cadence and intermediate CRL generation Co-authored-by: Creeper19472 <38857196+Creeper19472@users.noreply.github.com> --- .github/workflows/deploy.yml | 106 ++++++++++++++++++++++++++++++++++- README.md | 7 ++- 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 86465d8..2002429 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,6 +4,8 @@ on: push: branches: - main + schedule: + - cron: '0 0 * * *' permissions: contents: write @@ -11,23 +13,47 @@ permissions: jobs: deploy: runs-on: ubuntu-latest + env: + CRL_INTERVAL_DAYS: '21' steps: + - name: Check 21-day cadence for scheduled runs + id: cadence + run: | + if [ "${{ github.event_name }}" != "schedule" ]; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + days_since_epoch=$(( $(date -u +%s) / 86400 )) + if [ $(( days_since_epoch % $CRL_INTERVAL_DAYS )) -eq 0 ]; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + echo "Scheduled run is on 21-day cadence." + else + echo "should_run=false" >> "$GITHUB_OUTPUT" + echo "Skipping scheduled run: not on 21-day cadence." + fi + - name: Checkout main branch + if: steps.cadence.outputs.should_run == 'true' uses: actions/checkout@v4 - name: Clone certtools + if: steps.cadence.outputs.should_run == 'true' run: git clone https://github.com/cfms-dev/certtools.git /tmp/certtools - name: Set up Python + if: steps.cadence.outputs.should_run == 'true' uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install certtools dependencies + if: steps.cadence.outputs.should_run == 'true' run: pip install cryptography - name: Validate certificates using certtools + if: steps.cadence.outputs.should_run == 'true' run: | # Use the cryptography library (certtools dependency) to validate every # PEM file in certs/ before they are rehashed and published to dist. @@ -50,13 +76,91 @@ jobs: sys.exit(1) EOF + - name: Generate CRL for intermediate CA + if: steps.cadence.outputs.should_run == 'true' + env: + INTERMEDIATE_CA_KEY_PEM: ${{ secrets.INTERMEDIATE_CA_KEY_PEM }} + run: | + python - <<'EOF' + import datetime + import os + import sys + from cryptography import x509 + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding + from cryptography.hazmat.primitives import hashes, serialization + + crl_interval_days = int(os.getenv("CRL_INTERVAL_DAYS", "21")) + key_pem = os.getenv("INTERMEDIATE_CA_KEY_PEM", "").strip() + if not key_pem: + print( + "Missing secret INTERMEDIATE_CA_KEY_PEM for CRL generation.", + file=sys.stderr, + ) + sys.exit(1) + + with open("certs/intermediate_ca.pem", "rb") as f: + int_cert = x509.load_pem_x509_certificate(f.read()) + + is_self_signed = False + if int_cert.subject == int_cert.issuer: + pub = int_cert.public_key() + try: + if isinstance(pub, rsa.RSAPublicKey): + pub.verify( + int_cert.signature, + int_cert.tbs_certificate_bytes, + padding.PKCS1v15(), + int_cert.signature_hash_algorithm, + ) + elif isinstance(pub, ec.EllipticCurvePublicKey): + pub.verify( + int_cert.signature, + int_cert.tbs_certificate_bytes, + ec.ECDSA(int_cert.signature_hash_algorithm), + ) + else: + raise TypeError("Unsupported public key type for self-signed check.") + is_self_signed = True + except InvalidSignature: + is_self_signed = False + + if is_self_signed: + print( + "intermediate_ca.pem is self-signed (root CA). " + "CRL generation must exclude root CA.", + file=sys.stderr, + ) + sys.exit(1) + + int_key = serialization.load_pem_private_key( + key_pem.encode("utf-8"), password=None + ) + + now = datetime.datetime.now(datetime.timezone.utc) + crl = ( + x509.CertificateRevocationListBuilder() + .issuer_name(int_cert.subject) + .last_update(now) + .next_update(now + datetime.timedelta(days=crl_interval_days)) + .sign(private_key=int_key, algorithm=hashes.SHA256()) + ) + + with open("certs/intermediate_ca.crl.pem", "wb") as f: + f.write(crl.public_bytes(serialization.Encoding.PEM)) + + print("Generated certs/intermediate_ca.crl.pem") + EOF + - name: Generate certificate hash symlinks + if: steps.cadence.outputs.should_run == 'true' run: | # Use OpenSSL rehash to create .[0-9] symlinks for certificates # and .r[0-9] symlinks for CRLs in the certs/ directory openssl rehash certs/ - name: Collect certificate artifacts + if: steps.cadence.outputs.should_run == 'true' run: | mkdir -p /tmp/dist @@ -72,6 +176,7 @@ jobs: ls -la /tmp/dist/ - name: Deploy to dist branch + if: steps.cadence.outputs.should_run == 'true' run: | cd /tmp/dist @@ -87,4 +192,3 @@ jobs: # Force-push to dist as an orphan branch (no shared history with main) git push origin HEAD:dist --force - diff --git a/README.md b/README.md index a3bdb05..46bf67d 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,10 @@ For more information, please refer to OpenSSL's naming conventions on this topic ## Deployment A GitHub Actions workflow (`.github/workflows/deploy.yml`) automatically triggers on -every push to `main`. It uses `openssl rehash` to produce hash-named certificate +every push to `main` and on a daily schedule. Scheduled runs only proceed when the +workflow's 21-day cadence check passes, so certificate/CRL publishing occurs every +21 days. It validates source certificates, generates a CRL for the intermediate CA +(excluding the root CA), and uses `openssl rehash` to produce hash-named certificate files (`.[0-9]` for certificates, `.r[0-9]` for CRLs) from the source PEM files in `certs/`, then force-pushes those artifacts to the `dist` branch as an orphan branch with no shared history from `main`. @@ -46,4 +49,4 @@ to the `dist` branch, which exposes only the certificate artifacts. For security reasons, this code repository is unlikely to accept certificate addition requests from outside sources. For system administrators, maintaining -a proprietary certificate store is likely a better practice. \ No newline at end of file +a proprietary certificate store is likely a better practice.