Skip to main content

Evidence Report Signing

Cryptographically sign evidence reports to prove they have not been tampered with after generation. Auditors can independently verify the signature using the public key.

Why Sign Evidence Reports?

Unsigned evidence is an assertion. Signed evidence is proof.

Without signingWith signing
Auditor must trust the operator produced the reportAuditor can verify the report independently
Reports can be modified after generationAny modification invalidates the signature
No chain of custodySigning key binds report to a specific identity
May not satisfy SOX tamper-evidence requirementsSHA-256 checksums accepted by SOX auditors

How It Works

1. Run validation checks → collect results
2. Serialize to canonical JSON (sorted keys, no whitespace)
3. Compute SHA-256 of canonical JSON → report_sha256
4. Sign the canonical JSON with ECDSA-P256 private key → signature
5. Store: report.json + report.pdf + report.sig

The .sig file is a detached signature — the JSON report and signature are stored separately. This allows auditors to hash the JSON file themselves and verify it matches.

Generating Signing Keys

Using OpenSSL

# Generate an ECDSA P-256 private key in PKCS#8 format
$ openssl ecparam -genkey -name prime256v1 -noout | \
openssl pkcs8 -topk8 -nocrypt -out signing-key.pem

# Extract the public key
$ openssl ec -in signing-key.pem -pubout -out signing-key-pub.pem
warning

The private key must be in PKCS#8 format (begins with -----BEGIN PRIVATE KEY-----). If your key begins with -----BEGIN EC PRIVATE KEY-----, convert it:

$ openssl pkcs8 -topk8 -nocrypt -in ec-key.pem -out pkcs8-key.pem

Key Storage Best Practices

EnvironmentRecommendation
DevelopmentLocal file with restricted permissions (chmod 600)
Docker/VMMount as a read-only volume
KubernetesKubernetes Secret or external secret operator (Vault, AWS Secrets Manager)
CI/CDInjected as a pipeline secret, never committed to git

Configuring Signing

Add to your validation.yaml:

validation.yaml
evidence:
signing:
enabled: true
private_key_path: "/etc/kafka-backup/signing-key.pem"
public_key_path: "/etc/kafka-backup/signing-key-pub.pem" # Optional

When signing is enabled, the stored JSON report uses canonical (compact) serialization to ensure the stored bytes exactly match what was signed.

Verifying Signatures

Using the CLI

$ kafka-backup validation evidence-verify \
--report evidence-report.json \
--signature evidence-report.sig \
--public-key signing-key-pub.pem

Output:

Report ID: validation-9275b4aa-2aeb-4910-a3a6-9e4aa1dc016a
Algorithm: ECDSA-P256-SHA256
Report SHA-256: 2482bbdfa113146e39a4884767002554a622ecc573f16f74e183a532de94590a
SHA-256 checksum: VALID
ECDSA signature: VALID

Evidence report integrity: VERIFIED

Manual Verification

Auditors can verify without the kafka-backup CLI:

# 1. Compute SHA-256 of the JSON report
$ sha256sum evidence-report.json
2482bbdfa113146e39a4884767002554... evidence-report.json

# 2. Compare against the SHA-256 in the .sig file
$ cat evidence-report.sig
-----BEGIN KAFKA BACKUP EVIDENCE SIGNATURE-----
Algorithm: ECDSA-P256-SHA256
Report-ID: validation-9275b4aa
Report-SHA256: 2482bbdfa113146e39a4884767002554...
Signature: MEUCIQDk...
-----END KAFKA BACKUP EVIDENCE SIGNATURE-----

# 3. If SHA-256 matches, the report has not been modified

Signature File Format

The .sig file uses a simple text format:

-----BEGIN KAFKA BACKUP EVIDENCE SIGNATURE-----
Algorithm: ECDSA-P256-SHA256
Report-ID: weekly-compliance-check-20260406-020000
Report-SHA256: e3b0c44298fc1c149afb4c8996fb924...
Signature: MEUCIQDk8nGq... (base64-encoded ECDSA signature)
-----END KAFKA BACKUP EVIDENCE SIGNATURE-----
FieldDescription
AlgorithmAlways ECDSA-P256-SHA256
Report-IDUnique validation run identifier
Report-SHA256SHA-256 hex digest of the canonical JSON report
SignatureBase64-encoded ECDSA signature over the canonical JSON bytes

Integrating with Your PKI

cert-manager (Kubernetes)

If your cluster uses cert-manager, you can use a Certificate resource to provision signing keys:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: kafka-backup-signing
spec:
secretName: kafka-backup-signing-key
issuerRef:
name: internal-ca
kind: ClusterIssuer
privateKey:
algorithm: ECDSA
size: 256
usages:
- digital signature

Mount the secret into the kafka-backup pod and reference the key path in your validation config.

HashiCorp Vault

# Store the signing key in Vault
$ vault kv put secret/kafka-backup/signing private_key=@signing-key.pem

# Retrieve at runtime
$ vault kv get -field=private_key secret/kafka-backup/signing > /tmp/signing-key.pem

Troubleshooting

"Failed to load signing key"

The private key is not in PKCS#8 format. Convert it:

$ openssl pkcs8 -topk8 -nocrypt -in your-key.pem -out pkcs8-key.pem

"Invalid ECDSA signature" during verification

  • Ensure the JSON report file has not been reformatted (the bytes must match exactly what was signed)
  • Verify you are using the correct public key that corresponds to the private key used for signing

SHA-256 mismatch

The report file was modified after signing. This could mean:

  • The file was pretty-printed or reformatted
  • The file was opened and saved by an editor that changed whitespace
  • The file was genuinely tampered with

Next Steps