What We’re Testing
QuickZTNA issues real X.509v3 certificates using ASN.1 DER encoding with ECDSA P-256 (secp256r1) signatures. These certificates are standard-compliant and can be verified with OpenSSL. This chapter confirms that issued certificates have:
- Correct subject CN matching the requested
subject_cn - Correct issuer (CN=
QuickZTNA CA, O=QuickZTNA) - Valid date range (Not Before / Not After matching the requested TTL)
- Proper X.509v3 extensions: Basic Constraints (CA:FALSE for end-entity), Key Usage (Digital Signature, Key Encipherment)
- A valid ECDSA signature verifiable against the CA certificate
- Standard PEM encoding that OpenSSL can parse
The CA certificate itself should show: issuer = subject (self-signed), Basic Constraints CA:TRUE, Key Usage keyCertSign + cRLSign.
Your Test Setup
| Machine | Role |
|---|---|
| ⊞ Win-A | PowerShell for API calls; OpenSSL if installed (Git Bash includes it) |
| 🐧 Linux-C | OpenSSL verification commands |
Prerequisites:
- CA initialized and at least one certificate issued (complete Chapter 56 ST1 and ST2 first)
- CA certificate saved as
ca.pem - Issued certificate saved as
cert.pem - Issued private key saved as
key.pem
ST1 — Inspect the CA Certificate
What it verifies: The CA certificate is a valid self-signed X.509v3 certificate with CA:TRUE and the expected subject/issuer fields.
Steps:
- On 🐧 Linux-C , save the CA certificate from the
initialize_caresponse (or re-fetch it):
# If you have it from a previous test, use that file.
# Otherwise, re-initialize (this rotates the CA key):
curl -s -X POST "https://login.quickztna.com/api/issue-certificate" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"action": "initialize_ca", "org_id": "YOUR_ORG_ID"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['data']['ca_certificate'])" > ca.pem
- Inspect the CA certificate:
openssl x509 -in ca.pem -text -noout
Expected output (key fields):
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx
Signature Algorithm: ecdsa-with-SHA256
Issuer: O = QuickZTNA, CN = QuickZTNA CA
Validity
Not Before: Mar 17 ... 2026 GMT
Not After : Mar 15 ... 2036 GMT
Subject: O = QuickZTNA, CN = QuickZTNA CA
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
Signature Algorithm: ecdsa-with-SHA256
- Verify:
- Version is 3 (X.509v3)
- Signature Algorithm is ecdsa-with-SHA256
- Issuer and Subject are both O = QuickZTNA, CN = QuickZTNA CA (self-signed)
- Not After is approximately 10 years after Not Before
- Public Key Algorithm is id-ecPublicKey with P-256
- Basic Constraints is CA:TRUE and marked critical
- Key Usage includes Certificate Sign and CRL Sign
Pass: OpenSSL parses the CA certificate without errors. All fields match the expected values for a self-signed ECDSA P-256 CA.
Fail / Common issues:
- “unable to load certificate” — the PEM content was corrupted during copy. Ensure the file starts with
-----BEGIN CERTIFICATE-----and ends with-----END CERTIFICATE-----with no extra whitespace. - Issuer/Subject mismatch — the certificate is not self-signed. This would indicate a bug in the CA initialization code.
ST2 — Inspect an End-Entity Certificate
What it verifies: An issued certificate has the correct subject CN, is signed by the CA, and has end-entity extensions (CA:FALSE).
Steps:
- On 🐧 Linux-C , save the issued certificate from the
issueresponse:
# Issue a fresh certificate
curl -s -X POST "https://login.quickztna.com/api/issue-certificate" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"action": "issue", "org_id": "YOUR_ORG_ID", "subject_cn": "test-server.internal"}' \
| python3 -c "import sys,json; d=json.load(sys.stdin)['data']; open('cert.pem','w').write(d['certificate']); open('key.pem','w').write(d['private_key'])"
- Inspect the certificate:
openssl x509 -in cert.pem -text -noout
Expected output (key fields):
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx
Signature Algorithm: ecdsa-with-SHA256
Issuer: O = QuickZTNA, CN = QuickZTNA CA
Validity
Not Before: Mar 17 ... 2026 GMT
Not After : Apr 16 ... 2026 GMT
Subject: CN = test-server.internal
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
Signature Algorithm: ecdsa-with-SHA256
- Verify:
- Issuer is
O = QuickZTNA, CN = QuickZTNA CA(signed by the CA, not self-signed) - Subject CN matches what you requested (
test-server.internal) - Not After minus Not Before matches the TTL (default 720 hours = 30 days, or capped by
max_cert_ttl_hours) - Basic Constraints is CA:FALSE (end-entity, not a CA)
- Key Usage includes Digital Signature and Key Encipherment (not Certificate Sign)
- Issuer is
Pass: Certificate subject CN matches the request. Issuer is the QuickZTNA CA. Extensions show CA:FALSE with end-entity key usage.
Fail / Common issues:
- Subject says “unknown” instead of the requested CN — the
subject_cnfield was not provided in the API request. The handler defaults tomachine_idor"unknown". - CA:TRUE on an end-entity cert — this is a critical security bug. End-entity certificates must never have CA:TRUE.
ST3 — Verify Chain of Trust with OpenSSL
What it verifies: The issued certificate’s signature can be cryptographically verified against the CA certificate using openssl verify.
Prerequisites: Both ca.pem (CA certificate) and cert.pem (issued certificate) from ST1 and ST2.
Steps:
- On 🐧 Linux-C , verify the chain:
openssl verify -CAfile ca.pem cert.pem
Expected output:
cert.pem: OK
- Verify with a wrong CA certificate (negative test). Create a dummy CA:
openssl ecparam -genkey -name prime256v1 -out fake-ca-key.pem 2>/dev/null
openssl req -new -x509 -key fake-ca-key.pem -out fake-ca.pem -days 365 -subj "/CN=Fake CA" 2>/dev/null
openssl verify -CAfile fake-ca.pem cert.pem
Expected output:
cert.pem: ... error ... unable to get local issuer certificate
The verification should fail because the certificate was not signed by the fake CA.
- Verify the signature algorithm explicitly:
openssl x509 -in cert.pem -text -noout | grep "Signature Algorithm"
Expected: Both lines show ecdsa-with-SHA256 (the TBS signature algorithm and the outer signature algorithm must match).
Pass: openssl verify -CAfile ca.pem cert.pem returns OK. Verification against a different CA fails.
Fail / Common issues:
- “unable to get local issuer certificate” with the real CA — the CA was re-initialized between issuing the certificate and this test. Re-initializing rotates the CA key, so old certificates will not verify against the new CA.
- “certificate signature failure” — the DER encoding or ECDSA signature is malformed. This would indicate a bug in the ASN.1 encoder.
ST4 — Verify Private Key Matches Certificate
What it verifies: The private key returned at issuance corresponds to the public key embedded in the certificate.
Steps:
- On 🐧 Linux-C , extract the public key from the certificate:
openssl x509 -in cert.pem -pubkey -noout > cert-pubkey.pem
- Extract the public key from the private key:
openssl ec -in key.pem -pubout -out key-pubkey.pem 2>/dev/null
Note: the private key is in PKCS#8 format (-----BEGIN PRIVATE KEY-----). OpenSSL’s ec command can read PKCS#8 and extract the EC public key.
- Compare the two public keys:
diff cert-pubkey.pem key-pubkey.pem
Expected: No output (files are identical). The public key in the certificate matches the public key derived from the private key.
- Alternative verification — use the key pair to sign and verify:
echo "test data" > /tmp/testdata.txt
openssl dgst -sha256 -sign key.pem -out /tmp/sig.bin /tmp/testdata.txt
openssl dgst -sha256 -verify cert-pubkey.pem -signature /tmp/sig.bin /tmp/testdata.txt
Expected output:
Verified OK
Pass: The public keys match. A signature created with the private key verifies against the certificate’s public key.
Fail / Common issues:
diffshows differences — the private key does not match the certificate. This would be a critical bug where different key pairs were generated for the certificate and the returned private key.- “unable to load key” — the private key PEM was corrupted during copy. Verify the file starts with
-----BEGIN PRIVATE KEY-----.
ST5 — Verify Fingerprint Matches
What it verifies: The SHA-256 fingerprint returned by the API matches the fingerprint computed locally from the certificate DER.
Steps:
- On 🐧 Linux-C , compute the SHA-256 fingerprint of the certificate:
openssl x509 -in cert.pem -outform DER -out cert.der
sha256sum cert.der
Or use OpenSSL’s built-in fingerprint command:
openssl x509 -in cert.pem -fingerprint -sha256 -noout
Expected output (example):
sha256 Fingerprint=EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD
- Compare with the
fingerprintfield from the API response (ST2).
The API computes the fingerprint as: SHA-256(certificate DER bytes) formatted as colon-separated uppercase hex. OpenSSL’s -fingerprint -sha256 output uses the same format.
- Also verify the CA certificate fingerprint:
openssl x509 -in ca.pem -fingerprint -sha256 -noout
Compare with the fingerprint from the initialize_ca response (Chapter 56 ST1).
Pass: The locally computed SHA-256 fingerprint matches the fingerprint field returned by the API, character for character.
Fail / Common issues:
- Fingerprints do not match — the API may be computing the fingerprint from a different encoding (e.g., PEM instead of DER). The handler uses
crypto.subtle.digest('SHA-256', certDer)on the raw DER bytes, which should match OpenSSL’s DER fingerprint. - Case mismatch — the API returns uppercase hex; ensure your local tool also outputs uppercase (OpenSSL’s
-fingerprintdoes this by default).
Summary
| Sub-test | What it proves |
|---|---|
| ST1 | CA certificate is a valid self-signed X.509v3 with CA:TRUE, ECDSA P-256, 10-year validity |
| ST2 | End-entity certificates have correct subject CN, issuer from CA, CA:FALSE, and end-entity key usage |
| ST3 | Chain-of-trust verification passes with openssl verify; fails against an unrelated CA |
| ST4 | The private key returned at issuance matches the public key in the certificate |
| ST5 | API-reported SHA-256 fingerprints match locally computed fingerprints from the DER encoding |