Skip to main content
Version: Latest

8. Certificates

8.1 Signing Process — Full Chain

Invoice XML → XadesSignedXml.ComputeSignature() (XmlDsigC14N11Transform + XPath transforms
stripping ext:UBLExtensions/cac:Signature/QR node) → SHA-256 DigestValue = invoice hash
→ ecdsa-sha256 XML-DSig signature over the canonicalized XML
→ CryptoHelper.GenerateDigitalSignature (BouncyCastle) re-signs the hash,
producing the QR "cryptographic stamp" (tag 7)

This is a real, working ECDSA-secp256k1 implementation matching ZATCA's Phase 2 spec — not a stub.

8.2 Private Key Handling

  • The EC private key (secp256k1) is generated by ECKeyPairHelper, PEM-encoded, and stored in FatooraSecretSettings.PrivateKey — a plain string property.
  • Persisted as plaintext: AppSettingService.SetAppSetting<T> writes the value with no encryption call anywhere in the write path. The private key sits in the AppSetting.Value SQL column, or in appsettings.json (EGSConfigSettings.PrivateKey) when UseStaticKeys is enabled — either way, unencrypted at rest. See Chapter 7 — Security.
  • At runtime, the key is re-imported into an ECDsa object from the plaintext DB value on each signing operation (ecdsa.ImportFromPem(...)) — it is not sourced from a hardware security module, key vault, or any secret-manager abstraction.
  • Logging: as of Phase 0.2, X509CertificateService.GenerateCSR() no longer logs the private key via Serilog destructuring (both forks) — see Chapter 7 — Security § 7.3.

8.3 Certificate Storage

  • X509CertifcateSettings.Certificate (decoded certificate bytes as a string) and SerialNumber/IssuedOn/ExpiresOn/EncryptionPassword are persisted the same way as the private key — plaintext in AppSetting.Value, no encryption.
  • At runtime, the certificate (without its private key, per .NET's X509Store semantics for how it's added) is cached in the Windows certificate store (X509Store(StoreName.My, StoreLocation.CurrentUser)) — a process/machine-local cache, not itself a persistence mechanism.
  • ComplianceCsidSettings/ProductionCsidSettings (BinarySecurityToken, Secret — the credentials ZATCA issues after onboarding, used for Basic-Auth on every subsequent API call) are stored the same unencrypted way.

8.4 Certificate Loading

Loaded exclusively via IEInvoicingAppSettingService.GetAppSetting<T>() (cached via IEasyCachingProvider), then ECDsa.ImportFromPem(...)/X509Certificate2 construction from the retrieved plaintext string. No file-based, HSM-based, or Key-Vault-based loading path exists.

8.5 Two Documented Code-Level Anomalies

These are in the certificate code, not the signing math itself:

  • Curve identifier inconsistency: key generation correctly specifies SecObjectIdentifiers.SecP256k1 (ECKeyPairHelper.cs), but X509CertificateService.cs seeds ECDsa.Create(ECCurve.NamedCurves.nistP256) (a different curve, P-256) at two call sites before ImportFromPem immediately overwrites it with the real secp256k1 key. Functionally masked by the import, but wrong as written — a maintenance trap for anyone reading or modifying this code without noticing the import overwrites the seed. Classified Low severity, not addressed in Phase 0.2.
  • CSR field OID divergence between forks: the core SDK uses a custom OID (2.5.4.26) for the LocationAddress field in the CSR subject; the Tenancy fork uses the standard X509Name.L (Locality) OID instead. This is compliance-relevant — ZATCA's CSR subject-field spec is exact, and a wrong OID could cause CSR rejection or misclassification. Classified Medium severity, not addressed in Phase 0.2 (out of that phase's scope).

8.6 Certificate Renewal

No scheduled/automatic renewal exists — this flow is manually triggered via the onboarding controller with a fresh OTP; there is no job that watches ExpiresOn and proactively renews. See Chapter 12 — Known Limitations.