emudhra CA Hack - Determining vulnerable Certificates
The below Script should be saved as a .rego file and uploaded to the
- Open the Aqua CSP platform and navigate to Workload Protection -> Policies -> Assurance Policies -> Custom Compliance Checks
- Select "Create New". Leave all fields blank and click "upload"
- Upload certificate, fields will auto-populate, hit Enter
- Click up one level to Assurance Policies, open an Image Assurance Policy, select the Custom compliance Checks control, and add the check in question. note: please ensure the Application scope includes the script in question
- Rescan the image in question, then navigate to Images & Functions -> Images -> expand the repository, click the image, and review the Scan Results at bottom!
Raw rego scripts provided below, as well as in downloadable format for convenience!
Image Assurance Detection
Detects according to filename and path, which may create false positives for secure emudhra certificates.
This is due to technology limitations that limit rego to the filenames, and not the contents themselves, so we do not anticipate a SHA-based solution will be made available for rego
Note: please save as a .rego file
package custom.aqua.selectors.emcert_paths
import future.keywords.if
import future.keywords.in
# METADATA
# title: eMudhra path check
# description: Enforce on certificate file paths containing eMudhra or emSign, without claiming certificate content verification
# custom:
# id: AQUA-CERT-PATH-001
# severity: MEDIUM
# input:
# selectors:
# - selectorId: cert_files
# type: file
# name: '.*[.](crt|pem|cer|der|CRT|PEM|CER|DER)$'
# path: ["/*"]
# Filename/path indicators only; does not inspect Subject/Issuer or certificates
# inside a bundle. Case-insensitive matches cover eMudhra, emSign and names such
# as emSign_ECC_Root_CA. Directory names also count, including emudhra-test.
# Renamed files and generic bundle names can evade these indicators.
# A layer digest is not a certificate fingerprint.
# Exactly one deny; no result is emitted when there are no matching paths.
path_findings[finding] {
resource := input.custom_resources[_]
detection := resource.detections[_]
detection.selector_id == "cert_files"
candidate_paths := {
object.get(resource, "path", ""),
object.get(detection, "path", ""),
}
path := candidate_paths[_]
is_string(path)
indicators := {"emudhra", "emsign"}
indicator := indicators[_]
contains(lower(path), indicator)
finding := {
"path": path,
"matched_indicator": indicator,
"layer_digest": object.get(resource, "layer_digest", ""),
"evidence_type": "filename_or_path_match",
}
}
deny[res] {
findings := sort(path_findings)
count(findings) > 0
matched_paths := {finding.path | finding := findings[_]}
lines := [
sprintf("MATCH: %s | indicator=%s | layer=%s", [finding.path, finding.matched_indicator, finding.layer_digest]) |
finding := findings[_]
]
metadata := object.get(input, "metadata", {})
res := {
"msg": "eMudhra or emSign certificate path found",
"severity": "medium",
"resolution": sprintf(
"ENFORCED: %d distinct certificate file paths matched eMudhra/emSign text (%d path/indicator/layer findings). Image tags: %v. Image ID: %s. MATCHED PATH EVIDENCE: %s. Review or remove these files as required by your policy. This finding is based on filenames/directory paths only; certificate identity, bundle contents, compromise and effective trust were not verified. No matches does not establish absence of affected certificates.",
[count(matched_paths), count(findings), object.get(metadata, "repo_tags", []), object.get(metadata, "id", ""), concat(" ; ", lines)],
),
"status": "enforce",
}
}Container Detection
Find emudhra in all active containers. Searched by SHA to prevent false positives!
Note: please save as .yaml file
---
controls: null
version: 1.0.0
id: emcert-docker
description: Running Docker container certificate evidence, including host, container and image identity;
shell/OpenSSL only
type: master
groups:
- id: '1'
description: Running Docker container certificate detection
checks:
- id: '1.1'
description: eMudhra certificate indicators detected in running Docker containers
audit: |
# Self-contained Aqua host audit: Docker client + host OpenSSL; no Python.
# Access must be to the local Docker daemon and the actual host's utilities.
AQUA_SCAN_HOST=$(hostname -f 2>/dev/null || hostname 2>/dev/null || uname -n 2>/dev/null)
if [ -z "$AQUA_SCAN_HOST" ]; then echo 'INCOMPLETE: cannot determine hostname'; exit 0; fi
export AQUA_SCAN_HOST
printf 'HOST=%s\n' "$AQUA_SCAN_HOST"
umask 077
workspace=$(mktemp -d /tmp/aqua-docker-cert-check.XXXXXXXX) || { echo 'INCOMPLETE: cannot create workspace'; exit 0; }
trap 'rm -rf "$workspace"' 0
trap 'exit 1' 1 2 15
cat > "$workspace/scanner.sh" <<'AQUA_DOCKER_SCANNER'
#!/bin/sh
# Read-only inspection of copied files; no commands are run inside containers.
# Dependencies are installed on the Docker client host only.
# Usage: sudo sh scan-running-container-certs.sh [all|affected|dn|roots] [container]
# Without a container argument, inspect the containers running at list time.
# Exit 0: no selected-mode matches/errors in inspected files; 1: matches;
# 2: incomplete coverage or an execution error. None of these proves TLS trust.
set -u
umask 077
mode=${1:-all}
case "$mode" in all) modes='dn affected roots' ;; dn|affected|roots) modes=$mode ;; *) echo 'Usage: sh scan-running-container-certs.sh [all|affected|dn|roots] [container]' >&2; exit 2 ;; esac
for tool in docker openssl find awk grep sed tr wc mktemp cat rm mkdir mv; do
command -v "$tool" >/dev/null 2>&1 || { echo "ERROR: missing host command: $tool" >&2; exit 2; }
done
root=$(mktemp -d /tmp/aqua-container-certs.XXXXXXXX) || exit 2
trap 'rm -rf "$root"' 0
trap 'exit 2' 1 2 15
if [ "$#" -ge 2 ]; then
if ! docker inspect --type container --format '{{.Id}}' "$2" > "$root/ids"; then exit 2; fi
else
if ! docker ps --no-trunc --quiet > "$root/ids"; then exit 2; fi
fi
if [ ! -s "$root/ids" ]; then echo 'No running containers found.'; exit 0; fi
cat > "$root/audit.sh" <<'AQUA_AUDIT'
# All detection logic and indicators are embedded in this YAML audit.
# No Python, installed helper, JSON source file or network access is required.
# Scan results describe discovered certificate files, not effective application trust.
# Supported: PEM/DER, PEM bundles and selected OpenSSL hash-named links.
# Excluded: opaque JKS/PKCS12/NSS stores, unknown filenames and inaccessible paths.
# OpenSSL validates X.509 parsing and calculates identities; it does not prove
# effective trust, certificate revocation enforcement or TLS exploitability.
for tool in openssl find awk grep sed tr wc mktemp cat rm; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "FAIL: missing required OS command: $tool"
exit 0
fi
done
work=$(mktemp -d /tmp/aqua-cert-audit.XXXXXXXX) || { echo "FAIL: cannot create audit workspace"; exit 0; }
trap 'rm -rf "$work"' 0
trap 'exit 1' 1 2 15
: > "$work/errors"
cat "$CONTAINER_COPY_ERRORS" >> "$work/errors"
: > "$work/files"
: > "$work/parsed"
: > "$work/matches"
: > "$work/evidence"
: > "$work/skipped"
cat > "$work/indicators" <<'AQUA_INDICATORS'
CERT|42C4ACCD12C4F59597925E1A08A7A90102608543809308D50EC427E118BA1493|EM DV TLS CA - G3B
CERT|45A883F349AEDFD5F3A649B5498FFB121712BA8B448F3F2A91DBC5A766FCDDBE|EM DV TLS CA - G3B
CERT|98B896625C0C230ADEEC3EB2CF6AAD5001ECF9156A68644E82374CE5833BC25B|EM OV TLS CA - G3B
CERT|5F1DED4DD590AF74631290CF073B122A2AAC6A1FA479778A0BD43579BD832699|EM OV TLS CA - G3B
CERT|E7568FE6E12331F4FD7DE61DA4C7559D0F8EB192A218D4741A3BA75184333838|EM EV TLS CA - G3B
CERT|579480AEA166290BD3C643589E795FC0D0E54E86505D180F1DFF99EFD3199512|EM EV TLS CA - G3B
CERT|0164D7CEAE5BB909C272AF151FF44B1177AE9B47D9424B94F1D9558C30934D19|EM DV TLS CA - G3C
CERT|62B7F9868263F72F6911FC78CEAF72C1979158B7368BF648683BB2AA39B02E72|EM DV TLS CA - G3C
CERT|CB6E15165AC802E9B3F993F16C25D41F4E81C21EBDE71AE1B012AC00BF9C7C3F|EM OV TLS CA - G3C
CERT|7485B51C0ED192878FA1D2F948FB529AE1842CC5086C4B67F7BE5B2039DE9489|EM OV TLS CA - G3C
CERT|432EC25F0D28DE9006D97BF752F17BBB2CD41F669933FCB072D9A1148D0261F3|EM EV TLS CA - G3C
CERT|FD9D5C2DBBD4995AB69B8D50C439D9527B00529199C2FD01B2174EB58F07021F|EM EV TLS CA - G3C
CERT|9E39DE2881273ACC225A149C72BA3E0F51424963669AB633EE1DC7DF5DB067B4|EM OV CS CA - G3B
CERT|A496E23AC0CAB8155FC9B117B97EAE368EFCBB9041AEC8804D5A308D2D01C2D8|EM OV CS CA - G3B
CERT|4B2460A466F95FD40113BCDCCA2CEB29B3B093921DE45474DA7DF48874C630B1|EM OV CS CA - G3C
CERT|84112972B8E59D14560AE7E7119654474AAD119990CF3722BFC1E3B91615E1FD|EM OV CS CA - G3C
KEY|EBED6475C2C2D80FCB199F3FE8DD6E6B3CE60B77EB15D4E42C3DEC0C6F5B5105|EM DV TLS CA - G3B
KEY|43BFFA19D6EF651451F6841E019931E96F6F2EAEFF28AB4B6F1CE4576DE2717B|EM OV TLS CA - G3B
KEY|E0254C7FCDC265E2F0F8A428DABB75AB4AE039B4872D62F5EE57F0877D5A8A41|EM EV TLS CA - G3B
KEY|49DD353DC2B919A3770645117D0E60C16115E7D1BC259EEFE734B00EA434FE5F|EM DV TLS CA - G3C
KEY|63ECEFEE431D49523BC1136C4E72D0206A477086CAF606CD5CF025686C109CF8|EM OV TLS CA - G3C
KEY|5C0660A2B164910D8FF589C18E14552E9E0EC237DCEFE2CB736D41A4811F81A0|EM EV TLS CA - G3C
KEY|EC4FFA1AD0AC88081314B7AAFFEF6A91F97CD57291BB5BA686C5415F5EB4F6B6|EM OV CS CA - G3B
KEY|92C063D67E8F14DB71D288E11266FFDAF69CBF115F7BAB14F3BC2F68D25E0E30|EM OV CS CA - G3C
ROOT|7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D|emSign Root TLS CA - G3
ROOT|F2EDAAFF3D9B701FB4D14C9F285F8C0BAD90FC3BCC1FA07D47F4718EDABA34A5|emSign Root CS CA - G3
ROOT|D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4|AAA Certificate Services
ROOT|86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B|emSign ECC Root CA - G3
AQUA_INDICATORS
cat > "$work/scan-files.sh" <<'AQUA_SCANNER'
work=$1
mode=$2
shift 2
for file do
display_path=${file#"$CONTAINER_CERT_ROOT"}
[ -f "$file" ] || continue
if [ ! -r "$file" ]; then
printf 'Unreadable file: %s\n' "$file" >> "$work/errors"
continue
fi
printf '1\n' >> "$work/files"
size=$(wc -c < "$file")
if [ "$size" -gt 33554432 ]; then
printf 'File exceeds 32 MiB inspection limit: %s\n' "$file" >> "$work/errors"
continue
fi
parts=$(mktemp -d "$work/parts.XXXXXXXX") || { echo "Cannot create certificate workspace" >> "$work/errors"; continue; }
# Extract every PEM certificate, including OpenSSL TRUSTED CERTIFICATE blocks.
if ! awk -v dir="$parts" '
/^-----BEGIN (TRUSTED )?CERTIFICATE-----\r?$/ {
if (inside) bad=1
n++; dest=sprintf("%s/cert-%06d.pem",dir,n); inside=1
}
inside { print > dest }
/^-----END (TRUSTED )?CERTIFICATE-----\r?$/ {
if (inside) { close(dest); inside=0 } else bad=1
}
END { if (inside || bad) exit 2 }
' "$file"; then
printf 'Malformed or unreadable PEM: %s\n' "$file" >> "$work/errors"
fi
if [ ! -f "$parts/cert-000001.pem" ]; then
if grep -q '^-----BEGIN .*-----' "$file"; then
# Known non-certificate PEM files are outside certificate inventory scope.
# Malformed CERTIFICATE PEMs must remain coverage errors.
if grep -Eq '^-----BEGIN (TRUSTED )?CERTIFICATE' "$file"; then
printf 'No complete certificate block: %s\n' "$file" >> "$work/errors"
elif grep -Eq '^-----BEGIN (.*PRIVATE KEY|PUBLIC KEY|CERTIFICATE REQUEST|NEW CERTIFICATE REQUEST|X509 CRL|DH PARAMETERS)-----' "$file"; then
printf 'Non-certificate PEM skipped: %s\n' "$file" >> "$work/skipped"
else
printf 'Unsupported PEM content: %s\n' "$file" >> "$work/errors"
fi
rm -rf "$parts"
continue
fi
if ! openssl x509 -inform DER -in "$file" -out "$parts/cert-000001.pem" 2>/dev/null; then
printf 'Cannot parse certificate as PEM or DER: %s\n' "$file" >> "$work/errors"
rm -rf "$parts"
continue
fi
fi
index=-1
for cert in "$parts"/cert-*.pem; do
index=$((index + 1))
if ! openssl x509 -in "$cert" -outform DER -out "$parts/certificate.der" 2>/dev/null; then
printf 'Invalid X.509 certificate: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
fingerprint=$(openssl dgst -sha256 "$parts/certificate.der" 2>/dev/null | awk '{print toupper($NF)}')
if ! printf '%s\n' "$fingerprint" | grep -Eq '^[0-9A-F]{64}$'; then
printf 'Certificate fingerprint calculation failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
printf '1\n' >> "$work/parsed"
name=
kind=
spki=
case "$mode" in
dn)
dn=$(openssl x509 -in "$cert" -noout -subject -issuer -nameopt RFC2253 2>/dev/null)
if printf '%s\n' "$dn" | grep -Eiq '(emudhra|emsign)'; then
name='eMudhra/emSign Subject or Issuer'
kind=DN_MATCH
fi
;;
affected)
name=$(awk -F '|' -v h="$fingerprint" '$1=="CERT" && $2==h {print $3;exit}' "$work/indicators")
if [ -n "$name" ]; then
kind=AFFECTED_CERT_SHA256
else
if ! openssl x509 -in "$cert" -pubkey -noout > "$parts/pubkey.pem" 2>/dev/null; then
printf 'Public key extraction failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
if ! openssl pkey -pubin -in "$parts/pubkey.pem" -outform DER -out "$parts/spki.der" 2>/dev/null; then
printf 'SPKI conversion failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
spki=$(openssl dgst -sha256 "$parts/spki.der" 2>/dev/null | awk '{print toupper($NF)}')
if ! printf '%s\n' "$spki" | grep -Eq '^[0-9A-F]{64}$'; then
printf 'SPKI fingerprint calculation failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
name=$(awk -F '|' -v h="$spki" '$1=="KEY" && $2==h {print $3;exit}' "$work/indicators")
[ -z "$name" ] || kind=AFFECTED_SPKI_SHA256
fi
;;
roots)
name=$(awk -F '|' -v h="$fingerprint" '$1=="ROOT" && $2==h {print $3;exit}' "$work/indicators")
[ -z "$name" ] || kind=ASSOCIATED_ROOT
;;
esac
if [ -n "$kind" ]; then
location_role=certificate_file
case "$file" in
*/blocklist/*|*/blacklist/*) location_role=distrust_directory_review ;;
esac
dn=$(openssl x509 -in "$cert" -noout -subject -issuer -nameopt RFC2253 2>/dev/null)
printf '1\n' >> "$work/matches"
{
printf ' host=%s\n container=%s\n container_id=%s\n image=%s\n image_id=%s\n' "$AQUA_SCAN_HOST" "$AQUA_CONTAINER_NAME" "$AQUA_CONTAINER_ID" "$AQUA_IMAGE_NAME" "$AQUA_IMAGE_ID"
printf 'MATCH type=%s name=%s\n' "$kind" "$name"
printf ' location_role=%s\n' "$location_role"
printf ' path=%s\n certificate_index=%s (zero-based)\n' "$display_path" "$index"
printf ' certificate_sha256=%s\n' "$fingerprint"
[ -z "$spki" ] || printf ' spki_sha256=%s\n' "$spki"
printf '%s\n' "$dn" | sed 's/^/ /'
} >> "$work/evidence"
fi
done
rm -rf "$parts"
done
AQUA_SCANNER
# Recursively inspect common Linux certificate roots; overlapping subfolders
# are covered by their parents. Absent OS/application directories are skipped.
# Java/NSS/PKCS12 stores require separate parsing and are not covered.
# Readable file symlinks are inspected; directory symlinks are not followed.
roots_scanned=0
echo 'SCAN_SCOPE: /etc/ssl /usr/local/share/ca-certificates /usr/share/ca-certificates /etc/pki /usr/share/pki /usr/local/ssl/certs /etc/letsencrypt (recursive)'
for directory in "$CONTAINER_CERT_ROOT/etc/ssl" "$CONTAINER_CERT_ROOT/usr/local/share/ca-certificates" "$CONTAINER_CERT_ROOT/usr/share/ca-certificates" "$CONTAINER_CERT_ROOT/etc/pki" "$CONTAINER_CERT_ROOT/usr/share/pki" "$CONTAINER_CERT_ROOT/usr/local/ssl/certs" "$CONTAINER_CERT_ROOT/etc/letsencrypt"; do
if [ ! -d "$directory" ]; then
echo "SCOPE_NOTE: directory absent: $directory"
continue
fi
roots_scanned=$((roots_scanned + 1))
if ! find "$directory" \( -type f -o -type l \) \
\( -iname '*.crt' -o -iname '*.pem' -o -iname '*.cer' -o -iname '*.der' \
-o -name ca-bundle -o -name ca-certificates \
-o -name '[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F].[0-9]*' \) \
-exec sh "$work/scan-files.sh" "$work" "$SCAN_MODE" {} + 2>> "$work/errors"; then
echo "Certificate-directory traversal or audit execution failed: $directory" >> "$work/errors"
fi
done
files=$(wc -l < "$work/files" | tr -d ' ')
parsed=$(wc -l < "$work/parsed" | tr -d ' ')
matches=$(wc -l < "$work/matches" | tr -d ' ')
skipped=$(wc -l < "$work/skipped" | tr -d ' ')
printf 'SUMMARY mode=%s directories=%s files=%s parsed_certificate_occurrences=%s matching_occurrences=%s skipped_noncertificate_PEM_files=%s\n' "$SCAN_MODE" "$roots_scanned" "$files" "$parsed" "$matches" "$skipped"
# Sanitize the reserved success token in untrusted DN/path text, so evidence
# cannot accidentally satisfy Aqua's output flag test.
sed 's/AQUA_CERT_CHECK_OK_7C89/[reserved-token]/g' "$work/evidence"
if [ -s "$work/errors" ]; then
echo 2 > "$CONTAINER_AUDIT_STATUS"
echo 'INCOMPLETE: copy/collection/parsing errors; coverage is incomplete'
sed 's/AQUA_CERT_CHECK_OK_7C89/[reserved-token]/g' "$work/errors"
elif [ "$parsed" -eq 0 ]; then
echo 2 > "$CONTAINER_AUDIT_STATUS"
echo 'INCOMPLETE: no certificates parsed; coverage is unconfirmed'
elif [ "$matches" -gt 0 ]; then
echo 1 > "$CONTAINER_AUDIT_STATUS"
echo 'MATCHES FOUND: matching certificate content; effective trust not assessed'
else
echo 0 > "$CONTAINER_AUDIT_STATUS"
echo 'NO MATCHES IN INSPECTED CERTIFICATE FILES: effective trust not assessed'
fi
exit 0
AQUA_AUDIT
cat > "$root/resolve-links.sh" <<'AQUA_LINKS'
stage=$1
container=$2
errors=$3
shift 3
for link do
rel=${link#"$stage"}
temp=$(mktemp -d "$stage/../link.XXXXXXXX") || { echo 'Could not create symlink resolution workspace' >> "$errors"; continue; }
if docker cp -L "$container:$rel" "$temp/resolved" > /dev/null 2> "$temp/error"; then
if [ -f "$temp/resolved" ] && [ ! -L "$temp/resolved" ]; then
rm -- "$link"
mv -- "$temp/resolved" "$link"
else
echo "Unresolved/non-file symlink: container=$container path=$rel (not followed in the host filesystem)" >> "$errors"
rm -- "$link"
fi
else
echo "Cannot resolve container symlink: container=$container path=$rel" >> "$errors"
cat "$temp/error" >> "$errors"
rm -- "$link"
fi
rm -rf "$temp"
done
AQUA_LINKS
any_matches=0
any_errors=0
while IFS= read -r id; do
name=$(docker inspect --type container --format '{{.Name}}' "$id" 2>/dev/null) || { echo "ERROR: cannot inspect $id"; any_errors=1; continue; }
image=$(docker inspect --type container --format '{{.Config.Image}}' "$id" 2>/dev/null) || { echo "ERROR: cannot inspect image for $id"; any_errors=1; continue; }
image_id=$(docker inspect --type container --format '{{.Image}}' "$id" 2>/dev/null) || { echo "ERROR: cannot inspect image ID for $id"; any_errors=1; continue; }
export AQUA_CONTAINER_NAME="$name" AQUA_CONTAINER_ID="$id" AQUA_IMAGE_NAME="$image" AQUA_IMAGE_ID="$image_id"
printf 'HOST=%s CONTAINER=%s CONTAINER_ID=%s IMAGE=%s IMAGE_ID=%s\n' "$AQUA_SCAN_HOST" "$name" "$id" "$image" "$image_id"
echo 'Source files can change during copying; this is not an atomic snapshot.'
echo 'Container mounts (only certificate paths within the configured scope are attempted):'
if ! docker inspect --type container --format '{{range .Mounts}}{{println .Type .Destination}}{{end}}' "$id"; then
echo 'INCOMPLETE: cannot inspect container mount configuration'
any_errors=1
fi
stage="$root/$id/rootfs"
mkdir -p "$stage" || { any_errors=1; continue; }
errors="$root/$id/copy-errors"
: > "$errors"
for directory in /etc/ssl /usr/local/share/ca-certificates /usr/share/ca-certificates /etc/pki /usr/share/pki /usr/local/ssl/certs /etc/letsencrypt; do
target="$stage$directory"
mkdir -p "$target" || { echo "Cannot create staging directory: $directory" >> "$errors"; continue; }
if docker cp -L "$id:$directory/." "$target/" > /dev/null 2> "$root/copy-error"; then
echo "COPIED $directory"
elif grep -Eiq 'could not find the file|no such file or directory|does not exist' "$root/copy-error"; then
# A failed recursive copy may have copied part of an existing source.
# Only treat missing paths as ordinary absence if no entry was copied.
if [ -n "$(find "$target" -mindepth 1 -print -quit)" ]; then
echo "Partial copy of $directory; coverage incomplete" >> "$errors"
cat "$root/copy-error" >> "$errors"
else
echo "SCOPE_NOTE: missing or unavailable path: $directory"
fi
else
echo "Copy failed: container=$id path=$directory" >> "$errors"
cat "$root/copy-error" >> "$errors"
fi
done
# Never interpret a copied absolute symlink against the HOST filesystem.
# Resolve each copied symlink using Docker's container-side path handling.
if ! find "$stage" -type l -exec sh "$root/resolve-links.sh" "$stage" "$id" "$errors" {} +; then
echo 'Symlink traversal/resolution failed' >> "$errors"
fi
for selected in $modes; do
status="$root/$id/status-$selected"
echo "--- CHECK $selected ---"
if ! CONTAINER_CERT_ROOT="$stage" CONTAINER_COPY_ERRORS="$errors" SCAN_MODE="$selected" CONTAINER_AUDIT_STATUS="$status" sh "$root/audit.sh"; then
echo 'INCOMPLETE: audit execution failed'
any_errors=1
continue
fi
if [ ! -s "$status" ]; then
echo 'INCOMPLETE: no audit status produced'
any_errors=1
continue
fi
case "$(cat "$status")" in 0) : ;; 1) any_matches=1 ;; *) any_errors=1 ;; esac
done
rm -rf "$root/$id"
done < "$root/ids"
if [ "$any_errors" -ne 0 ]; then echo 'OVERALL: incomplete coverage; review errors and any matches above'; exit 2; fi
if [ "$any_matches" -ne 0 ]; then echo 'OVERALL: selected certificate indicators found'; exit 1; fi
echo 'OVERALL: no selected indicators in inspected files; application trust not assessed'
exit 0
AQUA_DOCKER_SCANNER
# Capture all output so untrusted certificate names cannot inject the pass flag.
sh "$workspace/scanner.sh" all > "$workspace/result" 2>&1
scan_status=$?
sed 's/AQUA_DOCKER_CERT_OK_91B7/[reserved-token]/g' "$workspace/result"
case "$scan_status" in
0) echo 'AQUA_DOCKER_CERT_OK_91B7' ;;
1) echo 'FAIL: certificate indicators found; review match types and evidence above' ;;
*) echo 'FAIL: incomplete collection or inspection; review errors and findings above' ;;
esac
# Aqua evaluates the output flag, rather than relying on the shell exit code.
exit 0
tests:
test_items:
- flag: AQUA_DOCKER_CERT_OK_91B7
set: true
remediation: Review AFFECTED_CERT_SHA256 and AFFECTED_SPKI_SHA256 matches as known affected certificate/key
identities. DN_MATCH indicates name association only. ASSOCIATED_ROOT does not establish a compromised
root key or effective trust. Remove or replace affected material according to your PKI response
process, rebuild images where applicable, and review application trust. Resolve INCOMPLETE errors
before accepting coverage.
scored: true
Host Detection
please save as .yaml file
---
controls: null
version: 2.3.0
id: emcert-host-os
description: Certificate content evidence in common Linux certificate directories; shell/OpenSSL only;
directory role is reported, effective trust is not inferred
type: master
groups:
- id: '1'
description: Certificate detection findings and evidence
checks:
- id: '1.1'
description: eMudhra/emSign certificate DN association detected
audit: |
# All detection logic and indicators are embedded in this YAML audit.
# No Python, installed helper, JSON source file or network access is required.
# Scan results describe discovered certificate files, not effective application trust.
# Supported: PEM/DER, PEM bundles and selected OpenSSL hash-named links.
# Excluded: opaque JKS/PKCS12/NSS stores, unknown filenames and inaccessible paths.
# OpenSSL validates X.509 parsing and calculates identities; it does not prove
# effective trust, certificate revocation enforcement or TLS exploitability.
for tool in openssl find awk grep sed tr wc mktemp cat rm; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "FAIL: missing required OS command: $tool"
exit 0
fi
done
work=$(mktemp -d /tmp/aqua-cert-audit.XXXXXXXX) || { echo "FAIL: cannot create audit workspace"; exit 0; }
trap 'rm -rf "$work"' 0
trap 'exit 1' 1 2 15
: > "$work/errors"
: > "$work/files"
: > "$work/parsed"
: > "$work/matches"
: > "$work/evidence"
: > "$work/skipped"
cat > "$work/indicators" <<'AQUA_INDICATORS'
CERT|42C4ACCD12C4F59597925E1A08A7A90102608543809308D50EC427E118BA1493|EM DV TLS CA - G3B
CERT|45A883F349AEDFD5F3A649B5498FFB121712BA8B448F3F2A91DBC5A766FCDDBE|EM DV TLS CA - G3B
CERT|98B896625C0C230ADEEC3EB2CF6AAD5001ECF9156A68644E82374CE5833BC25B|EM OV TLS CA - G3B
CERT|5F1DED4DD590AF74631290CF073B122A2AAC6A1FA479778A0BD43579BD832699|EM OV TLS CA - G3B
CERT|E7568FE6E12331F4FD7DE61DA4C7559D0F8EB192A218D4741A3BA75184333838|EM EV TLS CA - G3B
CERT|579480AEA166290BD3C643589E795FC0D0E54E86505D180F1DFF99EFD3199512|EM EV TLS CA - G3B
CERT|0164D7CEAE5BB909C272AF151FF44B1177AE9B47D9424B94F1D9558C30934D19|EM DV TLS CA - G3C
CERT|62B7F9868263F72F6911FC78CEAF72C1979158B7368BF648683BB2AA39B02E72|EM DV TLS CA - G3C
CERT|CB6E15165AC802E9B3F993F16C25D41F4E81C21EBDE71AE1B012AC00BF9C7C3F|EM OV TLS CA - G3C
CERT|7485B51C0ED192878FA1D2F948FB529AE1842CC5086C4B67F7BE5B2039DE9489|EM OV TLS CA - G3C
CERT|432EC25F0D28DE9006D97BF752F17BBB2CD41F669933FCB072D9A1148D0261F3|EM EV TLS CA - G3C
CERT|FD9D5C2DBBD4995AB69B8D50C439D9527B00529199C2FD01B2174EB58F07021F|EM EV TLS CA - G3C
CERT|9E39DE2881273ACC225A149C72BA3E0F51424963669AB633EE1DC7DF5DB067B4|EM OV CS CA - G3B
CERT|A496E23AC0CAB8155FC9B117B97EAE368EFCBB9041AEC8804D5A308D2D01C2D8|EM OV CS CA - G3B
CERT|4B2460A466F95FD40113BCDCCA2CEB29B3B093921DE45474DA7DF48874C630B1|EM OV CS CA - G3C
CERT|84112972B8E59D14560AE7E7119654474AAD119990CF3722BFC1E3B91615E1FD|EM OV CS CA - G3C
KEY|EBED6475C2C2D80FCB199F3FE8DD6E6B3CE60B77EB15D4E42C3DEC0C6F5B5105|EM DV TLS CA - G3B
KEY|43BFFA19D6EF651451F6841E019931E96F6F2EAEFF28AB4B6F1CE4576DE2717B|EM OV TLS CA - G3B
KEY|E0254C7FCDC265E2F0F8A428DABB75AB4AE039B4872D62F5EE57F0877D5A8A41|EM EV TLS CA - G3B
KEY|49DD353DC2B919A3770645117D0E60C16115E7D1BC259EEFE734B00EA434FE5F|EM DV TLS CA - G3C
KEY|63ECEFEE431D49523BC1136C4E72D0206A477086CAF606CD5CF025686C109CF8|EM OV TLS CA - G3C
KEY|5C0660A2B164910D8FF589C18E14552E9E0EC237DCEFE2CB736D41A4811F81A0|EM EV TLS CA - G3C
KEY|EC4FFA1AD0AC88081314B7AAFFEF6A91F97CD57291BB5BA686C5415F5EB4F6B6|EM OV CS CA - G3B
KEY|92C063D67E8F14DB71D288E11266FFDAF69CBF115F7BAB14F3BC2F68D25E0E30|EM OV CS CA - G3C
ROOT|7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D|emSign Root TLS CA - G3
ROOT|F2EDAAFF3D9B701FB4D14C9F285F8C0BAD90FC3BCC1FA07D47F4718EDABA34A5|emSign Root CS CA - G3
ROOT|D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4|AAA Certificate Services
ROOT|86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B|emSign ECC Root CA - G3
AQUA_INDICATORS
cat > "$work/scan-files.sh" <<'AQUA_SCANNER'
work=$1
mode=$2
shift 2
for file do
[ -f "$file" ] || continue
if [ ! -r "$file" ]; then
printf 'Unreadable file: %s\n' "$file" >> "$work/errors"
continue
fi
printf '1\n' >> "$work/files"
size=$(wc -c < "$file")
if [ "$size" -gt 33554432 ]; then
printf 'File exceeds 32 MiB inspection limit: %s\n' "$file" >> "$work/errors"
continue
fi
parts=$(mktemp -d "$work/parts.XXXXXXXX") || { echo "Cannot create certificate workspace" >> "$work/errors"; continue; }
# Extract every PEM certificate, including OpenSSL TRUSTED CERTIFICATE blocks.
if ! awk -v dir="$parts" '
/^-----BEGIN (TRUSTED )?CERTIFICATE-----\r?$/ {
if (inside) bad=1
n++; dest=sprintf("%s/cert-%06d.pem",dir,n); inside=1
}
inside { print > dest }
/^-----END (TRUSTED )?CERTIFICATE-----\r?$/ {
if (inside) { close(dest); inside=0 } else bad=1
}
END { if (inside || bad) exit 2 }
' "$file"; then
printf 'Malformed or unreadable PEM: %s\n' "$file" >> "$work/errors"
fi
if [ ! -f "$parts/cert-000001.pem" ]; then
if grep -q '^-----BEGIN .*-----' "$file"; then
# Known non-certificate PEM files are outside certificate inventory scope.
# Malformed CERTIFICATE PEMs must remain coverage errors.
if grep -Eq '^-----BEGIN (TRUSTED )?CERTIFICATE' "$file"; then
printf 'No complete certificate block: %s\n' "$file" >> "$work/errors"
elif grep -Eq '^-----BEGIN (.*PRIVATE KEY|PUBLIC KEY|CERTIFICATE REQUEST|NEW CERTIFICATE REQUEST|X509 CRL|DH PARAMETERS)-----' "$file"; then
printf 'Non-certificate PEM skipped: %s\n' "$file" >> "$work/skipped"
else
printf 'Unsupported PEM content: %s\n' "$file" >> "$work/errors"
fi
rm -rf "$parts"
continue
fi
if ! openssl x509 -inform DER -in "$file" -out "$parts/cert-000001.pem" 2>/dev/null; then
printf 'Cannot parse certificate as PEM or DER: %s\n' "$file" >> "$work/errors"
rm -rf "$parts"
continue
fi
fi
index=-1
for cert in "$parts"/cert-*.pem; do
index=$((index + 1))
if ! openssl x509 -in "$cert" -outform DER -out "$parts/certificate.der" 2>/dev/null; then
printf 'Invalid X.509 certificate: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
fingerprint=$(openssl dgst -sha256 "$parts/certificate.der" 2>/dev/null | awk '{print toupper($NF)}')
if ! printf '%s\n' "$fingerprint" | grep -Eq '^[0-9A-F]{64}$'; then
printf 'Certificate fingerprint calculation failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
printf '1\n' >> "$work/parsed"
name=
kind=
spki=
case "$mode" in
dn)
dn=$(openssl x509 -in "$cert" -noout -subject -issuer -nameopt RFC2253 2>/dev/null)
if printf '%s\n' "$dn" | grep -Eiq '(emudhra|emsign)'; then
name='eMudhra/emSign Subject or Issuer'
kind=DN_MATCH
fi
;;
affected)
name=$(awk -F '|' -v h="$fingerprint" '$1=="CERT" && $2==h {print $3;exit}' "$work/indicators")
if [ -n "$name" ]; then
kind=AFFECTED_CERT_SHA256
else
if ! openssl x509 -in "$cert" -pubkey -noout > "$parts/pubkey.pem" 2>/dev/null; then
printf 'Public key extraction failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
if ! openssl pkey -pubin -in "$parts/pubkey.pem" -outform DER -out "$parts/spki.der" 2>/dev/null; then
printf 'SPKI conversion failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
spki=$(openssl dgst -sha256 "$parts/spki.der" 2>/dev/null | awk '{print toupper($NF)}')
if ! printf '%s\n' "$spki" | grep -Eq '^[0-9A-F]{64}$'; then
printf 'SPKI fingerprint calculation failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
name=$(awk -F '|' -v h="$spki" '$1=="KEY" && $2==h {print $3;exit}' "$work/indicators")
[ -z "$name" ] || kind=AFFECTED_SPKI_SHA256
fi
;;
roots)
name=$(awk -F '|' -v h="$fingerprint" '$1=="ROOT" && $2==h {print $3;exit}' "$work/indicators")
[ -z "$name" ] || kind=ASSOCIATED_ROOT
;;
esac
if [ -n "$kind" ]; then
location_role=certificate_file
case "$file" in
*/blocklist/*|*/blacklist/*) location_role=distrust_directory_review ;;
esac
dn=$(openssl x509 -in "$cert" -noout -subject -issuer -nameopt RFC2253 2>/dev/null)
printf '1\n' >> "$work/matches"
{
printf 'MATCH type=%s name=%s\n' "$kind" "$name"
printf ' location_role=%s\n' "$location_role"
printf ' path=%s\n certificate_index=%s (zero-based)\n' "$file" "$index"
printf ' certificate_sha256=%s\n' "$fingerprint"
[ -z "$spki" ] || printf ' spki_sha256=%s\n' "$spki"
printf '%s\n' "$dn" | sed 's/^/ /'
} >> "$work/evidence"
fi
done
rm -rf "$parts"
done
AQUA_SCANNER
# Recursively inspect common Linux certificate roots; overlapping subfolders
# are covered by their parents. Absent OS/application directories are skipped.
# Java/NSS/PKCS12 stores require separate parsing and are not covered.
# Readable file symlinks are inspected; directory symlinks are not followed.
roots_scanned=0
echo 'SCAN_SCOPE: /etc/ssl /usr/local/share/ca-certificates /usr/share/ca-certificates /etc/pki /usr/share/pki /usr/local/ssl/certs /etc/letsencrypt (recursive)'
for directory in /etc/ssl /usr/local/share/ca-certificates /usr/share/ca-certificates /etc/pki /usr/share/pki /usr/local/ssl/certs /etc/letsencrypt; do
if [ ! -d "$directory" ]; then
echo "SCOPE_NOTE: directory absent: $directory"
continue
fi
roots_scanned=$((roots_scanned + 1))
if ! find "$directory" \( -type f -o -type l \) \
\( -iname '*.crt' -o -iname '*.pem' -o -iname '*.cer' -o -iname '*.der' \
-o -name ca-bundle -o -name ca-certificates \
-o -name '[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F].[0-9]*' \) \
-exec sh "$work/scan-files.sh" "$work" dn {} + 2>> "$work/errors"; then
echo "Certificate-directory traversal or audit execution failed: $directory" >> "$work/errors"
fi
done
files=$(wc -l < "$work/files" | tr -d ' ')
parsed=$(wc -l < "$work/parsed" | tr -d ' ')
matches=$(wc -l < "$work/matches" | tr -d ' ')
skipped=$(wc -l < "$work/skipped" | tr -d ' ')
printf 'SUMMARY mode=dn directories=%s files=%s parsed_certificate_occurrences=%s matching_occurrences=%s skipped_noncertificate_PEM_files=%s\n' "$roots_scanned" "$files" "$parsed" "$matches" "$skipped"
# Sanitize the reserved success token in untrusted DN/path text, so evidence
# cannot accidentally satisfy Aqua's output flag test.
sed 's/AQUA_CERT_CHECK_OK_7C89/[reserved-token]/g' "$work/evidence"
if [ -s "$work/errors" ]; then
echo 'FAIL: collection/parsing errors; coverage is incomplete'
sed 's/AQUA_CERT_CHECK_OK_7C89/[reserved-token]/g' "$work/errors"
elif [ "$parsed" -eq 0 ]; then
echo 'FAIL: no certificates parsed; coverage is unconfirmed'
elif [ "$matches" -gt 0 ]; then
echo 'FAIL: matching certificate content found; presence does not establish effective trust'
else
echo 'AQUA_CERT_CHECK_OK_7C89'
fi
exit 0
tests:
test_items:
- flag: AQUA_CERT_CHECK_OK_7C89
set: true
remediation: Review the Subject/Issuer, certificate path and bundle index. A DN match is association
evidence, not proof of compromise.
scored: true
- id: '1.2'
description: Compromised eMudhra intermediate certificate or public key detected
audit: |
# All detection logic and indicators are embedded in this YAML audit.
# No Python, installed helper, JSON source file or network access is required.
# Scan results describe discovered certificate files, not effective application trust.
# Supported: PEM/DER, PEM bundles and selected OpenSSL hash-named links.
# Excluded: opaque JKS/PKCS12/NSS stores, unknown filenames and inaccessible paths.
# OpenSSL validates X.509 parsing and calculates identities; it does not prove
# effective trust, certificate revocation enforcement or TLS exploitability.
for tool in openssl find awk grep sed tr wc mktemp cat rm; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "FAIL: missing required OS command: $tool"
exit 0
fi
done
work=$(mktemp -d /tmp/aqua-cert-audit.XXXXXXXX) || { echo "FAIL: cannot create audit workspace"; exit 0; }
trap 'rm -rf "$work"' 0
trap 'exit 1' 1 2 15
: > "$work/errors"
: > "$work/files"
: > "$work/parsed"
: > "$work/matches"
: > "$work/evidence"
: > "$work/skipped"
cat > "$work/indicators" <<'AQUA_INDICATORS'
CERT|42C4ACCD12C4F59597925E1A08A7A90102608543809308D50EC427E118BA1493|EM DV TLS CA - G3B
CERT|45A883F349AEDFD5F3A649B5498FFB121712BA8B448F3F2A91DBC5A766FCDDBE|EM DV TLS CA - G3B
CERT|98B896625C0C230ADEEC3EB2CF6AAD5001ECF9156A68644E82374CE5833BC25B|EM OV TLS CA - G3B
CERT|5F1DED4DD590AF74631290CF073B122A2AAC6A1FA479778A0BD43579BD832699|EM OV TLS CA - G3B
CERT|E7568FE6E12331F4FD7DE61DA4C7559D0F8EB192A218D4741A3BA75184333838|EM EV TLS CA - G3B
CERT|579480AEA166290BD3C643589E795FC0D0E54E86505D180F1DFF99EFD3199512|EM EV TLS CA - G3B
CERT|0164D7CEAE5BB909C272AF151FF44B1177AE9B47D9424B94F1D9558C30934D19|EM DV TLS CA - G3C
CERT|62B7F9868263F72F6911FC78CEAF72C1979158B7368BF648683BB2AA39B02E72|EM DV TLS CA - G3C
CERT|CB6E15165AC802E9B3F993F16C25D41F4E81C21EBDE71AE1B012AC00BF9C7C3F|EM OV TLS CA - G3C
CERT|7485B51C0ED192878FA1D2F948FB529AE1842CC5086C4B67F7BE5B2039DE9489|EM OV TLS CA - G3C
CERT|432EC25F0D28DE9006D97BF752F17BBB2CD41F669933FCB072D9A1148D0261F3|EM EV TLS CA - G3C
CERT|FD9D5C2DBBD4995AB69B8D50C439D9527B00529199C2FD01B2174EB58F07021F|EM EV TLS CA - G3C
CERT|9E39DE2881273ACC225A149C72BA3E0F51424963669AB633EE1DC7DF5DB067B4|EM OV CS CA - G3B
CERT|A496E23AC0CAB8155FC9B117B97EAE368EFCBB9041AEC8804D5A308D2D01C2D8|EM OV CS CA - G3B
CERT|4B2460A466F95FD40113BCDCCA2CEB29B3B093921DE45474DA7DF48874C630B1|EM OV CS CA - G3C
CERT|84112972B8E59D14560AE7E7119654474AAD119990CF3722BFC1E3B91615E1FD|EM OV CS CA - G3C
KEY|EBED6475C2C2D80FCB199F3FE8DD6E6B3CE60B77EB15D4E42C3DEC0C6F5B5105|EM DV TLS CA - G3B
KEY|43BFFA19D6EF651451F6841E019931E96F6F2EAEFF28AB4B6F1CE4576DE2717B|EM OV TLS CA - G3B
KEY|E0254C7FCDC265E2F0F8A428DABB75AB4AE039B4872D62F5EE57F0877D5A8A41|EM EV TLS CA - G3B
KEY|49DD353DC2B919A3770645117D0E60C16115E7D1BC259EEFE734B00EA434FE5F|EM DV TLS CA - G3C
KEY|63ECEFEE431D49523BC1136C4E72D0206A477086CAF606CD5CF025686C109CF8|EM OV TLS CA - G3C
KEY|5C0660A2B164910D8FF589C18E14552E9E0EC237DCEFE2CB736D41A4811F81A0|EM EV TLS CA - G3C
KEY|EC4FFA1AD0AC88081314B7AAFFEF6A91F97CD57291BB5BA686C5415F5EB4F6B6|EM OV CS CA - G3B
KEY|92C063D67E8F14DB71D288E11266FFDAF69CBF115F7BAB14F3BC2F68D25E0E30|EM OV CS CA - G3C
ROOT|7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D|emSign Root TLS CA - G3
ROOT|F2EDAAFF3D9B701FB4D14C9F285F8C0BAD90FC3BCC1FA07D47F4718EDABA34A5|emSign Root CS CA - G3
ROOT|D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4|AAA Certificate Services
ROOT|86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B|emSign ECC Root CA - G3
AQUA_INDICATORS
cat > "$work/scan-files.sh" <<'AQUA_SCANNER'
work=$1
mode=$2
shift 2
for file do
[ -f "$file" ] || continue
if [ ! -r "$file" ]; then
printf 'Unreadable file: %s\n' "$file" >> "$work/errors"
continue
fi
printf '1\n' >> "$work/files"
size=$(wc -c < "$file")
if [ "$size" -gt 33554432 ]; then
printf 'File exceeds 32 MiB inspection limit: %s\n' "$file" >> "$work/errors"
continue
fi
parts=$(mktemp -d "$work/parts.XXXXXXXX") || { echo "Cannot create certificate workspace" >> "$work/errors"; continue; }
# Extract every PEM certificate, including OpenSSL TRUSTED CERTIFICATE blocks.
if ! awk -v dir="$parts" '
/^-----BEGIN (TRUSTED )?CERTIFICATE-----\r?$/ {
if (inside) bad=1
n++; dest=sprintf("%s/cert-%06d.pem",dir,n); inside=1
}
inside { print > dest }
/^-----END (TRUSTED )?CERTIFICATE-----\r?$/ {
if (inside) { close(dest); inside=0 } else bad=1
}
END { if (inside || bad) exit 2 }
' "$file"; then
printf 'Malformed or unreadable PEM: %s\n' "$file" >> "$work/errors"
fi
if [ ! -f "$parts/cert-000001.pem" ]; then
if grep -q '^-----BEGIN .*-----' "$file"; then
# Known non-certificate PEM files are outside certificate inventory scope.
# Malformed CERTIFICATE PEMs must remain coverage errors.
if grep -Eq '^-----BEGIN (TRUSTED )?CERTIFICATE' "$file"; then
printf 'No complete certificate block: %s\n' "$file" >> "$work/errors"
elif grep -Eq '^-----BEGIN (.*PRIVATE KEY|PUBLIC KEY|CERTIFICATE REQUEST|NEW CERTIFICATE REQUEST|X509 CRL|DH PARAMETERS)-----' "$file"; then
printf 'Non-certificate PEM skipped: %s\n' "$file" >> "$work/skipped"
else
printf 'Unsupported PEM content: %s\n' "$file" >> "$work/errors"
fi
rm -rf "$parts"
continue
fi
if ! openssl x509 -inform DER -in "$file" -out "$parts/cert-000001.pem" 2>/dev/null; then
printf 'Cannot parse certificate as PEM or DER: %s\n' "$file" >> "$work/errors"
rm -rf "$parts"
continue
fi
fi
index=-1
for cert in "$parts"/cert-*.pem; do
index=$((index + 1))
if ! openssl x509 -in "$cert" -outform DER -out "$parts/certificate.der" 2>/dev/null; then
printf 'Invalid X.509 certificate: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
fingerprint=$(openssl dgst -sha256 "$parts/certificate.der" 2>/dev/null | awk '{print toupper($NF)}')
if ! printf '%s\n' "$fingerprint" | grep -Eq '^[0-9A-F]{64}$'; then
printf 'Certificate fingerprint calculation failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
printf '1\n' >> "$work/parsed"
name=
kind=
spki=
case "$mode" in
dn)
dn=$(openssl x509 -in "$cert" -noout -subject -issuer -nameopt RFC2253 2>/dev/null)
if printf '%s\n' "$dn" | grep -Eiq '(emudhra|emsign)'; then
name='eMudhra/emSign Subject or Issuer'
kind=DN_MATCH
fi
;;
affected)
name=$(awk -F '|' -v h="$fingerprint" '$1=="CERT" && $2==h {print $3;exit}' "$work/indicators")
if [ -n "$name" ]; then
kind=AFFECTED_CERT_SHA256
else
if ! openssl x509 -in "$cert" -pubkey -noout > "$parts/pubkey.pem" 2>/dev/null; then
printf 'Public key extraction failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
if ! openssl pkey -pubin -in "$parts/pubkey.pem" -outform DER -out "$parts/spki.der" 2>/dev/null; then
printf 'SPKI conversion failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
spki=$(openssl dgst -sha256 "$parts/spki.der" 2>/dev/null | awk '{print toupper($NF)}')
if ! printf '%s\n' "$spki" | grep -Eq '^[0-9A-F]{64}$'; then
printf 'SPKI fingerprint calculation failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
name=$(awk -F '|' -v h="$spki" '$1=="KEY" && $2==h {print $3;exit}' "$work/indicators")
[ -z "$name" ] || kind=AFFECTED_SPKI_SHA256
fi
;;
roots)
name=$(awk -F '|' -v h="$fingerprint" '$1=="ROOT" && $2==h {print $3;exit}' "$work/indicators")
[ -z "$name" ] || kind=ASSOCIATED_ROOT
;;
esac
if [ -n "$kind" ]; then
location_role=certificate_file
case "$file" in
*/blocklist/*|*/blacklist/*) location_role=distrust_directory_review ;;
esac
dn=$(openssl x509 -in "$cert" -noout -subject -issuer -nameopt RFC2253 2>/dev/null)
printf '1\n' >> "$work/matches"
{
printf 'MATCH type=%s name=%s\n' "$kind" "$name"
printf ' location_role=%s\n' "$location_role"
printf ' path=%s\n certificate_index=%s (zero-based)\n' "$file" "$index"
printf ' certificate_sha256=%s\n' "$fingerprint"
[ -z "$spki" ] || printf ' spki_sha256=%s\n' "$spki"
printf '%s\n' "$dn" | sed 's/^/ /'
} >> "$work/evidence"
fi
done
rm -rf "$parts"
done
AQUA_SCANNER
# Recursively inspect common Linux certificate roots; overlapping subfolders
# are covered by their parents. Absent OS/application directories are skipped.
# Java/NSS/PKCS12 stores require separate parsing and are not covered.
# Readable file symlinks are inspected; directory symlinks are not followed.
roots_scanned=0
echo 'SCAN_SCOPE: /etc/ssl /usr/local/share/ca-certificates /usr/share/ca-certificates /etc/pki /usr/share/pki /usr/local/ssl/certs /etc/letsencrypt (recursive)'
for directory in /etc/ssl /usr/local/share/ca-certificates /usr/share/ca-certificates /etc/pki /usr/share/pki /usr/local/ssl/certs /etc/letsencrypt; do
if [ ! -d "$directory" ]; then
echo "SCOPE_NOTE: directory absent: $directory"
continue
fi
roots_scanned=$((roots_scanned + 1))
if ! find "$directory" \( -type f -o -type l \) \
\( -iname '*.crt' -o -iname '*.pem' -o -iname '*.cer' -o -iname '*.der' \
-o -name ca-bundle -o -name ca-certificates \
-o -name '[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F].[0-9]*' \) \
-exec sh "$work/scan-files.sh" "$work" affected {} + 2>> "$work/errors"; then
echo "Certificate-directory traversal or audit execution failed: $directory" >> "$work/errors"
fi
done
files=$(wc -l < "$work/files" | tr -d ' ')
parsed=$(wc -l < "$work/parsed" | tr -d ' ')
matches=$(wc -l < "$work/matches" | tr -d ' ')
skipped=$(wc -l < "$work/skipped" | tr -d ' ')
printf 'SUMMARY mode=affected directories=%s files=%s parsed_certificate_occurrences=%s matching_occurrences=%s skipped_noncertificate_PEM_files=%s\n' "$roots_scanned" "$files" "$parsed" "$matches" "$skipped"
# Sanitize the reserved success token in untrusted DN/path text, so evidence
# cannot accidentally satisfy Aqua's output flag test.
sed 's/AQUA_CERT_CHECK_OK_7C89/[reserved-token]/g' "$work/evidence"
if [ -s "$work/errors" ]; then
echo 'FAIL: collection/parsing errors; coverage is incomplete'
sed 's/AQUA_CERT_CHECK_OK_7C89/[reserved-token]/g' "$work/errors"
elif [ "$parsed" -eq 0 ]; then
echo 'FAIL: no certificates parsed; coverage is unconfirmed'
elif [ "$matches" -gt 0 ]; then
echo 'FAIL: matching certificate content found; presence does not establish effective trust'
else
echo 'AQUA_CERT_CHECK_OK_7C89'
fi
exit 0
tests:
test_items:
- flag: AQUA_CERT_CHECK_OK_7C89
set: true
remediation: Review exact certificate/SPKI fingerprint matches. Apply relevant vendor trust-store/runtime
updates and verify actual chain rejection separately.
scored: true
- id: '1.3'
description: Associated root CA detected for affected eMudhra chains
audit: |
# All detection logic and indicators are embedded in this YAML audit.
# No Python, installed helper, JSON source file or network access is required.
# Scan results describe discovered certificate files, not effective application trust.
# Supported: PEM/DER, PEM bundles and selected OpenSSL hash-named links.
# Excluded: opaque JKS/PKCS12/NSS stores, unknown filenames and inaccessible paths.
# OpenSSL validates X.509 parsing and calculates identities; it does not prove
# effective trust, certificate revocation enforcement or TLS exploitability.
for tool in openssl find awk grep sed tr wc mktemp cat rm; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "FAIL: missing required OS command: $tool"
exit 0
fi
done
work=$(mktemp -d /tmp/aqua-cert-audit.XXXXXXXX) || { echo "FAIL: cannot create audit workspace"; exit 0; }
trap 'rm -rf "$work"' 0
trap 'exit 1' 1 2 15
: > "$work/errors"
: > "$work/files"
: > "$work/parsed"
: > "$work/matches"
: > "$work/evidence"
: > "$work/skipped"
cat > "$work/indicators" <<'AQUA_INDICATORS'
CERT|42C4ACCD12C4F59597925E1A08A7A90102608543809308D50EC427E118BA1493|EM DV TLS CA - G3B
CERT|45A883F349AEDFD5F3A649B5498FFB121712BA8B448F3F2A91DBC5A766FCDDBE|EM DV TLS CA - G3B
CERT|98B896625C0C230ADEEC3EB2CF6AAD5001ECF9156A68644E82374CE5833BC25B|EM OV TLS CA - G3B
CERT|5F1DED4DD590AF74631290CF073B122A2AAC6A1FA479778A0BD43579BD832699|EM OV TLS CA - G3B
CERT|E7568FE6E12331F4FD7DE61DA4C7559D0F8EB192A218D4741A3BA75184333838|EM EV TLS CA - G3B
CERT|579480AEA166290BD3C643589E795FC0D0E54E86505D180F1DFF99EFD3199512|EM EV TLS CA - G3B
CERT|0164D7CEAE5BB909C272AF151FF44B1177AE9B47D9424B94F1D9558C30934D19|EM DV TLS CA - G3C
CERT|62B7F9868263F72F6911FC78CEAF72C1979158B7368BF648683BB2AA39B02E72|EM DV TLS CA - G3C
CERT|CB6E15165AC802E9B3F993F16C25D41F4E81C21EBDE71AE1B012AC00BF9C7C3F|EM OV TLS CA - G3C
CERT|7485B51C0ED192878FA1D2F948FB529AE1842CC5086C4B67F7BE5B2039DE9489|EM OV TLS CA - G3C
CERT|432EC25F0D28DE9006D97BF752F17BBB2CD41F669933FCB072D9A1148D0261F3|EM EV TLS CA - G3C
CERT|FD9D5C2DBBD4995AB69B8D50C439D9527B00529199C2FD01B2174EB58F07021F|EM EV TLS CA - G3C
CERT|9E39DE2881273ACC225A149C72BA3E0F51424963669AB633EE1DC7DF5DB067B4|EM OV CS CA - G3B
CERT|A496E23AC0CAB8155FC9B117B97EAE368EFCBB9041AEC8804D5A308D2D01C2D8|EM OV CS CA - G3B
CERT|4B2460A466F95FD40113BCDCCA2CEB29B3B093921DE45474DA7DF48874C630B1|EM OV CS CA - G3C
CERT|84112972B8E59D14560AE7E7119654474AAD119990CF3722BFC1E3B91615E1FD|EM OV CS CA - G3C
KEY|EBED6475C2C2D80FCB199F3FE8DD6E6B3CE60B77EB15D4E42C3DEC0C6F5B5105|EM DV TLS CA - G3B
KEY|43BFFA19D6EF651451F6841E019931E96F6F2EAEFF28AB4B6F1CE4576DE2717B|EM OV TLS CA - G3B
KEY|E0254C7FCDC265E2F0F8A428DABB75AB4AE039B4872D62F5EE57F0877D5A8A41|EM EV TLS CA - G3B
KEY|49DD353DC2B919A3770645117D0E60C16115E7D1BC259EEFE734B00EA434FE5F|EM DV TLS CA - G3C
KEY|63ECEFEE431D49523BC1136C4E72D0206A477086CAF606CD5CF025686C109CF8|EM OV TLS CA - G3C
KEY|5C0660A2B164910D8FF589C18E14552E9E0EC237DCEFE2CB736D41A4811F81A0|EM EV TLS CA - G3C
KEY|EC4FFA1AD0AC88081314B7AAFFEF6A91F97CD57291BB5BA686C5415F5EB4F6B6|EM OV CS CA - G3B
KEY|92C063D67E8F14DB71D288E11266FFDAF69CBF115F7BAB14F3BC2F68D25E0E30|EM OV CS CA - G3C
ROOT|7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D|emSign Root TLS CA - G3
ROOT|F2EDAAFF3D9B701FB4D14C9F285F8C0BAD90FC3BCC1FA07D47F4718EDABA34A5|emSign Root CS CA - G3
ROOT|D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4|AAA Certificate Services
ROOT|86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B|emSign ECC Root CA - G3
AQUA_INDICATORS
cat > "$work/scan-files.sh" <<'AQUA_SCANNER'
work=$1
mode=$2
shift 2
for file do
[ -f "$file" ] || continue
if [ ! -r "$file" ]; then
printf 'Unreadable file: %s\n' "$file" >> "$work/errors"
continue
fi
printf '1\n' >> "$work/files"
size=$(wc -c < "$file")
if [ "$size" -gt 33554432 ]; then
printf 'File exceeds 32 MiB inspection limit: %s\n' "$file" >> "$work/errors"
continue
fi
parts=$(mktemp -d "$work/parts.XXXXXXXX") || { echo "Cannot create certificate workspace" >> "$work/errors"; continue; }
# Extract every PEM certificate, including OpenSSL TRUSTED CERTIFICATE blocks.
if ! awk -v dir="$parts" '
/^-----BEGIN (TRUSTED )?CERTIFICATE-----\r?$/ {
if (inside) bad=1
n++; dest=sprintf("%s/cert-%06d.pem",dir,n); inside=1
}
inside { print > dest }
/^-----END (TRUSTED )?CERTIFICATE-----\r?$/ {
if (inside) { close(dest); inside=0 } else bad=1
}
END { if (inside || bad) exit 2 }
' "$file"; then
printf 'Malformed or unreadable PEM: %s\n' "$file" >> "$work/errors"
fi
if [ ! -f "$parts/cert-000001.pem" ]; then
if grep -q '^-----BEGIN .*-----' "$file"; then
# Known non-certificate PEM files are outside certificate inventory scope.
# Malformed CERTIFICATE PEMs must remain coverage errors.
if grep -Eq '^-----BEGIN (TRUSTED )?CERTIFICATE' "$file"; then
printf 'No complete certificate block: %s\n' "$file" >> "$work/errors"
elif grep -Eq '^-----BEGIN (.*PRIVATE KEY|PUBLIC KEY|CERTIFICATE REQUEST|NEW CERTIFICATE REQUEST|X509 CRL|DH PARAMETERS)-----' "$file"; then
printf 'Non-certificate PEM skipped: %s\n' "$file" >> "$work/skipped"
else
printf 'Unsupported PEM content: %s\n' "$file" >> "$work/errors"
fi
rm -rf "$parts"
continue
fi
if ! openssl x509 -inform DER -in "$file" -out "$parts/cert-000001.pem" 2>/dev/null; then
printf 'Cannot parse certificate as PEM or DER: %s\n' "$file" >> "$work/errors"
rm -rf "$parts"
continue
fi
fi
index=-1
for cert in "$parts"/cert-*.pem; do
index=$((index + 1))
if ! openssl x509 -in "$cert" -outform DER -out "$parts/certificate.der" 2>/dev/null; then
printf 'Invalid X.509 certificate: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
fingerprint=$(openssl dgst -sha256 "$parts/certificate.der" 2>/dev/null | awk '{print toupper($NF)}')
if ! printf '%s\n' "$fingerprint" | grep -Eq '^[0-9A-F]{64}$'; then
printf 'Certificate fingerprint calculation failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
printf '1\n' >> "$work/parsed"
name=
kind=
spki=
case "$mode" in
dn)
dn=$(openssl x509 -in "$cert" -noout -subject -issuer -nameopt RFC2253 2>/dev/null)
if printf '%s\n' "$dn" | grep -Eiq '(emudhra|emsign)'; then
name='eMudhra/emSign Subject or Issuer'
kind=DN_MATCH
fi
;;
affected)
name=$(awk -F '|' -v h="$fingerprint" '$1=="CERT" && $2==h {print $3;exit}' "$work/indicators")
if [ -n "$name" ]; then
kind=AFFECTED_CERT_SHA256
else
if ! openssl x509 -in "$cert" -pubkey -noout > "$parts/pubkey.pem" 2>/dev/null; then
printf 'Public key extraction failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
if ! openssl pkey -pubin -in "$parts/pubkey.pem" -outform DER -out "$parts/spki.der" 2>/dev/null; then
printf 'SPKI conversion failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
spki=$(openssl dgst -sha256 "$parts/spki.der" 2>/dev/null | awk '{print toupper($NF)}')
if ! printf '%s\n' "$spki" | grep -Eq '^[0-9A-F]{64}$'; then
printf 'SPKI fingerprint calculation failed: %s index=%s\n' "$file" "$index" >> "$work/errors"
continue
fi
name=$(awk -F '|' -v h="$spki" '$1=="KEY" && $2==h {print $3;exit}' "$work/indicators")
[ -z "$name" ] || kind=AFFECTED_SPKI_SHA256
fi
;;
roots)
name=$(awk -F '|' -v h="$fingerprint" '$1=="ROOT" && $2==h {print $3;exit}' "$work/indicators")
[ -z "$name" ] || kind=ASSOCIATED_ROOT
;;
esac
if [ -n "$kind" ]; then
location_role=certificate_file
case "$file" in
*/blocklist/*|*/blacklist/*) location_role=distrust_directory_review ;;
esac
dn=$(openssl x509 -in "$cert" -noout -subject -issuer -nameopt RFC2253 2>/dev/null)
printf '1\n' >> "$work/matches"
{
printf 'MATCH type=%s name=%s\n' "$kind" "$name"
printf ' location_role=%s\n' "$location_role"
printf ' path=%s\n certificate_index=%s (zero-based)\n' "$file" "$index"
printf ' certificate_sha256=%s\n' "$fingerprint"
[ -z "$spki" ] || printf ' spki_sha256=%s\n' "$spki"
printf '%s\n' "$dn" | sed 's/^/ /'
} >> "$work/evidence"
fi
done
rm -rf "$parts"
done
AQUA_SCANNER
# Recursively inspect common Linux certificate roots; overlapping subfolders
# are covered by their parents. Absent OS/application directories are skipped.
# Java/NSS/PKCS12 stores require separate parsing and are not covered.
# Readable file symlinks are inspected; directory symlinks are not followed.
roots_scanned=0
echo 'SCAN_SCOPE: /etc/ssl /usr/local/share/ca-certificates /usr/share/ca-certificates /etc/pki /usr/share/pki /usr/local/ssl/certs /etc/letsencrypt (recursive)'
for directory in /etc/ssl /usr/local/share/ca-certificates /usr/share/ca-certificates /etc/pki /usr/share/pki /usr/local/ssl/certs /etc/letsencrypt; do
if [ ! -d "$directory" ]; then
echo "SCOPE_NOTE: directory absent: $directory"
continue
fi
roots_scanned=$((roots_scanned + 1))
if ! find "$directory" \( -type f -o -type l \) \
\( -iname '*.crt' -o -iname '*.pem' -o -iname '*.cer' -o -iname '*.der' \
-o -name ca-bundle -o -name ca-certificates \
-o -name '[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F].[0-9]*' \) \
-exec sh "$work/scan-files.sh" "$work" roots {} + 2>> "$work/errors"; then
echo "Certificate-directory traversal or audit execution failed: $directory" >> "$work/errors"
fi
done
files=$(wc -l < "$work/files" | tr -d ' ')
parsed=$(wc -l < "$work/parsed" | tr -d ' ')
matches=$(wc -l < "$work/matches" | tr -d ' ')
skipped=$(wc -l < "$work/skipped" | tr -d ' ')
printf 'SUMMARY mode=roots directories=%s files=%s parsed_certificate_occurrences=%s matching_occurrences=%s skipped_noncertificate_PEM_files=%s\n' "$roots_scanned" "$files" "$parsed" "$matches" "$skipped"
# Sanitize the reserved success token in untrusted DN/path text, so evidence
# cannot accidentally satisfy Aqua's output flag test.
sed 's/AQUA_CERT_CHECK_OK_7C89/[reserved-token]/g' "$work/evidence"
if [ -s "$work/errors" ]; then
echo 'FAIL: collection/parsing errors; coverage is incomplete'
sed 's/AQUA_CERT_CHECK_OK_7C89/[reserved-token]/g' "$work/errors"
elif [ "$parsed" -eq 0 ]; then
echo 'FAIL: no certificates parsed; coverage is unconfirmed'
elif [ "$matches" -gt 0 ]; then
echo 'FAIL: matching certificate content found; presence does not establish effective trust'
else
echo 'AQUA_CERT_CHECK_OK_7C89'
fi
exit 0
tests:
test_items:
- flag: AQUA_CERT_CHECK_OK_7C89
set: true
remediation: This flags related public roots, not compromised root private keys. Set scored false
if associated-root presence should be review-only.
scored: trueAdditional Resources
Did you find it helpful? Yes No
Send feedbackSorry we couldn't be helpful. Help us improve this article with your feedback.