Skip to content

Profiles

Profiles are YAML templates that define certificate characteristics: algorithm, validity, subject DN, extensions, and more. Each profile produces exactly one certificate type.

A profile is a policy template stored as a YAML file that determines:

  • Algorithm: The cryptographic algorithm for this certificate
  • Mode: How multiple algorithms are combined (simple, catalyst, composite)
  • Validity period: How long the certificate remains valid
  • Extensions: X.509 extensions configuration

Design Principle: 1 Profile = 1 Certificate

Section titled “Design Principle: 1 Profile = 1 Certificate”

Each profile produces exactly one certificate. To create multiple certificates (e.g., signature + encryption), use multiple profiles.

Profiles are organized by category and stored in profiles/:

  • ec/ - ECDSA-based profiles (modern classical)
  • rsa/ - RSA-based profiles (legacy compatibility)
  • rsa-pss/ - RSA-PSS profiles
  • ml/ - ML-DSA and ML-KEM profiles (post-quantum)
  • slh/ - SLH-DSA profiles (hash-based post-quantum)
  • hybrid/catalyst/ - Catalyst hybrid profiles (ITU-T X.509 Section 9.8)
  • hybrid/composite/ - IETF composite hybrid profiles
ModeDescriptionAlgorithm(s)
simpleSingle algorithm1
catalystDual-key certificate (ITU-T X.509 9.8)2
compositeIETF composite signature format2

Standard X.509 certificate with a single algorithm:

name: ec/tls-server
description: "TLS server ECDSA P-256"
algorithm: ecdsa-p256
validity: 365d
extensions:
keyUsage:
critical: true
values:
- digitalSignature
extKeyUsage:
values:
- serverAuth

A single certificate containing both classical and PQC public keys:

name: hybrid/catalyst/tls-server
description: "TLS server hybrid ECDSA P-256 + ML-DSA-65"
mode: catalyst
algorithms:
- ecdsa-p256 # Classical algorithm (first)
- ml-dsa-65 # PQC algorithm (second)
validity: 365d
extensions:
keyUsage:
critical: true
values:
- digitalSignature
extKeyUsage:
values:
- serverAuth

IETF composite signature where both signatures are combined and must validate:

name: hybrid/composite/tls-server
description: "TLS server hybrid composite ECDSA P-256 + ML-DSA-65"
mode: composite
algorithms:
- ecdsa-p256 # Classical algorithm (first)
- ml-dsa-65 # PQC algorithm (second)
validity: 365d
extensions:
keyUsage:
critical: true
values:
- digitalSignature
extKeyUsage:
values:
- serverAuth

NameAlgorithmUse Case
ec/root-caECDSA P-384Root CA
ec/issuing-caECDSA P-256Intermediate CA
ec/tls-serverECDSA P-256TLS server
ec/tls-clientECDSA P-256TLS client
ec/emailECDSA P-256S/MIME email
ec/code-signingECDSA P-256Code signing
ec/timestampingECDSA P-256RFC 3161 TSA
ec/ocsp-responderECDSA P-384OCSP responder
ec/signingECDSA P-256Document signing
NameAlgorithmUse Case
rsa/root-caRSA 4096Root CA
rsa/issuing-caRSA 4096Intermediate CA
rsa/tls-serverRSA 2048TLS server
rsa/tls-clientRSA 2048TLS client
rsa/emailRSA 2048S/MIME email
rsa/code-signingRSA 2048Code signing
rsa/timestampingRSA 2048RFC 3161 TSA
rsa/signingRSA 2048Document signing
rsa/encryptionRSA 2048Data encryption
NameAlgorithmUse Case
rsa-pss/tls-serverRSA 4096TLS server (TLS 1.3)
NameAlgorithmUse Case
ml/root-caML-DSA-87Root CA
ml/issuing-caML-DSA-65Intermediate CA
ml/tls-server-signML-DSA-65TLS server signature
ml/tls-server-encryptML-KEM-768TLS server encryption
ml/tls-clientML-DSA-65TLS client
ml/email-signML-DSA-65S/MIME signature
ml/email-encryptML-KEM-768S/MIME encryption
ml/code-signingML-DSA-65Code signing
ml/timestampingML-DSA-65RFC 3161 TSA
ml/ocsp-responderML-DSA-65OCSP responder
ml/signingML-DSA-65Document signing
ml/encryptionML-KEM-768Data encryption
NameAlgorithmUse Case
slh/root-caSLH-DSA-256fRoot CA
slh/issuing-caSLH-DSA-192fIntermediate CA
slh/tls-serverSLH-DSA-128fTLS server
slh/tls-clientSLH-DSA-128fTLS client
slh/timestampingSLH-DSA-256sRFC 3161 TSA
slh/signingSLH-DSA-256sDocument signing
slh/ocsp-responderSLH-DSA-256sOCSP responder
NameAlgorithmsUse Case
hybrid/catalyst/root-caECDSA P-384 + ML-DSA-87Root CA
hybrid/catalyst/issuing-caECDSA P-256 + ML-DSA-65Intermediate CA
hybrid/catalyst/tls-serverECDSA P-256 + ML-DSA-65TLS server
hybrid/catalyst/tls-clientECDSA P-256 + ML-DSA-65TLS client
hybrid/catalyst/timestampingECDSA P-384 + ML-DSA-65RFC 3161 TSA
hybrid/catalyst/ocsp-responderECDSA P-384 + ML-DSA-65OCSP responder
hybrid/catalyst/signingECDSA P-256 + ML-DSA-65Document signing
NameAlgorithmsUse Case
hybrid/composite/root-caECDSA P-384 + ML-DSA-87Root CA
hybrid/composite/issuing-caECDSA P-256 + ML-DSA-65Intermediate CA
hybrid/composite/tls-serverECDSA P-256 + ML-DSA-65TLS server
hybrid/composite/tls-clientECDSA P-256 + ML-DSA-65TLS client
hybrid/composite/timestampingECDSA P-384 + ML-DSA-65RFC 3161 TSA
hybrid/composite/signingECDSA P-384 + ML-DSA-87Document signing
hybrid/composite/ocsp-responderECDSA P-384 + ML-DSA-87OCSP responder
NameAlgorithmUse Case
eidas/qc-esignECDSA P-256Qualified electronic signature (natural person)
eidas/qc-esealECDSA P-256Qualified electronic seal (legal person)
eidas/qc-webECDSA P-256Qualified Website Authentication Certificate (QWAC)
eidas/qc-tsaECDSA P-256Qualified Timestamping Authority

These profiles include QCStatements extension for eIDAS compliance (EU 910/2014).

Note: For qualified timestamping, when a TSA certificate has qcCompliance, timestamp tokens automatically include the esi4-qtstStatement-1 extension per ETSI EN 319 422. See TSA.md.


Terminal window
qpki profile list
Terminal window
qpki profile info hybrid/catalyst/tls-server
Terminal window
qpki profile show ec/root-ca
Terminal window
# Export single profile
qpki profile export ec/tls-server ./my-tls-server.yaml
qpki profile export --all ./templates/
Terminal window
qpki profile validate my-profile.yaml

Export a builtin profile, modify it, and use it:

Terminal window
# Export a template
qpki profile export ec/tls-server ./my-custom.yaml
vim ./my-custom.yaml
qpki credential enroll --profile ./my-custom.yaml \
--var cn=server.example.com --var dns_names=server.example.com --ca-dir ./ca
qpki cert issue --profile ./my-custom.yaml --csr server.csr --out server.crt --ca-dir ./ca

QPKI uses a two-tier profile system:

  1. Built-in profiles - Embedded in the binary (default)
  2. Custom profiles - Loaded from the CA’s profiles/ directory

Custom profiles can be used in two ways:

  • Override: Use the same name as a built-in profile to replace it entirely
  • New profile: Use a different name to add a new profile alongside built-ins

To override a built-in profile:

Terminal window
# Export the built-in profile
qpki profile export ec/tls-server ./tls-server.yaml
vim ./tls-server.yaml
mkdir -p ./ca/profiles/ec
cp ./tls-server.yaml ./ca/profiles/ec/tls-server.yaml
qpki credential enroll --profile ec/tls-server --var cn=server.example.com --ca-dir ./ca --cred-dir ./credentials

To check which version is active, use qpki profile list:

Terminal window
qpki profile list --dir ./ca

The SOURCE column indicates:

  • default - Built-in profile
  • custom (overrides default) - Custom profile overriding a built-in
  • custom - Custom profile with no built-in equivalent

To revert to the built-in version, simply delete the custom profile file from CA/profiles/.


# =============================================================================
# =============================================================================
name: string # Profile identifier
description: string # Human-readable description
# Algorithm - Simple profile (single algorithm)
algorithm: string # e.g., ecdsa-p256, rsa-4096, ml-dsa-65
# Algorithm - Hybrid profile (two algorithms)
mode: string # catalyst | composite
algorithms: # List of algorithm IDs
- ecdsa-p256 # Classical algorithm (first)
- ml-dsa-65 # PQC algorithm (second)
# Signature - Override signature algorithm defaults
signature:
scheme: string # ecdsa | pkcs1v15 | rsassa-pss | ed25519
hash: string # sha256 | sha384 | sha512 | sha3-256 | sha3-384 | sha3-512
pss: # RSA-PSS specific parameters
salt_length: int # Salt length in bytes (-1 = hash length)
mgf: string # MGF hash algorithm (defaults to signature hash)
# Validity - fixed value or template
validity: duration # Duration format (e.g., 365d, 8760h, 1y)
# Or template: "{{ validity }}" (resolved at enrollment)
# Variables - Input parameters with validation
variables:
<name>:
type: string|integer|list|dns_name|dns_names|ip_list|email|uri|oid|duration
required: bool
default: value
description: string
# Type-specific constraints...
# Subject DN - Certificate subject fields
subject:
cn: "{{ variable }}" # Common Name
o: "{{ variable }}" # Organization
ou: "static value" # Organizational Unit (can be static)
c: "{{ variable }}" # Country
# Extensions - X.509 v3 extensions
extensions:
basicConstraints:
critical: bool # MUST true for CA (RFC 5280)
ca: bool # true=CA, false=end-entity
pathLen: int # Max sub-CAs (only if ca=true)
keyUsage:
critical: bool
values: [digitalSignature, keyEncipherment, ...]
extKeyUsage:
values: [serverAuth, clientAuth, ...]
subjectAltName:
dns: "{{ dns_names }}" # DNS names from variable
ip: "{{ ip_addresses }}" # IP addresses from variable
dns_include_cn: bool # Auto-add CN to DNS SANs
certificatePolicies:
policies:
- oid: string
cps: string
crlDistributionPoints:
urls: [string, ...]
authorityInfoAccess:
caIssuers: [string, ...]
ocsp: [string, ...]

Variables are referenced using {{ variable_name }} syntax. Supported locations:

LocationSupportedExample
subject: fields✅ Yescn: "{{ cn }}"
subjectAltName.dns✅ Yesdns: "{{ dns_names }}"
subjectAltName.ip✅ Yesip: "{{ ip_addresses }}"
subjectAltName.email✅ Yesemail: "{{ emails }}"
validity:✅ Yesvalidity: "{{ validity }}"
crlDistributionPoints.urls✅ Yesurls: ["{{ crl_url }}"]
authorityInfoAccess.caIssuers✅ YescaIssuers: ["{{ ca_issuer }}"]
authorityInfoAccess.ocsp✅ Yesocsp: ["{{ ocsp_url }}"]
certificatePolicies.cps✅ Yescps: "{{ cps_url }}"

Template variables are resolved at enrollment time. Use duration type for validity and uri type for URLs.

By default, DN attributes use UTF8String (ASN.1 tag 12). You can specify encoding per attribute:

subject:
cn: "{{ cn }}" # UTF8String (default)
o:
value: "ACME Corp"
encoding: printable # PrintableString (tag 19)
c:
value: "FR"
encoding: printable # Required by RFC 5280
email:
value: "{{ email }}"
encoding: ia5 # Required by RFC 5280

Available encodings:

EncodingASN.1 TagCharactersUse Case
utf812Full UnicodeDefault, RFC 5280 recommended
printable19A-Za-z0-9 ’()+,-./:=? spaceCountry (C), legacy
ia522ASCII 7-bitEmail addresses

RFC 5280 constraints (auto-applied):

  • c (country): automatically uses printable encoding
  • email: automatically uses ia5 encoding

You can omit the encoding for these attributes - it will be applied automatically. If you explicitly specify a wrong encoding (e.g., c: { encoding: utf8 }), a validation error is returned.


Profiles can declare typed variables with validation constraints. Variables enable:

  • Input validation before certificate issuance
  • Pattern matching (regex)
  • Enumerated values
  • Domain constraints (allowed_suffixes, allowed_ranges)
  • Default values
TypeGo TypeDescription
stringstringText with optional pattern/enum validation
integerintNumber with optional min/max validation
booleanboolTrue/false value
list[]stringList of strings with suffix/prefix constraints
ip_list[]stringList of IP addresses with CIDR range constraints
dns_namestringSingle DNS name with RFC 1035/1123 validation + wildcard policy
dns_names[]stringList of DNS names with RFC 1035/1123 validation + wildcard policy
emailstringEmail address with RFC 5322 validation
uristringURI with RFC 3986 validation + scheme/host constraints
oidstringObject Identifier in dot-notation (e.g., 1.2.3.4)
durationstringDuration string (Go format + d/w/y units)
name: ec/tls-server-secure
description: "Production TLS server with validation"
algorithm: ecdsa-p256
validity: "{{ validity }}" # Template - resolved at enrollment
variables:
cn:
type: string
required: true
pattern: "^[a-zA-Z0-9][a-zA-Z0-9.-]+$"
description: "Common Name (FQDN)"
organization:
type: string
required: false
default: "ACME Corp"
description: "Organization name"
country:
type: string
required: false
default: "FR"
pattern: "^[A-Z]{2}$"
minLength: 2
maxLength: 2
description: "ISO 3166-1 alpha-2 country code"
environment:
type: string
required: false
default: "production"
enum: ["development", "staging", "production"]
description: "Deployment environment"
dns_names:
type: list
required: false
default: []
constraints:
allowed_suffixes:
- ".example.com"
- ".internal"
denied_prefixes:
- "test-"
max_items: 10
description: "DNS Subject Alternative Names"
ip_addresses:
type: ip_list
required: false
constraints:
allowed_ranges:
- "10.0.0.0/8"
- "192.168.0.0/16"
max_items: 5
description: "IP Subject Alternative Names"
validity:
type: duration
required: false
default: "365d"
min_duration: "1d"
max_duration: "825d"
description: "Certificate validity period"
crl_url:
type: uri
required: false
constraints:
allowed_schemes: ["http", "https"]
description: "CRL distribution point URL"
ocsp_url:
type: uri
required: false
constraints:
allowed_schemes: ["http", "https"]
description: "OCSP responder URL"
subject:
cn: "{{ cn }}"
o: "{{ organization }}"
c: "{{ country }}"
extensions:
basicConstraints:
critical: true
ca: false
keyUsage:
critical: true
values:
- digitalSignature
- keyEncipherment
extKeyUsage:
values:
- serverAuth
# SANs with variable substitution
subjectAltName:
dns: "{{ dns_names }}"
ip: "{{ ip_addresses }}"
dns_include_cn: true
# CDP/AIA with template variables
crlDistributionPoints:
urls:
- "{{ crl_url }}"
authorityInfoAccess:
ocsp:
- "{{ ocsp_url }}"
variables:
my_var:
type: string
required: true # Must be provided
default: "value" # Default if not provided
pattern: "^[a-z]+$" # Regex pattern
enum: ["a", "b", "c"] # Allowed values
minLength: 1 # Minimum length
maxLength: 64 # Maximum length
variables:
days:
type: integer
required: false
default: 365
min: 1 # Minimum value
max: 825 # Maximum value
enum: ["30", "90", "365"] # Allowed values (as strings)
variables:
dns_names:
type: list
default: []
constraints:
allowed_suffixes: # Each item must end with one of these
- ".example.com"
denied_prefixes: # Items starting with these are rejected
- "internal-"
min_items: 1 # Minimum number of items
max_items: 10 # Maximum number of items
variables:
ip_addresses:
type: ip_list
constraints:
allowed_ranges: # IPs must be within one of these CIDRs
- "10.0.0.0/8"
- "192.168.0.0/16"
max_items: 5

The email type validates email addresses according to RFC 5322 using Go’s net/mail package.

Normalization (automatic):

  • Lowercase: User@Example.COMuser@example.com (RFC 5321 recommendation)

Constraints:

variables:
email:
type: email
required: true
constraints:
allowed_suffixes: # Domain must match one of these
- "@example.com"
- "@acme.com"
denied_prefixes: # Local part must not start with these
- "admin"
- "root"

Example validation:

ValueOptionsResult
user@example.comdefault🟢 Valid
User@Example.COMdefault🟢 Normalized to lowercase
user+tag@example.comdefault🟢 Plus addressing valid
admin@example.comdenied_prefixes: [admin]🔴 Denied prefix
user@other.comallowed_suffixes: [@example.com]🔴 Domain not allowed
not-an-emaildefault🔴 Invalid format

Use case: S/MIME certificates, TLS client authentication with email identity.

# Example: Email certificate for S/MIME
variables:
email:
type: email
required: true
constraints:
allowed_suffixes:
- "@acme.com"
- "@acme.fr"
description: "User email address (must be @acme.com or @acme.fr)"

The uri type validates URIs according to RFC 3986 and supports scheme/host constraints.

Normalization (automatic):

  • Scheme lowercase: HTTP://example.comhttp://example.com

Constraints:

variables:
ocsp_url:
type: uri
required: false
default: "http://ocsp.example.com"
constraints:
allowed_schemes: # Scheme must be one of these
- "http"
- "https"
allowed_hosts: # Host must be one of these
- "ocsp.example.com"
- "ocsp2.example.com"

Example validation:

ValueOptionsResult
http://example.comdefault🟢 Valid
https://example.com/pathdefault🟢 Valid with path
HTTP://Example.COMdefault🟢 Scheme normalized
ftp://example.comallowed_schemes: [http, https]🔴 Scheme not allowed
http://other.comallowed_hosts: [example.com]🔴 Host not allowed
example.comdefault🔴 Missing scheme

Use case: AIA (Authority Information Access) URLs, CRL Distribution Points, OCSP responder URLs.

# Example: AIA configuration
variables:
ocsp_url:
type: uri
constraints:
allowed_schemes: ["http", "https"]
allowed_hosts: ["ocsp.example.com"]
description: "OCSP responder URL"
ca_issuer_url:
type: uri
constraints:
allowed_schemes: ["http", "https"]
description: "CA certificate URL"

The oid type validates Object Identifiers in dot-notation format (e.g., 1.2.840.113549.1.1.11).

Validation rules:

  • Format: digits separated by dots (e.g., 1.2.3.4)
  • Minimum 2 arcs required (e.g., 1.2)
  • First arc must be 0, 1, or 2
  • Second arc must be < 40 when first arc is 0 or 1

Constraints:

variables:
policy_oid:
type: oid
required: false
default: "1.3.6.1.4.1.99999.1"
constraints:
allowed_suffixes: # OID must start with one of these (prefix check)
- "2.16.840.1.101.3.4" # NIST algorithms arc
- "1.3.6.1.4.1" # Private enterprise arc

Note: For OID type, allowed_suffixes acts as allowed prefixes - the OID must start with one of the specified values.

Example validation:

ValueOptionsResult
1.2.3default🟢 Valid
2.16.840.1.101.3.4.3.17default🟢 ML-DSA-44 OID
0.2.3default🟢 First arc 0
3.2.3default🔴 First arc > 2
0.40.1default🔴 Second arc >= 40 under arc 0
1default🔴 Single arc
1.a.3default🔴 Non-numeric

Use case: Certificate policies, custom extension OIDs, algorithm identifiers.

# Example: Certificate policy
variables:
policy_oid:
type: oid
default: "1.3.6.1.4.1.99999.1.1"
constraints:
allowed_suffixes:
- "1.3.6.1.4.1.99999" # Your private enterprise arc
description: "Certificate policy OID"

The duration type validates duration strings, supporting both Go’s standard format and extended units for days, weeks, and years.

Supported formats:

  • Go standard: 1h, 30m, 60s, 1h30m
  • Extended: 1d (days), 1w (weeks), 1y (years)
  • Combined: 1y6m, 30d12h, 1w1d

Conversion:

  • 1 day = 24 hours
  • 1 week = 7 days
  • 1 year = 365 days

Constraints:

variables:
validity:
type: duration
required: false
default: "365d"
min_duration: "1d" # Minimum duration
max_duration: "825d" # Maximum (CA/B Forum limit)

Example validation:

ValueOptionsResult
365ddefault🟢 Valid
1ydefault🟢 365 days
2wdefault🟢 14 days
30d12hdefault🟢 Combined
1h30mdefault🟢 Go format
12hmin_duration: "1d"🔴 Below minimum
3ymax_duration: "825d"🔴 Above maximum
abcdefault🔴 Invalid format

Use case: Certificate validity periods, CRL update intervals.

# Example: Validity with CA/B Forum constraints
variables:
validity:
type: duration
default: "365d"
min_duration: "1d"
max_duration: "825d" # CA/B Forum max for TLS
description: "Certificate validity period"
crl_validity:
type: duration
default: "7d"
min_duration: "1h"
max_duration: "30d"
description: "CRL validity period"

The dns_name and dns_names types provide built-in DNS name validation according to RFC 1035/1123, plus optional wildcard policy (RFC 6125).

Normalization (automatic):

  • Lowercase: API.Example.COMapi.example.com (RFC 4343)
  • Trailing dot stripped: example.com.example.com (FQDN)

Validation rules:

  • Total DNS name length ≤ 253 characters
  • Each label (between dots) ≤ 63 characters
  • No empty labels (double dots .. rejected)
  • Labels contain only alphanumeric characters and hyphens
  • Labels don’t start or end with a hyphen
  • Minimum 2 labels required (unless allow_single_label: true)
variables:
# Single DNS name (e.g., for CN)
cn:
type: dns_name
required: true
wildcard: # Wildcard policy (optional)
allowed: true # Permit wildcards like *.example.com (default: false)
single_label: true # RFC 6125: * matches exactly one label (default: true)
forbid_public_suffix: true # Block wildcards on public suffixes like *.co.uk
# Internal hostname (single label allowed)
internal_host:
type: dns_name
allow_single_label: true # Permit "localhost", "db-master", etc.
# List of DNS names (e.g., for SANs)
dns_names:
type: dns_names
wildcard:
allowed: false # No wildcards in SANs
constraints:
allowed_suffixes: # Domain restrictions (label boundary check)
- ".example.com"
max_items: 10

DNS Name Options:

OptionDefaultDescription
allow_single_labelfalsePermit single-label names like localhost

Wildcard Policy (RFC 6125):

OptionDefaultDescription
allowedfalseWhether wildcards are permitted
single_labeltrueRFC 6125: * matches exactly one DNS label
forbid_public_suffixfalseBlock wildcards on public suffixes (*.co.uk, *.com.au)

Wildcard validation rules:

  • Wildcard must be leftmost label: *.example.com 🟢, api.*.com 🔴
  • Minimum 3 labels required: *.example.com 🟢, *.com 🔴
  • Only one wildcard allowed: *.*.example.com 🔴
  • With forbid_public_suffix: true: *.co.uk 🔴, *.example.co.uk 🟢

Suffix matching (security):

The allowed_suffixes constraint uses label boundary matching to prevent security issues:

DNS NameSuffixResultReason
api.example.com.example.com🟢Matches on label boundary
fakeexample.com.example.com🔴Not on label boundary
example.com.example.com🟢Exact match

Example validation:

ValueOptionsResult
api.example.comdefault🟢 Valid DNS name
API.Example.COMdefault🟢 Normalized to lowercase
example.com.default🟢 Trailing dot stripped
*.example.comallowed: true🟢 Valid wildcard
*.example.comallowed: false🔴 Wildcards not allowed
*.co.ukforbid_public_suffix: true🔴 Public suffix blocked
localhostdefault🔴 Single label (needs 2+)
localhostallow_single_label: true🟢 Single label allowed
*.comallowed: true🔴 Too few labels
example..comany🔴 Empty label (double dot)

When to use dns_name vs string:

Use dns_name when:

  • You want automatic DNS format validation
  • You need wildcard certificate support with proper RFC 6125 enforcement
  • You want case normalization and trailing dot handling

Use string with pattern when:

  • You need custom regex validation
  • You have non-standard hostname requirements
# Preferred: Built-in DNS validation
cn:
type: dns_name
wildcard:
allowed: true
forbid_public_suffix: true # Recommended for production
internal_cn:
type: dns_name
allow_single_label: true
cn:
type: string
pattern: "^[a-z0-9][a-z0-9.-]+$"
Terminal window
# Single variable
qpki credential enroll --profile ec/tls-server-secure \
--var cn=api.example.com
qpki credential enroll --profile ec/tls-server-secure \
--var cn=api.example.com \
--var dns_names=api.example.com,api2.example.com \
--var environment=production \
--var organization="My Company"
qpki credential enroll --profile ec/tls-server-secure \
--var cn=api.example.com \
--var ip_addresses=10.0.0.1,10.0.0.2

Create a YAML file with variable values:

vars.yaml
cn: api.example.com
organization: "My Company"
country: US
environment: production
dns_names:
- api.example.com
- api2.example.com
ip_addresses:
- 10.0.0.1
- 10.0.0.2
validity: "365d"

Then use it:

Terminal window
qpki credential enroll --profile ec/tls-server-secure --var-file vars.yaml

File values are loaded first, then —var flags override:

Terminal window
# Load defaults from file, override CN
qpki credential enroll --profile ec/tls-server-secure \
--var-file defaults.yaml \
--var cn=custom.example.com

When using profiles with variables, the CLI automatically:

  1. Loads variables from --var-file (if provided)
  2. Overrides with --var flags
  3. Validates all values against profile constraints
  4. Applies default values for missing optional variables
  5. Builds subject DN from resolved variables
Terminal window
# Load defaults from file, override specific values
qpki credential enroll --profile ec/tls-server-secure \
--var-file defaults.yaml \
--var cn=custom.example.com

Variable validation provides clear error messages:

# Pattern mismatch
variable validation failed: cn: value "-invalid" does not match pattern "^[a-zA-Z0-9][a-zA-Z0-9.-]+$"
variable validation failed: environment: value "test" not in allowed values [development staging production]
variable validation failed: validity: duration "1000d" exceeds maximum "825d"
variable validation failed: dns_names: "api.other.com" does not match allowed suffixes [.example.com .internal]
variable validation failed: ip_addresses: IP "8.8.8.8" not in allowed ranges [10.0.0.0/8 192.168.0.0/16]

ExtensionOIDDefault CriticalDescription
keyUsage2.5.29.15trueKey usage restrictions (RFC 5280 §4.2.1.3)
extKeyUsage2.5.29.37falseExtended key usage purposes (RFC 5280 §4.2.1.12)
basicConstraints2.5.29.19trueCA flag and path length (RFC 5280 §4.2.1.9)
subjectAltName2.5.29.17falseAlternative identities (RFC 5280 §4.2.1.6)
crlDistributionPoints2.5.29.31falseCRL locations (RFC 5280 §4.2.1.13)
authorityInfoAccess1.3.6.1.5.5.7.1.1falseOCSP and CA issuer URLs (RFC 5280 §4.2.2.1)
certificatePolicies2.5.29.32falseCertificate policies (RFC 5280 §4.2.1.4)
nameConstraints2.5.29.30trueName restrictions for CA (RFC 5280 §4.2.1.10)
ocspNoCheck1.3.6.1.5.5.7.48.1.5falseSkip OCSP check for responder (RFC 6960 §4.2.2.2.1)
qcStatements1.3.6.1.5.5.7.1.3falseQualified Certificate statements (ETSI EN 319 412-5)

These extensions are automatically generated by QPKI and cannot be configured in profiles:

ExtensionOIDCriticalDescription
Subject Key Identifier2.5.29.14falseSHA-1 hash of public key (RFC 5280 §4.2.1.2)
Authority Key Identifier2.5.29.35falseCopied from issuer’s SKI (RFC 5280 §4.2.1.1)
  • SKI: Computed as SHA-1(SubjectPublicKeyInfo) per RFC 5280 method 1
  • AKI: Copied from the issuing CA certificate’s SKI
extensions:
keyUsage:
critical: true
values:
- digitalSignature
- keyEncipherment
extKeyUsage:
critical: false
values:
- serverAuth
- clientAuth
basicConstraints:
critical: true
ca: false
# CRL Distribution Points - static or template
crlDistributionPoints:
urls:
- "http://pki.example.com/crl/ca.crl" # Static URL
- "{{ crl_url }}" # Or template variable
# Authority Info Access - static or template
authorityInfoAccess:
ocsp:
- "{{ ocsp_url }}" # Template variable
caIssuers:
- "{{ ca_issuer }}" # Template variable
# Certificate Policies - CPS can be template
certificatePolicies:
policies:
- oid: "2.23.140.1.2.1"
cps: "{{ cps_url }}" # Template variable
# Subject Alternative Names - template variables
subjectAltName:
dns: "{{ dns_names }}" # Template variable (expanded at runtime)
email: "{{ email }}" # Template variable
ip: "{{ ip_addresses }}" # Template variable
dns_include_cn: true # Auto-add CN to DNS SANs
ValueDescription
digitalSignatureVerify digital signatures
contentCommitmentNon-repudiation
keyEnciphermentEncrypt keys (RSA key transport)
dataEnciphermentEncrypt data directly
keyAgreementKey agreement (ECDH)
keyCertSignSign certificates (CA only)
crlSignSign CRLs (CA only)
encipherOnlyEncipher only (with keyAgreement)
decipherOnlyDecipher only (with keyAgreement)
ValueDescriptionOID
serverAuthTLS server authentication1.3.6.1.5.5.7.3.1
clientAuthTLS client authentication1.3.6.1.5.5.7.3.2
codeSigningCode signing1.3.6.1.5.5.7.3.3
emailProtectionS/MIME email1.3.6.1.5.5.7.3.4
timeStampingTrusted timestamping1.3.6.1.5.5.7.3.8
ocspSigningOCSP responder signing1.3.6.1.5.5.7.3.9
anyAny extended key usage2.5.29.37.0

In addition to predefined values, you can specify custom OIDs directly in the values list using dot notation:

extKeyUsage:
values:
- serverAuth # Predefined value
- clientAuth # Predefined value
- "1.3.6.1.5.5.7.3.17" # Custom OID (Microsoft Document Signing)
- "1.2.3.4.5.6.7" # Organization-specific OID

OID format requirements:

  • Dot-separated integers (e.g., 1.2.3.4.5)
  • Must have at least 2 components
  • Components must be non-negative integers
  • Must be quoted in YAML to prevent parsing issues

Common custom OIDs:

OIDDescription
1.3.6.1.5.5.7.3.17Microsoft Document Signing
1.3.6.1.4.1.311.20.2.2Microsoft Smart Card Logon
1.3.6.1.5.2.3.5Kerberos PKINIT Client Authentication
extensions:
basicConstraints:
critical: true # RFC 5280: MUST be critical for CA
ca: true # true for CA, false for end-entity
pathLen: 0 # Optional: max intermediate CAs (0 = no intermediates)
extensions:
certificatePolicies:
critical: false
policies:
- oid: "2.23.140.1.2.1" # CA/Browser Forum DV
cps: "http://example.com/cps" # CPS URL
userNotice: "Certificate issued under DV policy" # Optional notice

Restricts which names a CA can issue certificates for. Only valid for CA certificates.

extensions:
nameConstraints:
critical: true # RFC 5280: MUST be critical
permitted:
dns:
- ".example.com" # Can issue for *.example.com
- "example.com" # Can issue for example.com
email:
- "@example.com" # Can issue for *@example.com
ip:
- "10.0.0.0/8" # CIDR notation
- "192.168.0.0/16"
excluded:
dns:
- ".forbidden.com" # Cannot issue for *.forbidden.com

Indicates that an OCSP responder certificate should not be checked for revocation. Used for OCSP responder certificates to avoid circular dependencies.

extensions:
ocspNoCheck:
critical: false # RFC 6960 default

QCStatements (eIDAS Qualified Certificates)

Section titled “QCStatements (eIDAS Qualified Certificates)”

The QCStatements extension is used for eIDAS qualified certificates according to ETSI EN 319 412-5. This extension contains statements that qualify the certificate for specific uses under EU regulation 910/2014.

extensions:
qcStatements:
critical: false # Per ETSI: typically not critical
qcCompliance: true # EU qualified certificate (0.4.0.1862.1.1)
qcType: esign # Certificate type: esign | eseal | web
qcSSCD: true # Key on Qualified Signature Creation Device (0.4.0.1862.1.4)
qcRetentionPeriod: 15 # Document retention in years (0.4.0.1862.1.3)
qcPDS: # PKI Disclosure Statements (0.4.0.1862.1.5)
- url: "https://pki.example.com/pds-en.pdf"
language: "en" # ISO 639-1 (2 chars)
- url: "https://pki.example.com/pds-fr.pdf"
language: "fr"
FieldOIDDescription
qcCompliance0.4.0.1862.1.1Certificate is EU qualified (eIDAS)
qcRetentionPeriod0.4.0.1862.1.3Document retention period in years
qcSSCD0.4.0.1862.1.4Private key on Qualified Signature Creation Device
qcPDS0.4.0.1862.1.5PKI Disclosure Statement locations
qcType0.4.0.1862.1.6Type of qualified certificate
ValueOIDDescription
esign0.4.0.1862.1.6.1Electronic signature (natural person)
eseal0.4.0.1862.1.6.2Electronic seal (legal person)
web0.4.0.1862.1.6.3Website authentication (QWAC)

The QcPDS statement references PKI Disclosure Statement documents. Each entry specifies:

  • url: URL to the PDS document (typically PDF)
  • language: ISO 639-1 language code (2 characters, e.g., “en”, “fr”, “de”)

Multiple PDS locations can be provided for multilingual documents.

QcPDS URLs and languages can use template variables:

variables:
pds_url:
type: uri
required: true
description: "PKI Disclosure Statement URL"
pds_lang:
type: string
required: true
pattern: "^[a-z]{2}$"
description: "ISO 639-1 language code"
extensions:
qcStatements:
qcCompliance: true
qcType: esign
qcPDS:
- url: "{{ pds_url }}"
language: "{{ pds_lang }}"
TypeProfileSubjectUse Case
QESeidas/qc-esignNatural person (CN, serialNumber)Qualified electronic signature
QESealeidas/qc-esealLegal person (O, organizationIdentifier)Qualified electronic seal
QWACeidas/qc-webLegal person + domainQualified website authentication
QTSAeidas/qc-tsaTSA serviceQualified timestamping

For advanced use cases, you can add arbitrary X.509 extensions with custom OIDs and DER-encoded values:

extensions:
custom:
- oid: "1.2.3.4.5.6.7"
critical: false
value_hex: "0403010203" # DER value as hexadecimal
- oid: "1.2.3.4.5.6.8"
critical: true
value_base64: "BAMBAgM=" # DER value as base64
FieldRequiredDescription
oidYesObject Identifier in dot notation (e.g., 1.2.3.4.5)
criticalNoWhether the extension is critical (default: false)
value_hexNo*DER-encoded value as hexadecimal string
value_base64No*DER-encoded value as base64 string

* At least one of value_hex or value_base64 must be provided, but not both.

  • The value must be valid DER-encoded ASN.1 data
  • You are responsible for correct DER encoding - QPKI passes the value as-is
  • Custom extensions are added to ExtraExtensions in the certificate template
  • Use critical: true only if clients MUST understand the extension to process the certificate
extensions:
custom:
- oid: "1.2.840.113583.1.1.9.1" # Adobe PDF archive timestamp
critical: false
value_hex: "0500" # ASN.1 NULL (0x05 0x00)
extensions:
custom:
- oid: "1.3.6.1.4.1.99999.1.1" # Your private enterprise OID
critical: false
value_base64: "MBExDzANBgNVBAMMBnZhbHVlMQ==" # DER-encoded value

To encode custom values in DER format:

Terminal window
# Create a simple UTF8String
echo -n "value" | openssl asn1parse -genstr "UTF8:value" -out - | xxd -p
echo "0403010203" | xxd -r -p | openssl asn1parse -inform DER
echo "0403010203" | xxd -r -p | base64

When the signature: field is not specified in a profile, the signature algorithm is automatically inferred from the key algorithm. The following table shows the defaults:

Key AlgorithmDefault SchemeDefault HashX.509 Signature Algorithm
ecdsa-p256ecdsasha256ECDSAWithSHA256
ecdsa-p384ecdsasha384ECDSAWithSHA384
ecdsa-p521ecdsasha512ECDSAWithSHA512
rsa-2048rsassa-psssha256SHA256WithRSAPSS
rsa-4096rsassa-psssha256SHA256WithRSAPSS
ed25519ed25519(none)PureEd25519
ml-dsa-*(intrinsic)(intrinsic)ML-DSA
slh-dsa-*(intrinsic)(intrinsic)SLH-DSA
# Use legacy PKCS#1 v1.5 instead of RSA-PSS (for compatibility)
algorithm: rsa-4096
signature:
scheme: pkcs1v15
hash: sha256
algorithm: rsa-4096
signature:
hash: sha384
algorithm: ecdsa-p384
signature:
hash: sha512

Note: Post-quantum algorithms (ML-DSA, SLH-DSA) have intrinsic signature schemes and do not use the signature: override.


IDAlgorithmTypeSecurity Level
ecdsa-p256ECDSA with P-256Classical~128-bit
ecdsa-p384ECDSA with P-384Classical~192-bit
ecdsa-p521ECDSA with P-521Classical~256-bit
ed25519Ed25519Classical~128-bit
rsa-2048RSA 2048-bitClassical~112-bit
rsa-4096RSA 4096-bitClassical~140-bit
ml-dsa-44ML-DSA-44PQCNIST Level 1
ml-dsa-65ML-DSA-65PQCNIST Level 3
ml-dsa-87ML-DSA-87PQCNIST Level 5
slh-dsa-128fSLH-DSA-128fPQCNIST Level 1
slh-dsa-192fSLH-DSA-192fPQCNIST Level 3
slh-dsa-256fSLH-DSA-256fPQCNIST Level 5
slh-dsa-256sSLH-DSA-256sPQCNIST Level 5
IDAlgorithmTypeSecurity Level
ml-kem-512ML-KEM-512PQCNIST Level 1
ml-kem-768ML-KEM-768PQCNIST Level 3
ml-kem-1024ML-KEM-1024PQCNIST Level 5

Terminal window
# Issue using an ECDSA profile
qpki credential enroll --profile ec/tls-server \
--var cn=server.example.com --var dns_names=server.example.com \
--ca-dir ./ca --cred-dir ./credentials
qpki credential enroll --profile hybrid/catalyst/tls-server \
--var cn=server.example.com --var dns_names=server.example.com \
--ca-dir ./ca --cred-dir ./credentials
qpki credential enroll --profile ml/tls-server-sign \
--var cn=server.example.com --var dns_names=server.example.com \
--ca-dir ./ca --cred-dir ./credentials
Terminal window
# Generate CSR first
qpki csr gen --algorithm ecdsa-p256 --keyout server.key \
--cn server.example.com --dns server.example.com --out server.csr
qpki cert issue --profile ec/tls-server --csr server.csr --out server.crt --ca-dir ./ca
Use CaseRecommended ProfileRationale
Maximum compatibilityec/tls-serverWorks with all modern systems
Legacy compatibilityrsa/tls-serverWorks with older systems
Quantum transitionhybrid/catalyst/tls-serverClassical + PQC in one cert
Full post-quantumml/tls-server-signPure PQC signature
Long-term archiveslh/timestampingConservative hash-based

For high-throughput scenarios (web services, APIs), profiles can be pre-compiled at startup to avoid per-certificate parsing overhead.

MetricStandardCompiledImprovement
Profile lookup~50ns~26ns2x faster
Extensions parsingPer-certOnce at loadEliminated
Regex compilationPer-certOnce at loadEliminated
CIDR parsingPer-certOnce at loadEliminated
// At startup: compile all profiles once
store := profile.NewCompiledProfileStore("./profiles")
if err := store.Load(); err != nil {
log.Fatal(err)
}
// Per-request: use pre-compiled profile (26ns lookup, 0 allocs)
cp, ok := store.Get("ec/tls-server")
if !ok {
return errors.New("profile not found")
}
// Issue certificate with pre-compiled profile
result, err := ca.EnrollWithCompiledProfile(req, cp)
  • KeyUsage: String values → x509.KeyUsage bits
  • ExtKeyUsage: String values → x509.ExtKeyUsage constants
  • BasicConstraints: Parsed once
  • NameConstraints: CIDR strings → net.IPNet structures
  • Variable patterns: Regex strings → *regexp.Regexp
  • CIDR ranges: IP ranges parsed once

  • CA - CA initialization and certificate issuance
  • Credentials - Credential enrollment with profiles
  • Keys - Key generation and CSR operations
  • Post-Quantum - Catalyst and PQC concepts
  • CLI Reference - Complete command reference