Skip to content

Commit 5131717

Browse files
fix(be): Sectigo R46 CA remediation
1 parent 6ffdcf7 commit 5131717

5 files changed

Lines changed: 140 additions & 58 deletions

File tree

.github/workflows/.tools-deploy.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,12 @@ jobs:
2424
tag_fallback: latest
2525
triggers: ('legacydb/')
2626

27-
- name: Build (backend) with Sectigo PEM
27+
- name: Build (backend)
2828
uses: bcgov/action-builder-ghcr@cb2629351c87dd1c2130073e4ebb7233a9653a63 # v4.4.1
2929
with:
3030
package: backend
3131
tag_fallback: latest
3232
triggers: ('backend/')
33-
build_args: |
34-
--build-arg SECTIGO_R46_PEM=${{ secrets.SECTIGO_R46_PEM }}
3533

3634
pre-tools:
3735
name: Pre Deploy Tools

.github/workflows/pr-open.yml

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,7 @@ jobs:
3535
matrix:
3636
package: [backend, database, frontend, legacy, processor]
3737
steps:
38-
- name: Build (backend) with Sectigo PEM
39-
if: ${{ matrix.package == 'backend' }}
40-
uses: bcgov/action-builder-ghcr@cb2629351c87dd1c2130073e4ebb7233a9653a63 # v4.4.1
41-
with:
42-
package: backend
43-
tag_fallback: latest
44-
triggers: ('backend/')
45-
# Pass the PEM as a build-arg from GitHub Secrets; DO NOT commit the PEM file to repo
46-
build_args: |
47-
--build-arg SECTIGO_R46_PEM=${{ secrets.SECTIGO_R46_PEM }}
48-
49-
- name: Build (other packages)
50-
if: ${{ matrix.package != 'backend' }}
38+
- name: Build
5139
uses: bcgov/action-builder-ghcr@cb2629351c87dd1c2130073e4ebb7233a9653a63 # v4.4.1
5240
with:
5341
package: ${{ matrix.package }}

backend/Dockerfile

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,69 @@
11
### Builder
22
FROM ghcr.io/graalvm/native-image:22.3.3 AS build
33

4-
# --- SSL Root CA Refresh (Sectigo R46) ---
4+
# --- SSL Root CA Import (Sectigo Public Server Authentication Root R46) ---
55
# Canada Post rotated its certificate chain to a root (Sectigo Public Server Authentication Root R46)
66
# missing from the older JDK bundled with this GraalVM image. We need that root in the trust store
77
# BEFORE native-image build so the embedded trust store trusts Canada Post endpoints.
8-
# Policy: do NOT commit the PEM. Approaches supported:
9-
# 1. Provide the PEM via build arg SECTIGO_R46_PEM (recommended; store content in GitHub/OpenShift secret).
10-
# 2. Fallback: attempt network fetch from crt.sh (public CT log) if build arg not supplied.
11-
ARG SECTIGO_R46_PEM=""
8+
# Source of truth: the reviewed, checked-in public certificate below (see backend/certs/ for
9+
# provenance and rotation notes). This is public CA material, not a secret. The fingerprint is
10+
# pinned and verified before import; there is no network fetch (e.g. crt.sh) in this build path.
11+
ARG SECTIGO_R46_ALIAS="sectigo-r46-root"
12+
ARG SECTIGO_R46_SHA256="7B:B6:47:A6:2A:EE:AC:88:BF:25:7A:A5:22:D0:1F:FE:A3:95:E0:AB:45:C7:3F:93:F6:56:54:EC:38:F2:5A:06"
1213

13-
RUN set -e; \
14+
COPY certs/sectigo-r46-root.pem /tmp/sectigo-r46-root.pem
15+
16+
RUN set -eu; \
1417
CERT_FILE=/tmp/sectigo-r46-root.pem; \
15-
echo "--- Starting Sectigo CA Import ---"; \
16-
if [ -n "$SECTIGO_R46_PEM" ]; then \
17-
echo "Using provided SECTIGO_R46_PEM build argument."; \
18-
printf '%b' "$SECTIGO_R46_PEM" > "$CERT_FILE"; \
19-
else \
20-
echo "Attempting remote fetch of Sectigo R46 root..."; \
21-
curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" \
22-
--retry 5 \
23-
--retry-delay 5 \
24-
--retry-connrefused \
25-
-fsSL 'https://crt.sh/?d=4256644734' \
26-
-o "$CERT_FILE" \
27-
|| { echo "ERROR: Curl failed to connect to crt.sh."; exit 1; }; \
28-
\
29-
echo "Verifying downloaded file format..."; \
30-
grep -q "BEGIN CERTIFICATE" "$CERT_FILE" || { \
31-
echo "ERROR: Downloaded file is not a valid PEM. crt.sh likely returned an HTML error page. File content:"; \
32-
cat "$CERT_FILE"; \
33-
exit 1; \
34-
}; \
18+
ALIAS="$SECTIGO_R46_ALIAS"; \
19+
KEYSTORE="$JAVA_HOME/lib/security/cacerts"; \
20+
STOREPASS=changeit; \
21+
echo "--- Sectigo R46 CA import ---"; \
22+
[ -s "$CERT_FILE" ] || { echo "ERROR: $CERT_FILE is missing or empty. Ensure backend/certs/sectigo-r46-root.pem is committed and included in the build context."; exit 1; }; \
23+
grep -q "BEGIN CERTIFICATE" "$CERT_FILE" || { echo "ERROR: $CERT_FILE does not contain PEM certificate data."; exit 1; }; \
24+
[ -f "$KEYSTORE" ] || { echo "ERROR: keystore $KEYSTORE was not found in this image."; exit 1; }; \
25+
[ -w "$KEYSTORE" ] || { echo "ERROR: keystore $KEYSTORE is not writable."; exit 1; }; \
26+
echo "Validating certificate with keytool..."; \
27+
keytool -printcert -file "$CERT_FILE" > /tmp/sectigo-r46-root.info 2>/tmp/sectigo-r46-root.err \
28+
|| { echo "ERROR: certificate is malformed and failed to parse:"; cat /tmp/sectigo-r46-root.err; rm -f "$CERT_FILE" /tmp/sectigo-r46-root.info /tmp/sectigo-r46-root.err; exit 1; }; \
29+
ACTUAL_SHA256=$(grep -i "SHA256:" /tmp/sectigo-r46-root.info | head -1 | sed 's/.*SHA256: *//' | tr -d ' \t'); \
30+
[ -n "$ACTUAL_SHA256" ] || { echo "ERROR: could not extract a SHA-256 fingerprint from the certificate."; exit 1; }; \
31+
EXPECTED_NORM=$(echo "$SECTIGO_R46_SHA256" | tr '[:lower:]' '[:upper:]' | tr -d ':'); \
32+
ACTUAL_NORM=$(echo "$ACTUAL_SHA256" | tr '[:lower:]' '[:upper:]' | tr -d ':'); \
33+
if [ "$ACTUAL_NORM" != "$EXPECTED_NORM" ]; then \
34+
echo "ERROR: certificate fingerprint mismatch."; \
35+
echo " expected sha256: $SECTIGO_R46_SHA256"; \
36+
echo " actual sha256: $ACTUAL_SHA256"; \
37+
rm -f "$CERT_FILE" /tmp/sectigo-r46-root.info /tmp/sectigo-r46-root.err; \
38+
exit 1; \
3539
fi; \
36-
\
37-
echo "Checking if alias already exists in trust store..."; \
38-
if keytool -list -keystore "$JAVA_HOME/lib/security/cacerts" -storepass changeit -alias sectigo-r46-root >/dev/null 2>&1; then \
39-
echo "Alias sectigo-r46-root already exists, skipping import."; \
40+
echo "Fingerprint verified (sha256=$ACTUAL_SHA256)."; \
41+
if keytool -list -keystore "$KEYSTORE" -storepass "$STOREPASS" -alias "$ALIAS" >/dev/null 2>&1; then \
42+
EXISTING_SHA256=$(keytool -list -v -keystore "$KEYSTORE" -storepass "$STOREPASS" -alias "$ALIAS" 2>/dev/null \
43+
| grep -i "SHA256:" | head -1 | sed 's/.*SHA256: *//' | tr -d ' \t' | tr '[:lower:]' '[:upper:]' | tr -d ':'); \
44+
if [ "$EXISTING_SHA256" = "$EXPECTED_NORM" ]; then \
45+
echo "Alias $ALIAS already present in $KEYSTORE with matching fingerprint; skipping import."; \
46+
else \
47+
echo "ERROR: alias $ALIAS already exists in $KEYSTORE with a mismatched fingerprint (found $EXISTING_SHA256)."; \
48+
rm -f "$CERT_FILE" /tmp/sectigo-r46-root.info /tmp/sectigo-r46-root.err; \
49+
exit 1; \
50+
fi; \
4051
else \
41-
echo "Importing certificate..."; \
52+
echo "Importing certificate as alias $ALIAS..."; \
4253
keytool -importcert \
4354
-trustcacerts \
4455
-noprompt \
45-
-alias sectigo-r46-root \
56+
-alias "$ALIAS" \
4657
-file "$CERT_FILE" \
47-
-keystore "$JAVA_HOME/lib/security/cacerts" \
48-
-storepass changeit; \
58+
-keystore "$KEYSTORE" \
59+
-storepass "$STOREPASS"; \
4960
fi; \
50-
\
5161
echo "Verifying successful installation..."; \
52-
keytool -list \
53-
-keystore "$JAVA_HOME/lib/security/cacerts" \
54-
-storepass changeit \
55-
-alias sectigo-r46-root >/dev/null; \
62+
keytool -list -keystore "$KEYSTORE" -storepass "$STOREPASS" -alias "$ALIAS" >/dev/null 2>&1 \
63+
|| { echo "ERROR: post-import verification failed for alias $ALIAS."; exit 1; }; \
5664
\
57-
rm -f "$CERT_FILE"; \
58-
echo "--- Sectigo CA Import Successful ---"
65+
rm -f "$CERT_FILE" /tmp/sectigo-r46-root.info /tmp/sectigo-r46-root.err; \
66+
echo "--- Sectigo R46 CA import complete (alias=$ALIAS, sha256=$ACTUAL_SHA256) ---"
5967

6068
# Copy
6169
WORKDIR /app
@@ -81,4 +89,4 @@ HEALTHCHECK CMD curl -f http://localhost:8080/actuator/health | grep '"status":"
8189
ENV SPRING_PROFILES_ACTIVE=container
8290

8391
# Startup
84-
ENTRYPOINT ["/app/nr-forest-client-backend"]
92+
ENTRYPOINT ["/app/nr-forest-client-backend"]

backend/certs/sectigo-r46-root.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Sectigo Public Server Authentication Root R46
2+
3+
This directory contains a **public** root CA certificate, checked into source control
4+
per Option A of the Sectigo R46 CA build remediation plan. It is not a credential:
5+
it contains no private key and grants no access by itself. It is required so the
6+
GraalVM native-image build trusts Canada Post's TLS chain, which was rotated to this
7+
root.
8+
9+
## Certificate record
10+
11+
| Field | Value |
12+
|---|---|
13+
| File | `sectigo-r46-root.pem` |
14+
| Subject | `CN=Sectigo Public Server Authentication Root R46, O=Sectigo Limited, C=GB` |
15+
| Issuer | `CN=Sectigo Public Server Authentication Root R46, O=Sectigo Limited, C=GB` (self-signed root) |
16+
| Serial number | `75:8d:fd:8b:ae:7c:07:00:fa:a9:25:a7:e1:c7:ad:14` |
17+
| Not before | 2021-03-22T00:00:00Z |
18+
| Not after | 2046-03-21T23:59:59Z |
19+
| SHA-256 fingerprint (pinned) | `7B:B6:47:A6:2A:EE:AC:88:BF:25:7A:A5:22:D0:1F:FE:A3:95:E0:AB:45:C7:3F:93:F6:56:54:EC:38:F2:5A:06` |
20+
| Import alias in `cacerts` | `sectigo-r46-root` |
21+
| Type | Root CA (`BasicConstraints: CA=true`), no private key present |
22+
23+
The pinned fingerprint above is also encoded as the `SECTIGO_R46_SHA256` build
24+
argument default in [`backend/Dockerfile`](../Dockerfile) and is verified there
25+
before the certificate is imported. A build fails if the checked-in PEM does not
26+
hash to this value.
27+
28+
## Provenance
29+
30+
- Source: extracted from the Mozilla-derived CA bundle published at
31+
`https://curl.se/ca/cacert.pem` (curl's official CA bundle), which lists this
32+
certificate under the entry `Sectigo Public Server Authentication Root R46`.
33+
- Not sourced from `crt.sh` or any other transient/CI-time lookup.
34+
- Verified independently with `X509Certificate2` parsing (subject, issuer, serial,
35+
validity, SHA-256 fingerprint) before being checked in.
36+
37+
> **Action required before merge:** this record was captured and formatted by an
38+
> automated coding assistant, not by the certificate/security owner named in
39+
> Phase 0 of the remediation plan. Before merging, the platform/security owner
40+
> must independently re-verify the fingerprint above (e.g. with
41+
> `openssl x509 -in sectigo-r46-root.pem -noout -sha256 -fingerprint`) against an
42+
> authoritative Sectigo source, sign off, and record their name/date here.
43+
44+
## Rotation
45+
46+
If Canada Post or Sectigo rotates this root:
47+
48+
1. Obtain the new certificate from an authoritative source (Sectigo's certificate
49+
repository or a verified endpoint capture) — not from an implicit build-time
50+
fetch.
51+
2. Validate it locally (`openssl x509 -noout -subject -issuer -serial -dates
52+
-fingerprint -sha256`).
53+
3. Replace `sectigo-r46-root.pem` and update the `SECTIGO_R46_SHA256` default in
54+
`backend/Dockerfile` and the table above in the same pull request, so the
55+
certificate and its pinned fingerprint change atomically.
56+
4. Have the change reviewed as a security-sensitive dependency update.

backend/certs/sectigo-r46-root.pem

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf
3+
MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD
4+
Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw
5+
HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY
6+
MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp
7+
YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB
8+
AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa
9+
ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz
10+
SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf
11+
iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X
12+
ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3
13+
IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS
14+
VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE
15+
SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu
16+
+Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt
17+
8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L
18+
HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt
19+
zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P
20+
AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c
21+
mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ
22+
YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52
23+
gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA
24+
Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB
25+
JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX
26+
DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui
27+
TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5
28+
dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65
29+
LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp
30+
0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY
31+
QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL
32+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)