Security & Crypto · امنیت و رمزنگاری سنیورSenior ~68 دقیقه مطالعه~61 min read

PKI، TLS، گواهی‌ها و GPGPKI, TLS, Certificates & GPG

از صفر تا سطح senior در زنجیره‌ی اعتماد: ساختار واقعی گواهی X.509، CA و CSR و ابطال، دست‌دادن TLS 1.2 در برابر 1.3، mTLS، دنیای keystore/truststore و خطاهای کلاسیک PKIX در جاوا، آشپزخانه‌ی openssl، چرخش گواهی با ACME و Kubernetes، و در پایان GPG برای امضای فایل و artifact و commit.A zero-to-senior walk through the chain of trust: what an X.509 certificate really contains, CAs, CSRs and revocation, the TLS 1.2 vs 1.3 handshake, mTLS, the Java keystore/truststore world and its classic PKIX failures, an openssl cookbook, certificate rotation with ACME and Kubernetes, and finally GPG for signing files, artifacts, commits and tags.

پیش‌نیاز:Prerequisites: رمزنگاری از پایه: AES، RSA، هش و امضای دیجیتالCryptography Foundations: AES, RSA, Hashing & Digital Signatures


پشت آن قفل کوچک کنار آدرس سایت، یک ماشین اعتماد چندلایه کار می‌کند که اکثر مهندس‌ها فقط لایه‌ی اولش را می‌شناسند. تا وقتی همه‌چیز کار می‌کند کسی سؤال نمی‌پرسد؛ اما لحظه‌ای که در لاگ production این خط ظاهر شود —

PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target

— فرق مهندس mid و senior معلوم می‌شود: یکی یک TrustManager که «همه‌چیز را قبول می‌کند» کپی می‌کند، دیگری در سی ثانیه می‌فهمد کدام حلقه‌ی زنجیره غایب است.

این فصل همان ماشین را از پیچ اول باز می‌کند: از «گواهی اصلاً چیست» تا handshake، ابزارهای روزمره، چرخش خودکار در Kubernetes، و در پایان دنیای موازی PGP/GPG که همان مسئله را با فلسفه‌ای متفاوت حل کرده است.

نقشه‌ی راه

۱. پایه: کلید عمومی/خصوصی، hash، امضا. ۲. گواهی: فیلدهای X.509، SAN، EKU. ۳. زنجیره‌ی اعتماد: root، intermediate، leaf، trust store. ۴. چرخه‌ی صدور: CSR، انقضا، کوتاه‌شدن جهانی عمر گواهی‌ها. ۵. ابطال: CRL، OCSP، stapling. ۶. TLS: 1.2 در برابر 1.3، cipher suite، forward secrecy، SNI، ALPN، resumption، 0-RTT. ۷. mTLS. ۸. جاوا: keystore/truststore، keytool، cacerts، SSLContext، خطاهای کلاسیک. ۹. openssl cookbook. ۱۰. عملیات: pinning، CT، ACME، Kubernetes. ۱۱. GPG: web of trust، subkey، امضای artifact و commit، keyserver، ابطال.


بخش ۱ — سه مفهوم پایه

صندوق پستِ درِ شکافدار

صندوق پستی با یک شکاف روی در: هرکسی می‌تواند نامه بیندازد (کلید عمومی)، ولی فقط دارنده‌ی کلید فلزی می‌تواند بازش کند (کلید خصوصی). حالا برعکسش کن: اگر صاحب صندوق یک مهر شخصی داشته باشد که فقط او دارد، هرکسی مهر را می‌بیند می‌تواند تأیید کند نامه از اوست — بدون اینکه بتواند مهر را بسازد. اولی رمزنگاری است، دومی امضا.

رمزنگاری نامتقارن یعنی جفتی از کلیدها که از روی عمومی نمی‌شود خصوصی را ساخت. با کلید عمومی رمز کن تا فقط خصوصی باز کند؛ با کلید خصوصی امضا کن تا همه با عمومی تأیید کنند. الگوریتم‌های امروز: RSA (کند و بزرگ، ولی همه‌جا هست)، ECDSA روی P-256/secp256r1 (سریع و کوچک)، و Ed25519 (مدرن‌ترین؛ پیش‌فرض جدید OpenPGP).

hash یک تابع یک‌طرفه است که هر ورودی را به اثر انگشتی با طول ثابت تبدیل می‌کند (SHA-256 = ۲۵۶ بیت). تغییر یک بیت ورودی کل خروجی را عوض می‌کند و پیدا کردن دو ورودی با hash یکسان (collision) باید ناممکن باشد. MD5 و SHA-1 هر دو شکسته‌اند؛ به همین دلیل JDK از نسخه‌ی ۲۵ امضای handshake با SHA-1 را پیش‌فرض غیرفعال کرده است.

امضای دیجیتال = از سند hash بگیر، hash را با کلید خصوصی رمز کن، کنار سند بگذار. تأیید = خودت hash بگیر، امضا را با کلید عمومی باز کن، مقایسه کن. اگر برابر بودند هم integrity داری هم authenticity.

تمام PKI و تمام GPG فقط یک جمله را حل می‌کنند: «این کلید عمومی مال چه کسی است؟» PKI جواب می‌دهد «چون یک مرجع مورد اعتماد تأییدش کرده»؛ PGP جواب می‌دهد «چون کسانی که من به آن‌ها اعتماد دارم تأییدش کرده‌اند».


بخش ۲ — گواهی واقعاً چیست؟

پاسپورت

پاسپورت یک کاغذ است که می‌گوید «این عکس و این نام مال یک شخص واقعی است»، با یک مهر دولتی پایش. مأمور مرزی به تو اعتماد نمی‌کند؛ به مهر اعتماد می‌کند چون نمونه‌اش را دارد. گواهی TLS همین است: سندی که می‌گوید «این کلید عمومی متعلق به api.example.com است»، با امضای یک CA پایش.

گواهی یک ساختار استاندارد X.509 v3 است که با ASN.1/DER کدگذاری می‌شود. همان DER را با Base64 بپیچ و بین -----BEGIN CERTIFICATE----- و -----END CERTIFICATE----- بگذار، می‌شود PEM — همان چیزی که در فایل‌های .crt و .pem می‌بینی.

فیلد معنی نکته‌ی عملی
Serial Number شناسه‌ی یکتا نزد صادرکننده برای ابطال و پیگیری با CA لازم است
Signature Algorithm مثلاً sha256WithRSAEncryption اگر SHA-1 دیدی، گواهی مرده است
Issuer DN صادرکننده باید دقیقاً با Subject گواهی بالادست برابر باشد
Validity notBefore / notAfter منشأ اکثر حوادث production
Subject DN دارنده (CN=...) برای تطبیق hostname دیگر استفاده نمی‌شود
SubjectPublicKeyInfo کلید عمومی + الگوریتمش چیزی که واقعاً امضا می‌شود
Subject Alternative Name فهرست دامنه‌ها/IPها تنها منبع حقیقت برای hostname
Basic Constraints CA:TRUE/FALSE و pathlen تفاوت یک CA با یک برگ
Key Usage digitalSignature, keyCertSign مصرف مجاز کلید
Extended Key Usage serverAuth, clientAuth, codeSigning mTLS بدون clientAuth کار نمی‌کند
AKI / SKI اثر انگشت کلید صادرکننده / خودش با این‌ها زنجیره ساخته می‌شود
CRL Distribution Points آدرس فهرست ابطال ممکن است اصلاً نباشد
Authority Information Access آدرس OCSP و گواهی صادرکننده caIssuers نجات‌دهنده‌ی زنجیره‌ی ناقص
SCT List رسیدهای Certificate Transparency مرورگرها بدون آن گواهی را رد می‌کنند
# گواهی یک سرور را بگیر و کامل بخوان
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -text

# فقط چیزهایی که ۹۰٪ مواقع لازم داری
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
`CN` مرده است — به SAN نگاه کن

سال‌ها دامنه در Subject CN نوشته می‌شد. از حدود ۲۰۱۷ مرورگرهای اصلی CN را برای تطبیق hostname کاملاً نادیده می‌گیرند و فقط subjectAltName را می‌خوانند. اگر گواهی داخلی می‌سازی و فقط -subj "/CN=api.internal" می‌دهی، در مرورگر و در curl و در کلاینت‌های جدید جاوا با hostname mismatch رد می‌شود. همیشه -addext "subjectAltName=DNS:api.internal" را هم بده.

گواهی self-signed یعنی Issuer == Subject. از نظر رمزنگاری معتبر است و TLS با آن کار می‌کند؛ فقط هیچ‌کس دلیلی برای اعتماد ندارد مگر دستی به trust store اضافه شود. تیم‌ها برای سرویس‌های داخلی self-signed می‌سازند و در هر کلاینت استثنا می‌گذارند؛ سه ماه بعد ۴۰ گواهی دارند بدون هیچ راهی برای ابطال یا چرخش. راه درست: یک CA داخلی بساز، root آن را یک‌بار در همه‌ی trust storeها پخش کن، بعد برای هر سرویس برگ صادر کن — آن‌وقت چرخش برگ‌ها هیچ تغییری در کلاینت‌ها لازم ندارد.


بخش ۳ — زنجیره‌ی اعتماد

مدیرعامل پای هر قرارداد کوچکی امضا نمی‌گذارد؛ به معاون‌ها وکالت می‌دهد و آن‌ها امضا می‌کنند. طرف مقابل امضای معاون را می‌بیند، وکالت‌نامه‌اش را چک می‌کند و به امضای مدیرعامل می‌رسد که نمونه‌اش را دارد. root CA مدیرعامل است، intermediate معاون، و گواهی سرور قرارداد.

  • Root CA: self-signed، کلیدش در HSM آفلاین، عمر ۱۵–۲۵ سال، و در trust store سیستم‌عامل/مرورگر/JDK حاضر است.
  • Intermediate CA: با root امضا شده، آنلاین، روزانه میلیون‌ها گواهی صادر می‌کند. لو رفتنش فقط خودش را می‌سوزاند نه root را.
  • Leaf: گواهی سرور تو، با CA:FALSE؛ نمی‌تواند چیزی امضا کند.

زنجیره از root تا برگ — Chain of trust from root to leaf:

flowchart TD
  R["Root CA<br/>self-signed, offline HSM<br/>in OS/JDK trust store"]
  I["Intermediate CA<br/>online, signs millions<br/>sent by the server"]
  L["Leaf certificate<br/>SAN = api.example.com<br/>CA:FALSE"]
  K["Server private key<br/>never leaves the host"]
  R -->|signs| I
  I -->|signs| L
  L -.-> K

قانون طلایی: سرور باید برگ + تمام intermediateها را بفرستد ولی نباید root را بفرستد. کلاینت با AKI/Issuer زنجیره را می‌سازد تا به یک root در trust store خودش برسد.

کلاسیک‌ترین باگ TLS دنیا: intermediate جاافتاده

سرور فقط برگ را می‌فرستد. در مرورگر دسکتاپ کار می‌کند (چون intermediate را cache کرده یا با AIA می‌آورد) ولی در curl، جاوا، اپ موبایل و سرویس‌های backend می‌شکند. علامتش unable to get local issuer certificate در openssl و PKIX path building failed در جاواست. تست قطعی:

openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null
# باید دو (یا بیشتر) بلوک CERTIFICATE ببینی و در انتها: Verify return code: 0 (ok)

راه‌حل: در nginx مقدار ssl_certificate باید fullchain باشد نه فقط cert.pem.

محیط trust store پیش‌فرض
Debian/Ubuntu /etc/ssl/certs/ca-certificates.crt (update-ca-certificates)
RHEL/Fedora /etc/pki/tls/certs/ca-bundle.crt (update-ca-trust)
JDK $JAVA_HOME/lib/security/cacerts (رمز پیش‌فرض changeit)
Python/requests بسته‌ی certifi
Node.js لیست داخلی + NODE_EXTRA_CA_CERTS
جاوا trust store سیستم‌عامل را نمی‌خواند

این نکته ساعت‌ها debug تلف می‌کند: CA شرکت را با update-ca-certificates اضافه می‌کنی، curl کار می‌کند، ولی اپلیکیشن جاوا همچنان PKIX path building failed می‌دهد چون JVM cacerts خودش را دارد. بعضی توزیع‌ها با بسته‌ی ca-certificates-java این دو را همگام می‌کنند، ولی روی image رسمی JDK داخل Docker معمولاً چنین چیزی نیست.


بخش ۴ — CSR، صدور و انقضا

CSR یک درخواست امضاشده است: «این کلید عمومی من، این هم نام‌هایی که می‌خواهم، لطفاً امضا کن». CSR با کلید خصوصی خودت امضا می‌شود تا CA مطمئن شود واقعاً آن کلید را در اختیار داری.

فرایند صدور گواهی — Certificate issuance flow:

sequenceDiagram
  participant S as Server / Ops
  participant CA as Certificate Authority
  participant DNS as DNS / HTTP endpoint
  S->>S: generate private key (never leaves)
  S->>S: build CSR (public key + SANs), self-sign it
  S->>CA: submit CSR
  CA->>DNS: domain control validation (HTTP-01 / DNS-01)
  DNS-->>CA: challenge satisfied
  CA->>CA: apply policy, add SCT, sign with intermediate key
  CA-->>S: leaf certificate + chain
  S->>S: install cert + chain, reload server

سه حقیقت درباره‌ی CSR که تازه‌کارها اشتباه می‌کنند: (۱) کلید خصوصی هرگز در CSR نیست و نباید برای CA فرستاده شود — اگر ارائه‌دهنده‌ای خودش کلید را تولید می‌کند، آن کلید دیگر خصوصی نیست. (۲) CA مجبور نیست هرچه در CSR هست را بپذیرد؛ عملاً همه‌چیز به‌جز کلید عمومی و SANها را خودش تعیین می‌کند. (۳) با هر چرخش کلید، CSR جدید لازم است.

# ۱) کلید خصوصی مدرن (ECDSA P-256)
openssl ecparam -name prime256v1 -genkey -noout -out api.key
chmod 600 api.key

# ۲) CSR با SAN — نکته: -noenc جایگزین -nodes قدیمی در OpenSSL 3.x است
openssl req -new -key api.key -out api.csr \
  -subj "/CN=api.example.com/O=Example Inc" \
  -addext "subjectAltName=DNS:api.example.com,DNS:www.api.example.com"

# ۳) قبل از ارسال، همیشه CSR را بخوان
openssl req -in api.csr -noout -text -verify

SAN، wildcard و انتخاب استراتژی

DNS:api.example.com دقیقاً همان نام است. DNS:*.example.com فقط یک برچسب را پوشش می‌دهد: با a.example.com می‌خورد، با a.b.example.com نه، و با خود example.com هم نه. برای IP هم باید IP:10.0.0.5 بنویسی؛ گذاشتن IP در فیلد DNS بی‌فایده است.

گزینه مزیت ریسکی که senior باید بداند
تک‌دامنه کوچک‌ترین شعاع انفجار، چرخش مستقل تعداد زیاد گواهی → اتوماسیون اجباری
SAN چنددامنه‌ای یک گواهی برای چند سرویس مرتبط افشای فهرست دامنه‌های داخلی؛ چرخش همه با هم
Wildcard بدون صدور جدید برای زیردامنه‌ی جدید یک کلید خصوصی روی همه‌ی ماشین‌ها؛ فقط با DNS-01
کوتاه‌عمر (۶ روزه) ابطال عملاً بی‌معنی می‌شود اتوماسیون باید بی‌نقص باشد
mTLS از CA خصوصی کنترل کامل، بدون هزینه‌ی CA عمومی خودت باید چرخش و HSM را اداره کنی

wildcard وسوسه‌انگیز است ولی یعنی همان یک کلید خصوصی روی هر ماشینی که یک زیردامنه سرو می‌کند کپی می‌شود — از load balancer تا آن سرویس آزمایشی که کسی patch نمی‌کند. با ACME، صدور جداگانه برای هر نام تقریباً رایگان است. wildcard را برای جایی نگه دار که زیردامنه‌ها پویا هستند (multi-tenant) و صدور on-demand عملی نیست.

انقضا: بزرگ‌ترین حادثه‌ی سری در مهندسی

عمر گواهی‌های عمومی به‌سرعت کوتاه می‌شود. طبق مصوبه‌ی SC-081v3 در CA/Browser Forum (آوریل ۲۰۲۵):

از تاریخ حداکثر عمر گواهی TLS عمومی
اکنون ۳۹۸ روز
۱۵ مارس ۲۰۲۶ ۲۰۰ روز
۱۵ مارس ۲۰۲۷ ۱۰۰ روز
۱۵ مارس ۲۰۲۹ ۴۷ روز

Let's Encrypt جلوتر رفته: پروفایل shortlived با اعتبار ۱۶۰ ساعت (کمی بیش از ۶ روز) از ژانویه ۲۰۲۶ عمومی شده، و اعلام کرده پروفایل پیش‌فرض classic را از فوریه ۲۰۲۷ به ۶۴ روز و از فوریه ۲۰۲۸ به ۴۵ روز کاهش می‌دهد.

اگر تجدید دستی است، تو یک حادثه‌ی زمان‌بندی‌شده داری

با عمر ۴۷ روزه، «یادآوری تقویم» جواب نمی‌دهد. سه چیز لازم است: inventory از همه‌ی گواهی‌ها با تاریخ انقضا، تجدید خودکار (ACME/cert-manager/Vault)، و alert مستقل از ابزار صدور که مستقیماً به پورت وصل می‌شود — چون خرابی رایج این نیست که گواهی صادر نشود، بلکه این است که صادر شود ولی سرویس reload نشود و همچنان گواهی قدیمی را سرو کند.

اگر inventory را در پایگاه‌داده نگه می‌داری، پرس‌وجوی هشدار چنین است:

SELECT common_name, environment, not_after,
       (not_after::date - CURRENT_DATE) AS days_left
FROM   cert_inventory
WHERE  not_after < CURRENT_DATE + INTERVAL '21 days'
ORDER  BY not_after;
تفاوت دیالکت

در PostgreSQL تفریق دو date عدد صحیح روز می‌دهد و INTERVAL صریح است؛ در Oracle تفریق دو DATE عدد اعشاری روز می‌دهد، پس TRUNC لازم است و جمع عدد ساده با SYSDATE یعنی روز. اگر ستون در Oracle از نوع TIMESTAMP باشد، نتیجه یک INTERVAL DAY TO SECOND می‌شود نه عدد — یک تله‌ی کلاسیک.


بخش ۵ — ابطال: CRL، OCSP و stapling

فرض کن کلید خصوصی سرور لو رفته و گواهی هنوز ۴۰ روز اعتبار دارد. چطور به دنیا بگوییم این گواهی دیگر معتبر نیست؟

  • CRL: CA فهرست امضاشده‌ای از serialهای ابطال‌شده منتشر می‌کند (آدرسش در CRL Distribution Points). حجیم است و باید مرتب دانلود شود.
  • OCSP: کلاینت برای همان یک گواهی از responder می‌پرسد و good/revoked/unknown می‌گیرد. یک رفت‌وبرگشت در مسیر بحرانی، به‌علاوه‌ی افشای حریم خصوصی — CA می‌فهمد کدام IP کدام سایت را می‌بیند.
  • OCSP Stapling: خودِ سرور پاسخ امضاشده و تازه را از CA می‌گیرد و در handshake «سنجاق» می‌کند (extension status_request). کلاینت هیچ تماسی با CA نمی‌گیرد.

جریان stapling — OCSP stapling flow:

sequenceDiagram
  participant C as Client
  participant S as Server
  participant O as OCSP responder (CA)
  Note over S,O: periodically, off the critical path
  S->>O: OCSP request for its own serial
  O-->>S: signed response (good, valid ~7 days)
  C->>S: ClientHello + status_request extension
  S-->>C: ServerHello + Certificate + stapled OCSP response
  C->>C: verify signature and freshness locally
ابطال در عمل تقریباً کار نمی‌کند — و صنعت پذیرفته است

اکثر کلاینت‌ها soft-fail هستند: اگر responder در دسترس نبود، اتصال را می‌پذیرند. یعنی مهاجمی که ترافیک را در دست دارد فقط کافی است پاسخ را drop کند. Let's Encrypt سرویس OCSP خود را در ۶ اوت ۲۰۲۵ کاملاً تعطیل کرد (دلیل اصلی: حریم خصوصی) و به CRL مهاجرت کرد. پاسخ واقعی صنعت به مسئله‌ی ابطال، گواهی کوتاه‌عمر است: گواهی ۶ روزه اصلاً اطلاعات ابطال ندارد چون سریع‌تر از هر CRL منقضی می‌شود.

اکستنشن «must-staple» (OID 1.3.6.1.5.5.7.1.24) soft-fail را به hard-fail تبدیل می‌کند: «اگر پاسخ stapled ندیدی، قطع کن». قدرتمند ولی خطرناک — خرابی stapling سایتت را به‌جای کند کردن، کاملاً از دسترس خارج می‌کند.

server {
    listen 443 ssl;
    http2 on;
    server_name api.example.com;

    ssl_certificate     /etc/ssl/api/fullchain.pem;   # leaf + intermediates
    ssl_certificate_key /etc/ssl/api/privkey.pem;

    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;                    # در TLS 1.3 انتخاب با کلاینت است
    ssl_session_cache   shared:SSL:10m;
    ssl_session_tickets off;                          # بدون چرخش کلید، forward secrecy را می‌شکند

    ssl_stapling        on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/ssl/api/chain.pem;   # فقط برای تأیید پاسخ OCSP

    add_header Strict-Transport-Security "max-age=63072000" always;
}

بخش ۶ — TLS: دست‌دادن، قدم به قدم

دو نفر در کافه‌ای شلوغ می‌خواهند حرف بزنند بدون اینکه کسی بشنود، در حالی که هم‌دیگر را ندیده‌اند: (۱) هویت طرف مقابل را با پاسپورتش تأیید کن، (۲) زبان رمزی مشترک انتخاب کنید، (۳) کلمه‌ی عبوری بسازید که هیچ‌کدام کاملاً آن را نفرستاده باشید، (۴) از این به بعد همه‌چیز به آن زبان. handshake دقیقاً همین چهار قدم است.

TLS 1.2 handshake (two round trips before data):

sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: ClientHello (versions, cipher suites, SNI, ALPN, random)
  S-->>C: ServerHello (chosen suite, random)
  S-->>C: Certificate (leaf + intermediates)
  S-->>C: ServerKeyExchange (ECDHE params, signed) + ServerHelloDone
  C->>S: ClientKeyExchange (ECDHE public share)
  C->>S: ChangeCipherSpec + Finished
  S-->>C: ChangeCipherSpec + Finished
  C->>S: Application data (HTTP request)

هزینه: 2-RTT قبل از اولین بایت داده. روی موبایل با ۱۰۰ms تأخیر یعنی ۲۰۰ms فقط برای handshake، به‌علاوه‌ی TCP و DNS.

TLS 1.3 handshake (one round trip, encrypted early) — RFC 8446:

sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: ClientHello + key_share (guessed group) + supported_versions + SNI/ALPN
  S-->>C: ServerHello + key_share
  Note over C,S: shared secret derived — everything below is encrypted
  S-->>C: {EncryptedExtensions, Certificate, CertificateVerify, Finished}
  C->>S: {Finished} + Application data
  S-->>C: {NewSessionTicket} for future resumption
جنبه TLS 1.2 TLS 1.3
RTT تا اولین داده ۲ ۱ (و ۰ با 0-RTT)
Forward secrecy اختیاری (فقط با ECDHE) اجباری
نام suite TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_AES_128_GCM_SHA256
مذاکره‌ی الگوریتم امضا داخل suite مستقل (signature_algorithms)
گواهی روی سیم متن ساده رمزشده
Resumption Session ID / Ticket فقط PSK با NewSessionTicket
RC4/3DES/CBC/RSA-kex هنوز مذاکره‌شدنی حذف‌شده از استاندارد

TLS 1.3 با حذف امن شد: RSA key transport، DH ایستا، فشرده‌سازی، renegotiation و تمام suiteهای غیر AEAD رفتند، و دسته‌ای کامل از حملات (ROBOT، CRIME، Logjam، FREAK) دیگر قابل اجرا نیستند چون آن ویژگی‌ها اصلاً وجود ندارند.

آناتومی یک cipher suite

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 را قطعه‌قطعه بخوان: ECDHE تبادل کلید است (آن E آخر یعنی ephemeral — کلید موقت که بعد دور ریخته می‌شود)، RSA الگوریتم امضای سرور (یعنی نوع کلید گواهی)، AES_128_GCM رمز متقارن با حالت AEAD، و SHA256 تابع hash برای KDF. در TLS 1.3 فقط پنج suite وجود دارد و همه AEAD هستند: TLS_AES_128_GCM_SHA256، TLS_AES_256_GCM_SHA384، TLS_CHACHA20_POLY1305_SHA256، TLS_AES_128_CCM_SHA256، TLS_AES_128_CCM_8_SHA256.

forward secrecy — چرا اهمیت دارد

با TLS_RSA_WITH_... کلاینت یک secret تصادفی را با کلید عمومی گواهی رمز می‌کرد و می‌فرستاد. یعنی هرکس ترافیک را امروز ضبط کند و پنج سال بعد کلید خصوصی سرور را به دست بیاورد، تمام آن ترافیک قدیمی را رمزگشایی می‌کند. با ECDHE هر اتصال کلید موقت خودش را دارد که هرگز روی سیم نمی‌رود؛ لو رفتن کلید گواهی، گذشته را لو نمی‌دهد. در TLS 1.3 این اجباری است.

JDK 25 و 21.0.10 مجموعه‌ی `TLS_RSA_*` را غیرفعال کردند

از JDK 25 (سپتامبر ۲۰۲۵) و به‌روزرسانی JDK 21.0.10 (ژانویه ۲۰۲۶)، suiteهای TLS_RSA_* که forward secrecy ندارند به‌صورت پیش‌فرض غیرفعال شده‌اند. اگر بعد از ارتقای JDK ناگهان اتصال به یک broker یا سرویس قدیمی با no cipher suites in common یا handshake_failure بشکند، دلیلش همین است. راه‌حل درست بردن سرور مقابل به ECDHE است؛ بازنویسی jdk.tls.disabledAlgorithms فقط یک مسکّن با تاریخ انقضاست.

SNI، ALPN، ECH و resumption

SNI نام میزبان را در ClientHello و در متن ساده می‌فرستد تا سروری که صد دامنه روی یک IP دارد بداند کدام گواهی را بدهد؛ بدون آن virtual hosting با HTTPS ممکن نیست. ALPN در همان پیام می‌گوید کلاینت h2/http/1.1/h3 را می‌فهمد — HTTP/2 روی TLS بدون ALPN راه نمی‌افتد و gRPC هم به همین دلیل به آن وابسته است. ECH نسل بعدی است که خود ClientHello را با کلید منتشرشده در DNS رمز می‌کند تا SNI هم پنهان شود.

اگر اتصال جاوایی‌ات پشت proxy با گواهی اشتباه برمی‌گردد، اول SNI را بررسی کن: با openssl s_client یک‌بار با -servername و یک‌بار بدون آن تست کن؛ دو گواهی متفاوت یعنی مشکل SNI است.

resumption اجازه می‌دهد اتصال بعدی از یک PSK استفاده کند: Session ID (state روی سرور، در معماری چندنودی دردسر) یا Session Ticket (state رمزشده دست کلاینت، سرور stateless می‌ماند). اگر کلید رمزگذاری ticket سال‌ها ثابت بماند و لو برود، تمام sessionهای resume‌شده‌ی گذشته قابل رمزگشایی می‌شوند — پس یا مرتب بچرخانش یا ssl_session_tickets off;.

0-RTT و حمله‌ی replay

در TLS 1.3 کلاینت می‌تواند داده‌ی برنامه را همراه اولین پیام بفرستد (early data). این داده در برابر replay محافظت‌شده نیست: مهاجم می‌تواند بسته را ضبط و ده بار بازپخش کند. اگر آن بسته POST /payments باشد، ده تراکنش ثبت می‌شود. قانون: 0-RTT فقط برای درخواست‌های idempotent. در nginx یعنی ssl_early_data on; را فقط وقتی فعال کن که اپلیکیشن هدر Early-Data: 1 را می‌بیند و درخواست‌های غیر idempotent را با 425 Too Early رد می‌کند.


بخش ۷ — mTLS

در TLS معمولی فقط سرور خودش را اثبات می‌کند. در mTLS سرور هم از کلاینت گواهی می‌خواهد و همان زنجیره‌ی اعتماد را روی آن اجرا می‌کند.

mTLS handshake — both sides present certificates:

sequenceDiagram
  participant C as Client (has cert + key)
  participant S as Server (has cert + key)
  C->>S: ClientHello
  S-->>C: ServerHello + Certificate + CertificateVerify
  S-->>C: CertificateRequest (acceptable CAs, sig algs)
  C->>S: Certificate (client leaf + chain)
  C->>S: CertificateVerify (proves possession of client key)
  S->>S: verify chain against client-CA truststore + check EKU clientAuth
  S-->>C: Finished
سناریو mTLS مناسب است؟ چرا
کاربر انسانی در مرورگر خیر توزیع گواهی روی دستگاه کاربر کابوس است؛ OIDC بهتر است
سرویس‌به‌سرویس داخل mesh بله هویت workload، صدور خودکار، عمر کوتاه
شریک B2B با تعداد ثابت بله تعداد محدود و قرارداد رسمی
API عمومی با هزاران مصرف‌کننده خیر onboarding و چرخش مقیاس‌پذیر نیست؛ OAuth2 بهتر است
اتصال اپ به DB/broker معمولاً بله کلاینت‌ها کم و اتوماسیون‌پذیرند
mTLS جایگزین authorization نیست

mTLS می‌گوید «طرف مقابل کیست»، نه «اجازه‌ی چه کاری دارد». الگوی بالغ: mTLS برای احراز هویت workload در لایه‌ی انتقال، و توکن (JWT/OAuth2) برای مجوز کاربر نهایی که میان سرویس‌ها propagate می‌شود. مدل مجوز در فصل امنیت میکروسرویس‌ها آمده؛ این دو مکمل‌اند نه رقیب.

دام بزرگ mTLS: چرخش هم‌زمان

اگر همه‌ی گواهی‌های کلاینت را یک روز و با یک اسکریپت صادر کنی، همه یک روز هم می‌میرند و کل سیستم هم‌زمان می‌ایستد. چرخش CA از خود گواهی‌ها سخت‌تر است: باید دوره‌ی گذاری داشته باشی که هر دو طرف هر دو CA را در truststore دارند، سپس برگ‌ها را جابه‌جا کنی، و در انتها CA قدیمی را حذف کنی. ترتیب برعکس یعنی قفل کردن خوشه‌ی خودت.


بخش ۸ — سمت جاوا

کیف پول در برابر دفترچه تلفن

keystore کیف پول توست: مدارک شناسایی خودت (گواهی + کلید خصوصی) و باید قفل باشد. truststore دفترچه تلفن است: فهرست کسانی که به آن‌ها اعتماد داری (گواهی‌های CA، بدون هیچ کلید خصوصی). اپلیکیشنی که فقط کلاینت است اغلب keystore لازم ندارد؛ سرور TLS حتماً دارد؛ در mTLS هر دو طرف هر دو را دارند.

ویژگی JKS PKCS12 (.p12/.pfx)
استاندارد اختصاصی Sun/Oracle استاندارد بین‌المللی (RFC 7292)
قابل حمل بین زبان‌ها خیر بله (openssl، .NET، مرورگرها)
پیش‌فرض JDK تا JDK 8 از JDK 9 به بعد (JEP 229)
توصیه‌ی امروز فقط سازگاری با سیستم قدیمی پیش‌فرض همیشگی

برگه‌ی تقلب keytool

کار فرمان
جفت‌کلید + self-signed keytool -genkeypair -alias api -keyalg EC -groupname secp256r1 -validity 365 -keystore ks.p12 -storetype PKCS12 -dname "CN=api.example.com" -ext "SAN=dns:api.example.com"
فهرست محتویات keytool -list -v -keystore ks.p12
ساخت CSR از کلید موجود keytool -certreq -alias api -file api.csr -keystore ks.p12 -ext "SAN=dns:api.example.com"
وارد کردن گواهی امضاشده keytool -importcert -alias api -file signed.crt -keystore ks.p12 -trustcacerts
افزودن CA به truststore keytool -importcert -alias corp-root -file corp-root.crt -keystore truststore.p12 -storetype PKCS12 -noprompt
صادر کردن به PEM keytool -exportcert -alias api -rfc -file api.pem -keystore ks.p12
حذف یک ورودی keytool -delete -alias old-ca -keystore truststore.p12
تغییر رمز store keytool -storepasswd -keystore ks.p12
تبدیل JKS به PKCS12 keytool -importkeystore -srckeystore old.jks -srcstoretype JKS -destkeystore new.p12 -deststoretype PKCS12
گرفتن گواهی از یک سرور keytool -printcert -sslserver api.example.com:443
خواندن فایل گواهی keytool -printcert -file chain.pem
دیدن تنظیمات TLS فعال JDK keytool -showinfo -tls

دو تله‌ی رایج keytool: اول وقتی گواهی امضاشده را برمی‌گردانی، keytool باید کل زنجیره را بسازد؛ اگر intermediate در همان keystore یا در cacerts نباشد Failed to establish chain from reply می‌گیری — یا intermediate را اول import کن یا یک PEM زنجیره‌ای بده. دوم گواهی برگ را باید با همان alias کلید خصوصی import کنی؛ با alias دیگر، به‌جای تکمیل ورودی کلید، یک ورودی trusted certificate جدا ساخته می‌شود و سرور همچنان گواهی self-signed قدیمی را سرو می‌کند.

cacerts و CA سازمانی

# آیا CA مورد نظر داخل truststore پیش‌فرض JDK هست؟
keytool -list -cacerts -storepass changeit | grep -i corp-root

# افزودن؛ -cacerts از JDK 9 مستقیماً به truststore پیش‌فرض اشاره می‌کند
keytool -importcert -trustcacerts -noprompt -alias corp-root \
        -file corp-root.crt -cacerts -storepass changeit

سه راه برای اعتماد به یک CA خصوصی، به ترتیب کیفیت: (۱) truststore جداگانه‌ی اپلیکیشن با -Djavax.net.ssl.trustStore=... — قابل نسخه‌بندی و چرخش، بدون دست‌زدن به JDK. (۲) افزودن در Dockerfile هنگام build — تکرارپذیر ولی با ارتقای JDK باید دوباره اجرا شود. (۳) دست‌کاری دستی cacerts روی سرور — در اولین rollout بعدی از بین می‌رود؛ انجامش نده.

FROM eclipse-temurin:25-jre
COPY corp-root.crt /tmp/corp-root.crt
RUN keytool -importcert -trustcacerts -noprompt \
      -alias corp-root -file /tmp/corp-root.crt \
      -cacerts -storepass changeit \
 && rm /tmp/corp-root.crt

ساختن SSLContext با کد

public static SSLContext build(Path trustStore, char[] trustPass,
                               Path keyStore,   char[] keyPass) throws Exception {

    KeyStore ts = KeyStore.getInstance("PKCS12");
    try (InputStream in = Files.newInputStream(trustStore)) { ts.load(in, trustPass); }
    TrustManagerFactory tmf =
            TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(ts);

    KeyManager[] kms = null;                       // فقط برای mTLS لازم است
    if (keyStore != null) {
        KeyStore ks = KeyStore.getInstance("PKCS12");
        try (InputStream in = Files.newInputStream(keyStore)) { ks.load(in, keyPass); }
        KeyManagerFactory kmf =
                KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(ks, keyPass);
        kms = kmf.getKeyManagers();
    }

    SSLContext ctx = SSLContext.getInstance("TLSv1.3");
    ctx.init(kms, tmf.getTrustManagers(), null);
    return ctx;
}

// HttpClient تطبیق hostname را خودش انجام می‌دهد
HttpClient client = HttpClient.newBuilder().sslContext(ctx).build();
`SSLSocket` خام hostname را تأیید نمی‌کند

خطرناک‌ترین پیش‌فرض JDK: HttpsURLConnection و java.net.http.HttpClient تطبیق hostname را انجام می‌دهند، ولی اگر خودت با SSLSocketFactory یا SSLEngine اتصال خام بسازی، JDK فقط زنجیره را تأیید می‌کند و نگاه نمی‌کند SAN با هاست می‌خورد یا نه — یعنی هر گواهی معتبری از هر CAیی برای هر دامنه‌ای پذیرفته می‌شود. راه‌حل صریح:

SSLSocket socket = (SSLSocket) ctx.getSocketFactory().createSocket("api.example.com", 443);
SSLParameters p = socket.getSSLParameters();
p.setEndpointIdentificationAlgorithm("HTTPS");   // بدون این خط، تأیید hostname انجام نمی‌شود
socket.setSSLParameters(p);
socket.startHandshake();
ضدالگوی «به همه اعتماد کن»
// هرگز، حتی «موقتاً» — این کد TLS را به رمزنگاری بدون احراز هویت تبدیل می‌کند
TrustManager[] trustAll = { new X509TrustManager() {
    public void checkClientTrusted(X509Certificate[] c, String a) { }
    public void checkServerTrusted(X509Certificate[] c, String a) { }   // ← MITM آزاد
    public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}};

این قطعه باید در هر code review رد شود؛ «موقتاً برای تست» همیشه به production می‌رسد. جایگزین درست: CA داخلی یا mkcert را به یک truststore جداگانه اضافه کن.

خطاهای کلاسیک و درمان واقعی

پیام خطا معنی واقعی درمان
PKIX path building failed … JVM نتوانست از گواهی سرور به یک root در truststore برسد سرور intermediate نمی‌فرستد (fullchain بگذار) یا CA در truststore نیست (import کن)
No name matching <host> found زنجیره درست است، SAN با hostname نمی‌خورد SAN را اصلاح کن؛ SNI را چک کن؛ IP را در IP: بگذار نه DNS:
CertificateExpiredException گواهی منقضی — یا ساعت ماشین اشتباه است date -u را چک کن؛ در container بدون NTP این خطا فریبنده است
no cipher suites in common هم‌پوشانی نسخه یا suite وجود ندارد با -Djavax.net.debug=ssl:handshake ببین چه پیشنهاد شده؛ به حذف TLS_RSA_* مشکوک شو
Received fatal alert: bad_certificate سرور گواهی کلاینت را رد کرده (mTLS) زنجیره‌ی کلاینت، EKU=clientAuth و truststore سرور را چک کن
Failed to establish chain from reply intermediate هنگام import برگ در دسترس نیست اول intermediate را import کن یا PEM زنجیره‌ای بده
UnrecoverableKeyException: Cannot recover key رمز store با رمز key فرق دارد در PKCS12 هر دو را یکی بگذار یا keyPassword را جدا پیکربندی کن
TrustAnchor … is not a CA certificate یک گواهی برگ را به‌عنوان CA import کرده‌ای root/intermediate واقعی را import کن
# کامل‌ترین دید به handshake (پرحجم؛ فقط در تست)
java -Djavax.net.debug=ssl:handshake:verbose:trustmanager -jar app.jar
# فقط ساخت مسیر گواهی
java -Djava.security.debug=certpath -jar app.jar
# فعال کردن بررسی ابطال (پیش‌فرض خاموش است)
java -Dcom.sun.net.ssl.checkRevocation=true \
     -Djdk.tls.client.enableStatusRequestExtension=true -jar app.jar

اگر سرور TLS خودت روی JVM اجرا می‌شود، پشتیبانی از OCSP stapling پیش‌فرض خاموش است و با -Djdk.tls.server.enableStatusRequestExtension=true روشن می‌شود (JEP 249). سمت کلاینت jdk.tls.client.enableStatusRequestExtension پیش‌فرض روشن است ولی تا com.sun.net.ssl.checkRevocation=true نباشد نتیجه‌ی بررسی اعمال نمی‌شود.

Spring Boot: SSL bundle

از Spring Boot 3.1 به‌جای پخش‌کردن مسیر keystore در ده جا، یک bundle نام‌دار تعریف می‌کنی؛ از 3.2 بارگذاری مجدد خودکار هم دارد:

spring:
  ssl:
    bundle:
      pem:
        server:
          reload-on-update: true            # فایل‌ها را watch می‌کند و bundle را دوباره می‌سازد
          keystore:
            certificate: "file:/etc/tls/tls.crt"
            private-key: "file:/etc/tls/tls.key"
          truststore:
            certificate: "file:/etc/tls/ca.crt"
      jks:
        partner:
          keystore:
            location: "classpath:client.p12"
            password: "${CLIENT_P12_PASSWORD}"
            type: "PKCS12"
server:
  ssl:
    bundle: "server"
چرا `reload-on-update` مسئله‌ی بزرگی را حل می‌کند

در Kubernetes، cert-manager گواهی تازه را در همان Secret می‌نویسد و kubelet فایل‌های mount‌شده را به‌روز می‌کند — ولی JVM آن‌ها را فقط یک‌بار در startup خوانده است. بدون reload، pod تا restart بعدی گواهی منقضی را سرو می‌کند. reload-on-update: true فایل‌ها را watch می‌کند و symlinkها را دنبال می‌کند (که برای Secretهای Kubernetes حیاتی است). اگر Spring Boot نداری، معادلش را لازم داری: یک X509ExtendedKeyManager که هر بار از فایل می‌خواند، یا rollout ساده‌ی pod در زمان تجدید.


بخش ۹ — آشپزخانه‌ی openssl

هدف فرمان
کلید ECDSA P-256 openssl ecparam -name prime256v1 -genkey -noout -out key.pem
کلید RSA 2048 openssl genrsa -out key.pem 2048
کلید Ed25519 openssl genpkey -algorithm ed25519 -out key.pem
کلید + CSR یک‌جا openssl req -newkey rsa:2048 -noenc -keyout key.pem -out req.csr -subj "/CN=x"
self-signed سریع با SAN openssl req -x509 -newkey rsa:2048 -noenc -keyout key.pem -out cert.pem -days 90 -subj "/CN=x" -addext "subjectAltName=DNS:x"
خواندن گواهی openssl x509 -in cert.pem -noout -text
فقط تاریخ‌ها / فقط SAN openssl x509 -in cert.pem -noout -dates · ... -ext subjectAltName
اثر انگشت SHA-256 openssl x509 -in cert.pem -noout -fingerprint -sha256
خواندن CSR openssl req -in req.csr -noout -text -verify
تست یک سرور openssl s_client -connect host:443 -servername host -showcerts
اجبار نسخه‌ی TLS openssl s_client -connect host:443 -tls1_2 (یا -tls1_3)
تست ALPN openssl s_client -connect host:443 -alpn h2 -servername host
تست mTLS openssl s_client -connect host:8443 -cert client.crt -key client.key -CAfile ca.crt
تأیید زنجیره آفلاین openssl verify -CAfile root.pem -untrusted intermediate.pem leaf.pem
PEM → PKCS12 openssl pkcs12 -export -inkey key.pem -in cert.pem -certfile chain.pem -name api -out bundle.p12
PKCS12 → PEM openssl pkcs12 -in bundle.p12 -noenc -out all.pem
DER ↔ PEM openssl x509 -inform DER -in cert.der -out cert.pem
سرور تست openssl s_server -accept 8443 -cert cert.pem -key key.pem -www
خواندن CRL openssl crl -in crl.der -inform DER -noout -text
پرس‌وجوی OCSP openssl ocsp -issuer chain.pem -cert leaf.pem -url http://ocsp.example.com -text

سه ترکیب که هر روز به کار می‌آیند:

# ۱) آیا این کلید خصوصی به این گواهی می‌خورد؟ (دو خروجی باید یکسان باشد)
openssl x509 -noout -pubkey -in cert.pem | openssl sha256
openssl pkey  -pubout      -in key.pem   | openssl sha256

# ۲) زنجیره‌ای که سرور واقعاً می‌فرستد چیست؟
openssl s_client -connect api.example.com:443 -servername api.example.com \
  -showcerts </dev/null 2>/dev/null | grep -E "^(depth|verify|s:|i:)"

# ۳) یک CA خصوصی کامل در ۶۰ ثانیه (برای dev و تست‌های integration)
openssl req -x509 -newkey rsa:4096 -noenc -days 3650 -keyout ca.key -out ca.crt \
  -subj "/CN=Dev Root CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"

openssl req -newkey rsa:2048 -noenc -keyout svc.key -out svc.csr -subj "/CN=svc.local"

openssl x509 -req -in svc.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out svc.crt -days 30 -sha256 \
  -extfile <(printf "subjectAltName=DNS:svc.local,IP:127.0.0.1\nextendedKeyUsage=serverAuth,clientAuth\nbasicConstraints=CA:FALSE")

openssl verify -CAfile ca.crt svc.crt      # → svc.crt: OK

نکته‌ی نسخه: -nodes (مخفف «no DES») از OpenSSL 3.0 deprecated شده و جایگزین رسمی‌اش -noenc است. هر دو یعنی «کلید خصوصی را رمز نکن» — پس chmod 600 و مالکیت درست را فراموش نکن و در production کلید را از secret manager بگیر نه از repo.


بخش ۱۰ — pinning، Certificate Transparency و ACME

pinning یعنی «فقط این کلید عمومی (یا این CA) را قبول دارم، حتی اگر CA معتبر دیگری گواهی درستی برای همین دامنه صادر کند». هدفش دفاع در برابر صدور نادرست توسط یک CA است. سه سطح دارد: pin روی گواهی برگ (با هر تجدید می‌شکند — تقریباً همیشه اشتباه)، pin روی کلید عمومی برگ یعنی SPKI (پایدارتر ولی یعنی چرخش کلید نداری)، و pin روی CA/intermediate (پایدارترین، حفاظت ضعیف‌تر).

# محاسبه‌ی pin به سبک SPKI برای یک سرور
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null \
  | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der \
  | openssl dgst -sha256 -binary | openssl enc -base64
HPKP مرده است و pinning در وب اشتباه است

هدر Public-Key-Pins به‌خاطر ریسک خودتخریبی — سایتی که کلیدش را گم می‌کند و تا پایان max-age برای همه غیرقابل‌دسترس می‌شود — از Chrome 72 حذف شد؛ جانشین آن Expect-CT هم پس از اجباری‌شدن CT در Chrome 107 حذف شد. برای وب پاسخ صنعت Certificate Transparency است: هر گواهی عمومی باید در لاگ‌های append-only ثبت شود و رسیدهای SCT را داخل خودش داشته باشد، و صاحب دامنه می‌تواند صدور غیرمجاز را ظرف چند دقیقه ببیند.

pinning هنوز در اپ موبایل و کلاینت ماشین‌به‌ماشین معنا دارد چون هر دو سر را کنترل می‌کنی. حتی آنجا: همیشه حداقل دو pin (کلید فعلی + پشتیبان آفلاین) و یک مسیر به‌روزرسانی سریع، وگرنه pinning فقط بمب ساعتی روز چرخش است.

ACME (RFC 8555) صدور را کاملاً خودکار می‌کند: کلاینت اثبات می‌کند دامنه را کنترل می‌کند و CA گواهی می‌دهد — بدون انسان و بدون فرم.

ACME issuance with HTTP-01 challenge:

sequenceDiagram
  participant A as ACME client (certbot)
  participant CA as ACME server (Let's Encrypt)
  participant W as Your web server
  A->>CA: newOrder for api.example.com
  CA-->>A: challenge token + authorization URL
  A->>W: publish token at /.well-known/acme-challenge/TOKEN
  A->>CA: ready to be validated
  CA->>W: GET http://api.example.com/.well-known/acme-challenge/TOKEN
  W-->>CA: token + account key thumbprint
  CA-->>A: authorization valid
  A->>CA: finalize with CSR
  CA-->>A: certificate + chain
challenge چطور اثبات می‌کند محدودیت
HTTP-01 فایلی روی پورت ۸۰ در /.well-known/acme-challenge/ پورت ۸۰ باید از اینترنت در دسترس باشد؛ wildcard نمی‌دهد
DNS-01 رکورد TXT روی _acme-challenge.<domain> نیاز به API ارائه‌دهنده‌ی DNS؛ تنها راه wildcard
TLS-ALPN-01 گواهی ویژه روی پورت ۴۴۳ با ALPN acme-tls/1 پورت ۴۴۳ اختصاصی؛ مناسب reverse proxy
# صدور با webroot (سرویس در حال اجرا می‌ماند)
certbot certonly --webroot -w /var/www/html \
  -d api.example.com -d www.api.example.com \
  --agree-tos -m ops@example.com --non-interactive

# wildcard فقط با DNS-01
certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cf.ini \
  -d "*.example.com" -d example.com

# گواهی ۶ روزه (پروفایل shortlived — از Certbot 4.0)
certbot certonly --preferred-profile shortlived --webroot -w /var/www/html -d api.example.com

# تست تجدید بدون مصرف rate limit، و reload بعد از هر تجدید موفق
certbot renew --dry-run
certbot renew --deploy-hook "systemctl reload nginx"
تجدید موفق ≠ سرویس گواهی جدید را ارائه می‌دهد

شایع‌ترین حادثه‌ی ACME این نیست که تجدید شکست بخورد؛ این است که تجدید موفق شود ولی nginx/JVM هرگز فایل جدید را نخواند و تا لحظه‌ی انقضا گواهی قدیمی را سرو کند. همیشه --deploy-hook (یا reload-on-update یا rollout) داشته باش و انقضا را از بیرون و روی پورت واقعی مانیتور کن نه با خواندن فایل روی دیسک.

هنگام ساخت اتوماسیون همیشه اول روی staging کار کن — Let's Encrypt محدودیت نرخ دارد و خطاهای مکرر تو را قفل می‌کند:

certbot certonly --server https://acme-staging-v02.api.letsencrypt.org/directory ...

بخش ۱۱ — چرخش گواهی در Kubernetes

الگوی استاندارد cert-manager است: یک controller که Issuer/ClusterIssuer و Certificate را می‌فهمد، گواهی می‌گیرد، در یک Secret از نوع kubernetes.io/tls می‌نویسد و قبل از انقضا خودکار تجدید می‌کند.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ops@example.com
    profile: tlsserver          # انتخاب پروفایل ACME — از cert-manager v1.18
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: api-tls
  namespace: production
spec:
  secretName: api-tls            # اینجا tls.crt و tls.key نوشته می‌شوند
  duration: 2160h                # 90d
  renewBefore: 720h              # 30d قبل از انقضا تجدید کن
  privateKey:
    algorithm: ECDSA
    size: 256
    rotationPolicy: Always       # با هر تجدید کلید جدید بساز
  dnsNames:
    - api.example.com
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer

تا cert-manager v1.17 مقدار پیش‌فرض rotationPolicy برابر Never بود — گواهی تجدید می‌شد ولی همان کلید خصوصی قدیمی برای همیشه می‌ماند. از v1.18 پیش‌فرض Always است، پس در هر تجدید هم کلید و هم گواهی عوض می‌شوند.

cert-manager rotation and pod reload:

flowchart LR
  C["Certificate CR<br/>renewBefore: 30d"] --> CM["cert-manager controller"]
  CM -->|ACME order| CA["Let's Encrypt"]
  CA -->|cert + chain| CM
  CM -->|writes| S["Secret kubernetes.io/tls<br/>tls.crt, tls.key"]
  S -->|projected volume| P["Pod filesystem /etc/tls"]
  P -->|file watcher / SIGHUP| APP["App reloads SSL context"]
  S -.->|read directly| ING["Ingress controller"]
نقطه‌ی ضعف واقعی: pod فایل جدید را نمی‌خواند

kubelet فایل‌های Secretِ mount‌شده را به‌روز می‌کند (با تأخیری تا حدود یک دقیقه)، ولی این کار را با یک symlink swap انجام می‌دهد نه با نوشتن روی همان inode — پس watcherهایی که فقط inotify روی خود فایل می‌گذارند تغییر را نمی‌بینند. سه راه: کتابخانه‌ای که symlink را دنبال می‌کند (مثل reload-on-update در Spring Boot)؛ یک hook که با تغییر Secret به فرایند SIGHUP می‌دهد؛ یا ساده‌ترین و قابل‌اعتمادترین، rollout در زمان تجدید با annotationی که hash گواهی را دارد.

در service meshها گواهی‌های workload عمر بسیار کوتاهی دارند (ساعتی یا کمتر) و در حافظه‌ی sidecar می‌مانند — هرگز روی دیسک نمی‌روند. هویت به‌جای نام DNS یک SPIFFE ID مثل spiffe://cluster.local/ns/production/sa/api است که در SAN از نوع URI قرار می‌گیرد. معماری mesh در فصل runtime میکروسرویس‌ها آمده؛ نکته‌ی این فصل این است که همان زنجیره‌ی X.509 پشت آن است.


بخش ۱۲ — GPG/PGP: همان مسئله، فلسفه‌ی متفاوت

مهر موم در برابر دفترخانه‌ی رسمی

در دنیای X.509 برای معتبر شدن امضایت باید به دفترخانه (CA) بروی و همه به دفترخانه اعتماد دارند. در دنیای PGP دفترخانه‌ای وجود ندارد: تو مهر شخصی خودت را می‌سازی و دوستانت با مهر خودشان پای مهر تو را امضا می‌کنند. اگر من به کسی اعتماد دارم و او مهر تو را امضا کرده، من هم می‌توانم به مهر تو اعتماد کنم. به این شبکه‌ی افقی می‌گویند web of trust.

PGP یک استاندارد است (OpenPGP) و GnuPG/gpg رایج‌ترین پیاده‌سازی آزادش؛ شاخه‌ی جدید در حال افزودن رمزنگاری پساکوانتومی (Kyber/ML-KEM) است.

جنبه X.509 / TLS OpenPGP / GPG
مدل اعتماد سلسله‌مراتبی؛ CA ریشه افقی؛ web of trust یا اعتماد دستی
هویت نام دامنه (SAN) نام و ایمیل (User ID)
کاربرد اصلی اتصال زنده (transport) داده در سکون: فایل، ایمیل، artifact، commit
ابطال CRL/OCSP از سمت CA revocation certificate که خود صاحب کلید منتشر می‌کند
انقضا اجباری و کوتاه اختیاری؛ بدون تغییر کلید تمدیدشدنی
ساختار کلید یک کلید در هر گواهی یک primary + چند subkey با نقش‌های متفاوت
چرا هنوز GPG یاد بگیریم؟

چون TLS فقط کانال را محافظت می‌کند نه محتوا را. وقتی یک .tar.gz از یک آینه دانلود می‌کنی، HTTPS فقط می‌گوید «این بایت‌ها دست‌نخورده از آن سرور آمدند» — نه اینکه «این بایت‌ها را واقعاً تیم اصلی ساخته است». امضای GPG روی artifact همین شکاف را پر می‌کند، و به همین دلیل تقریباً تمام توزیع‌های لینوکس و مخازن بسته و release‌های مهم به آن وابسته‌اند. تفاوت channel security و content security سؤال متداول مصاحبه‌های Platform است.

ساختار کلید: primary و subkey

flowchart TD
  P["Primary key (Ed25519)<br/>usage: Certify — keep offline"]
  S["Signing subkey<br/>usage: Sign"]
  E["Encryption subkey (Curve25519)<br/>usage: Encrypt"]
  A["Authentication subkey<br/>usage: Auth (SSH)"]
  U["User IDs — Name and email"]
  P -->|certifies| S
  P -->|certifies| E
  P -->|certifies| A
  P -->|binds| U

primary key هویت توست؛ با آن User IDها و subkeyها را تأیید می‌کنی و کلید دیگران را امضا می‌کنی. subkeyها کلیدهای کاری روزمره‌اند. مزیت بزرگ: اگر لپ‌تاپ را گم کنی فقط subkeyها را ابطال و جایگزین می‌کنی و هویت و تمام امضاهای گرفته‌شده روی primary باقی می‌ماند. الگوی senior: primary را offline نگه دار (درایو رمزشده یا کارت سخت‌افزاری) و روی ماشین کاری فقط subkeyها را داشته باش — gpg آن حالت را با sec# نشان می‌دهد.

# ساخت کلید؛ future-default امروز یعنی ed25519 برای امضا + cv25519 برای رمز
gpg --quick-generate-key "Jane Dev <jane@example.com>" future-default default 2y
gpg --quick-add-key <FINGERPRINT> ed25519 sign 1y      # subkey امضای مجزا

gpg --list-keys --keyid-format=long --with-subkey-fingerprints
gpg --list-secret-keys --keyid-format=long

gpg --armor --export jane@example.com > jane-public.asc          # برای دادن به دیگران
gpg --armor --export-secret-subkeys jane@example.com > subs.asc  # پشتیبان کاری
gpg --import colleague-public.asc

gpg --quick-set-expire <FINGERPRINT> 2y '*'            # تمدید همه‌ی subkeyها
انقضای کلید GPG مثل انقضای گواهی TLS نیست

اگر کلید GPG منقضی شود لازم نیست کلید جدید بسازی: کافی است تاریخ انقضا را جلو ببری و کلید عمومی به‌روز را دوباره منتشر کنی — تمام امضاها و اعتماد ساخته‌شده حفظ می‌شود. بزرگ‌ترین اشتباه تازه‌کارها این است که با دیدن expired یک کلید کاملاً جدید می‌سازند و کل web of trust خود را دور می‌ریزند. تاریخ انقضا اینجا یک کلید مرده‌رسان است: اگر روزی دسترسی را از دست بدهی، خودبه‌خود از اعتبار می‌افتد.

رمزکردن، امضا و تأیید

# رمزکردن برای چند گیرنده + خودت (وگرنه خودت هم نمی‌توانی بازش کنی)
gpg --encrypt -r bob@example.com -r audit@example.com -r jane@example.com \
    --output report.pdf.gpg report.pdf
gpg --decrypt report.pdf.gpg > report.pdf

gpg --symmetric --cipher-algo AES256 backup.tar     # رمز متقارن با passphrase
gpg --clear-sign announcement.txt                   # امضای متنی خوانا

# امضای جدا (detached) — استاندارد release artifact
gpg --armor --detach-sign app-1.4.0.tar.gz          # → app-1.4.0.tar.gz.asc
gpg --verify app-1.4.0.tar.gz.asc app-1.4.0.tar.gz

امضای detached استاندارد release است چون خود فایل دست‌نخورده می‌ماند: همان .tar.gz که هر ابزاری می‌خواند، به‌علاوه‌ی یک .asc کوچک کنارش. اگر امضا را داخل فایل ببری، همه مجبورند برای رسیدن به محتوا از gpg عبور کنند.

«Good signature» با هشدار یعنی نصفِ کار

خط Good signature فقط می‌گوید امضا با آن کلید عمومی جور است. هشدار not certified with a trusted signature یعنی «ولی نمی‌دانم آن کلید عمومی مال آن پروژه است». مهاجمی که آینه‌ی دانلود را در اختیار دارد می‌تواند فایل، امضا و کلید عمومی خودش را بگذارد و تو Good signature ببینی. تنها راه بستن حلقه: اثر انگشت کامل ۴۰ رقمی را از کانالی مستقل از محل دانلود بگیری و بعد کلید را محلی امضا کنی:

gpg --keyserver hkps://keys.openpgp.org --recv-keys <FULL_40_HEX_FINGERPRINT>
gpg --fingerprint <FULL_40_HEX_FINGERPRINT>    # با منبع مستقل مقایسه کن
gpg --lsign-key <FINGERPRINT>                  # امضای محلی، منتشر نمی‌شود

هرگز به key ID کوتاه (۸ یا ۱۶ رقم) اعتماد نکن؛ ساختن کلید با key ID کوتاهِ یکسان عملی و ارزان است.

keyserver، WKD و ابطال

keyserverهای قدیمی SKS اجازه می‌دادند هرکسی هر امضایی به هر کلیدی اضافه کند و چیزی هرگز حذف نشود — که به «key poisoning» و کلیدهایی با ده‌ها هزار امضای بی‌ربط منجر شد؛ آن شبکه امروز متروک است. keys.openpgp.org مدل جدید است: اطلاعات هویتی فقط با تأیید ایمیل و رضایت صاحب کلید قابل جست‌وجو می‌شود و امضاهای شخص ثالث حذف می‌شوند. WKD کلید عمومی را مستقیماً از دامنه‌ی خود شخص روی HTTPS در /.well-known/openpgpkey/... سرو می‌کند — یعنی PGP اعتماد را از PKI قرض می‌گیرد.

gpg --keyserver hkps://keys.openpgp.org --send-keys <FINGERPRINT>
gpg --locate-external-keys jane@example.com     # از WKD و keyserver
gpg --refresh-keys                              # به‌روزرسانی ابطال‌ها و انقضاها
گواهی ابطال باید *قبل* از حادثه ساخته شود

اگر کلید خصوصی را گم کنی یا passphrase را فراموش کنی، دیگر نمی‌توانی گواهی ابطال بسازی و کلید مرده‌ات برای همیشه معتبر می‌ماند. gpg 2.x هنگام ساخت کلید خودکار یکی در ~/.gnupg/openpgp-revocs.d/<FPR>.rev می‌سازد؛ آن را چاپ کن یا جدا از کلید در جای امن نگه دار. و توجه: هرکس آن فایل را داشته باشد می‌تواند کلیدت را ابطال کند — یک DoS ساده روی هویت تو.

gpg --output revoke.asc --gen-revoke <FINGERPRINT>   # امروز بساز، نه روز حادثه
gpg --import revoke.asc && gpg --keyserver hkps://keys.openpgp.org --send-keys <FINGERPRINT>

امضای commit و tag در گیت

git config --global user.signingkey <SIGNING_SUBKEY_FPR>!   # ! یعنی «دقیقاً همین subkey»
git config --global commit.gpgsign true
git config --global tag.gpgsign true
export GPG_TTY=$(tty)                                       # وگرنه passphrase پرسیده نمی‌شود

git commit -S -m "Fix token refresh race"
git tag -s v1.4.0 -m "Release 1.4.0"
git verify-commit HEAD && git verify-tag v1.4.0
git log --pretty="%h %G? %GS %s" -10    # %G? = G(ood) B(ad) U(ntrusted) N(one) E(rror)

از Git 2.34 می‌توانی با git config --global gpg.format ssh و user.signingkey ~/.ssh/id_ed25519.pub از کلید SSH استفاده کنی؛ ساده‌تر است ولی مدل انقضا و ابطال ندارد و تأیید به فایل gpg.ssh.allowedSignersFile وابسته است. برای امضایی که باید سال‌ها بعد قابل تأیید باشد، OpenPGP پخته‌تر است.

توجه کن که امضا نویسنده را اثبات نمی‌کند بلکه امضاکننده را: فیلد Author در گیت متن آزاد است و هرکسی هر چیزی می‌تواند در آن بنویسد. سیاست معنادار یعنی branch protection که امضای تأییدشده را الزامی کند به‌علاوه‌ی فهرست نگهداری‌شده‌ی کلیدهای مجاز؛ وگرنه امضا فقط یک آیکون سبز تزئینی است.

برگه‌ی تقلب gpg

کار فرمان
ساخت کلید تعاملی gpg --full-generate-key
ساخت سریع gpg --quick-generate-key "Name <mail>" future-default default 2y
افزودن subkey gpg --quick-add-key <FPR> ed25519 sign 1y
فهرست کلیدها gpg --list-keys --keyid-format=long · gpg --list-secret-keys
اثر انگشت gpg --fingerprint <FPR>
صادرکردن عمومی/خصوصی gpg --armor --export <FPR> · gpg --armor --export-secret-keys <FPR>
واردکردن gpg --import pub.asc
رمزکردن / رمزگشایی gpg -e -r bob@example.com -o f.gpg f · gpg -d f.gpg > f
رمز متقارن gpg -c --cipher-algo AES256 f
امضای جدا / متنی gpg --armor --detach-sign f · gpg --clear-sign f
تأیید gpg --verify f.asc f
امضای محلی کلید gpg --lsign-key <FPR>
تنظیم سطح اعتماد gpg --edit-key <FPR> سپس trust
تمدید انقضا gpg --quick-set-expire <FPR> 2y '*'
ساخت گواهی ابطال gpg --output rev.asc --gen-revoke <FPR>
ارسال/دریافت keyserver gpg --keyserver hkps://keys.openpgp.org --send-keys <FPR> · --recv-keys <FPR>
یافتن از WKD gpg --locate-external-keys mail@example.com
حذف کلید gpg --delete-keys <FPR> · gpg --delete-secret-keys <FPR>
اجرای غیرتعاملی در CI gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 ...
GPG در CI

در pipeline نه ترمینالی هست نه انسانی که passphrase بزند. الگوی امن: یک subkey امضای مخصوص CI با عمر کوتاه بساز (نه primary)، آن را در secret manager بگذار، در job واردش کن، و در پایان GNUPGHOME موقت را پاک کن. اگر نشت کند فقط همان subkey را ابطال می‌کنی.

export GNUPGHOME=$(mktemp -d)
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
echo "$GPG_PASSPHRASE" | gpg --batch --yes --pinentry-mode loopback \
    --passphrase-fd 0 --armor --detach-sign dist/app-1.4.0.tar.gz
rm -rf "$GNUPGHOME"

بخش ۱۳ — دفترچه‌ی عملیاتی senior

  • inventory داشته باش. فهرست هر گواهی، مالکش، محل استقرارش و تاریخ انقضایش. اگر نمی‌توانی در ۳۰ ثانیه بگویی کدام گواهی زودتر از همه منقضی می‌شود، انقضا تو را غافلگیر خواهد کرد.
  • از بیرون مانیتور کن. probeای که به پورت واقعی وصل می‌شود هم انقضا را می‌گیرد هم «تجدید شد ولی reload نشد» را — که خطای شایع‌تری است.
  • دو آستانه‌ی هشدار: نرم در ۳۰٪ عمر باقی‌مانده (تیکت)، سخت در ۱۰٪ (paging). با گواهی ۴۷ روزه یعنی ۱۴ و ۵ روز.
  • کلید خصوصی از repo بیرون. اگر کلیدی حتی یک‌بار در تاریخچه‌ی گیت رفت، سوخته است؛ باید ابطال و جایگزین شود، نه اینکه commit را پاک کنی.
  • چرخش را تمرین کن. فصلی یک‌بار در staging، CA را عوض کن. تیمی که هرگز چرخش CA را تمرین نکرده، در روز حادثه آن را برای اولین بار انجام می‌دهد.
  • زمان را جدی بگیر. container بدون NTP دقیقاً همان خطاهایی را می‌سازد که شبیه مشکل گواهی‌اند؛ date -u اولین دستور هر debug مربوط به TLS است.
  • الگوریتم‌ها را جلو ببر. امروز ECDSA P-256 یا Ed25519 برای گواهی‌های جدید، TLS 1.2 کف و 1.3 پیش‌فرض. مسیر بعدی رمزنگاری پساکوانتومی است: تبادل کلید ترکیبی X25519MLKEM768 از OpenSSL 3.5 (آوریل ۲۰۲۵) پیش‌فرض فعال است و JDK از نسخه‌ی ۲۴ پیاده‌سازی ML-KEM و ML-DSA را دارد. «برداشت امروز، رمزگشایی فردا» تهدیدی واقعی برای داده‌ی بلندعمر است.

بخش ۱۴ — پرسش‌های مصاحبه

گواهی دقیقاً چیست و چه چیزی را اثبات می‌کند؟

یک ساختار X.509 که یک کلید عمومی را به مجموعه‌ای از نام‌ها در SAN گره می‌زند، و یک CA پای این ادعا را امضا کرده است. گواهی هیچ چیز محرمانه‌ای ندارد و کاملاً عمومی است.

اثبات می‌کند: «دارنده‌ی کلید خصوصی متناظر، در زمان صدور کنترل این نام‌ها را داشته و یک CA این را تأیید کرده». اثبات نمی‌کند که سایت بدافزار ندارد، شرکت قانونی است، یا داده‌ات آن‌طرف امن نگه داشته می‌شود. برای گواهی‌های DV — یعنی اکثریت مطلق اینترنت — تنها چیز تأییدشده «کنترل دامنه» است.

در production خطای `PKIX path building failed` می‌گیری. مرحله‌به‌مرحله چه می‌کنی؟

اول تفکیک می‌کنم که مشکل زنجیره‌ی ناقص سرور است یا truststore ناقص کلاینت، با openssl s_client -connect host:443 -servername host -showcerts. اگر فقط یک بلوک CERTIFICATE دیدم، سرور intermediate نمی‌فرستد → راه‌حل جایگزینی cert.pem با fullchain.pem و reload.

اگر زنجیره کامل بود ولی openssl هم unable to get local issuer داد، root در سیستم نیست. اگر openssl و curl موفق بودند و فقط جاوا شکست خورد، تقریباً همیشه یعنی CA در cacerts نیست — چون JVM از trust store سیستم‌عامل استفاده نمی‌کند. تأیید با -Djava.security.debug=certpath و سپس import در truststore اختصاصی اپلیکیشن، نه دست‌کاری دستی cacerts که در rollout بعدی از بین می‌رود.

پرچم قرمز در پاسخ داوطلب: «یک TrustManager می‌نویسم که همه را قبول کند».

فرق keystore و truststore چیست؟ یک کلاینت به کدام نیاز دارد؟

keystore هویت خودت است: گواهی به‌همراه کلید خصوصی. truststore فهرست CAهایی است که باور داری و هیچ کلید خصوصی ندارد. از نظر فرمت هر دو می‌توانند یک فایل PKCS12 باشند؛ تفاوت در نقش است نه در نوع فایل.

کلاینت HTTPS ساده فقط truststore لازم دارد (اغلب cacerts پیش‌فرض کافی است)؛ سرور TLS حتماً keystore دارد؛ در mTLS هر دو طرف هر دو را دارند. اشتباه رایج: ریختن CAها در همان keystoreای که کلید سرور در آن است — کار می‌کند ولی چرخش کلید سرور و چرخش فهرست CAها دو چرخه‌ی کاملاً متفاوت‌اند و باید جدا بمانند.

TLS 1.3 نسبت به 1.2 چه چیزی را تغییر داد؟

سرعت: handshake از 2-RTT به 1-RTT رسید چون کلاینت گروه کلید را حدس می‌زند و key_share را از قبل در ClientHello می‌فرستد؛ با PSK حتی 0-RTT ممکن است.

امنیت با حذف: RSA key transport، DH ایستا، renegotiation، فشرده‌سازی و تمام suiteهای غیر AEAD حذف شدند؛ تعداد suiteها از صدها به پنج رسید و مذاکره‌ی الگوریتم امضا از suite جدا شد. دسته‌ای کامل از حملات (ROBOT، CRIME، Logjam، FREAK) دیگر قابل اجرا نیستند.

حریم خصوصی: از ServerHello به بعد همه‌چیز رمز است، از جمله گواهی سرور؛ در 1.2 گواهی در متن ساده می‌رفت. تنها چیز آشکار باقی‌مانده SNI است که ECH در حال حل کردنش است.

forward secrecy یعنی چه و چرا `TLS_RSA_*` حذف شد؟

در suiteهای TLS_RSA_WITH_... کلاینت یک pre-master secret را با کلید عمومی گواهی رمز می‌کرد؛ یعنی کل امنیت session به کلید بلندمدت سرور گره خورده بود. هرکس ترافیک را امروز ضبط کند و سال‌ها بعد آن کلید را به دست بیاورد، همه‌ی ترافیک گذشته را رمزگشایی می‌کند.

با ECDHE هر session جفت‌کلید موقت خودش را دارد که هرگز روی سیم نمی‌رود و بعد دور ریخته می‌شود؛ کلید بلندمدت فقط برای امضا به‌کار می‌رود نه انتقال راز. نکته‌ی عملی: JDK 25 و به‌روزرسانی 21.0.10 مجموعه‌ی TLS_RSA_* را پیش‌فرض غیرفعال کرده‌اند و همین باعث شکست ناگهانی اتصال به سیستم‌های قدیمی با no cipher suites in common می‌شود.

CRL، OCSP و stapling را مقایسه کن. امروز چه توصیه‌ای داری؟

CRL فهرست امضاشده‌ی serialهای ابطال‌شده است — حجیم و همیشه کمی کهنه. OCSP یک پرس‌وجوی زنده برای یک گواهی است — تازه‌تر، ولی یک رفت‌وبرگشت در مسیر بحرانی می‌گذارد و به CA می‌گوید چه کسی چه سایتی را می‌بیند. stapling همان پاسخ را به سرور می‌سپارد تا در handshake ضمیمه کند: بدون تأخیر و بدون افشای حریم خصوصی.

مشکل ریشه‌ای هر سه: کلاینت‌ها soft-fail هستند، پس مهاجم فقط کافی است پاسخ را مسدود کند. توصیه: stapling را روشن کن چون ارزان است، ولی به‌عنوان کنترل امنیتی جدی به آن تکیه نکن. پاسخ واقعی صنعت عمر کوتاه است — Let's Encrypt سرویس OCSP خود را در اوت ۲۰۲۵ تعطیل کرد و گواهی‌های ۶ روزه‌اش اصلاً اطلاعات ابطال ندارند.

چرا `CN` دیگر برای تطبیق hostname استفاده نمی‌شود؟ محدودیت wildcard چیست؟

CN بخشی از Distinguished Name و صرفاً یک رشته است؛ subjectAltName یک extension با نوع مشخص (DNS, IP, URI) و چندمقداری است. از حدود ۲۰۱۷ مرورگرها CN را برای اعتبارسنجی نادیده می‌گیرند و CAهای عمومی موظف‌اند هر نام موجود در CN را در SAN هم تکرار کنند.

wildcard فقط یک برچسب را پوشش می‌دهد: *.example.com با api.example.com می‌خورد، با a.b.example.com نه، و با خود example.com هم نه — apex باید جداگانه در SAN باشد. در ACME هم wildcard فقط با DNS-01 صادر می‌شود. ریسک معماری‌اش: یک کلید خصوصی مشترک روی هر ماشینی که هر زیردامنه‌ای را سرو می‌کند.

mTLS چه زمانی انتخاب درستی است و چه چیزی را حل نمی‌کند؟

مناسب است وقتی هر دو سر را کنترل می‌کنی و تعداد کلاینت‌ها اتوماسیون‌پذیر است: سرویس‌به‌سرویس داخل خوشه، اتصال اپ به DB و broker، و شرکای B2B مشخص. نامناسب است برای کاربران مرورگری و APIهای عمومی با هزاران مصرف‌کننده، چون توزیع و چرخش گواهی روی دستگاه‌های خارج از کنترل تو عملی نیست.

mTLS احراز هویت لایه‌ی انتقال است نه مجوز؛ اینکه بدانی طرف مقابل payment-service است چیزی درباره‌ی دسترسی‌اش نمی‌گوید. دام عملیاتی اصلی چرخش است: اگر همه‌ی گواهی‌ها هم‌زمان صادر شوند هم‌زمان هم می‌میرند، و چرخش CA به پنجره‌ی گذاری نیاز دارد که هر دو طرف هر دو CA را در truststore داشته باشند.

گواهی یک سرویس داخلی نیمه‌شب منقضی شد و ده سرویس افتاد. چه می‌کنی؟

مهار: سریع‌ترین مسیر بازیابی معمولاً صدور فوری گواهی جدید از همان CA و reload سرویس است. اگر CA در دسترس نیست، یک گواهی کوتاه‌عمر از CA داخلی بهتر از خاموش کردن اعتبارسنجی TLS در کلاینت‌هاست — آن کار یک قطعی را به یک حادثه‌ی امنیتی تبدیل می‌کند.

تشخیص: چرا مانیتورینگ هشدار نداد؟ معمولاً یا اصلاً نبود، یا فایل روی دیسک را می‌خواند در حالی که سرویس گواهی دیگری را سرو می‌کرد.

پیشگیری: اتوماسیون صدور، reload خودکار پس از تجدید، probe خارجی روی پورت واقعی با دو آستانه، مالک مشخص برای هر گواهی، و یک تست بازیابی فصلی. نکته‌ای که پاسخ را senior می‌کند: با روند رسیدن عمر گواهی به ۴۷ روز تا ۲۰۲۹، فرایند دستی از نظر ریاضی قابل دوام نیست.

0-RTT در TLS 1.3 چیست و چه خطری دارد؟

با استفاده از PSKای که از session قبلی مانده، کلاینت داده‌ی برنامه را همراه اولین پیام handshake می‌فرستد و صفر رفت‌وبرگشت تأخیر می‌دهد.

خطرش این است که این داده در برابر replay محافظت‌شده نیست، چون هنوز هیچ تبادل تازه‌ای یکتایی پیام را تضمین نکرده؛ مهاجم می‌تواند بسته را بارها بازپخش کند. قاعده: فقط برای درخواست‌های idempotent، عملاً GETهای قابل cache. سرور باید early data را علامت بزند (در HTTP هدر Early-Data: 1) و اپلیکیشن هر درخواست غیر idempotent را با 425 Too Early رد کند تا کلاینت آن را روی اتصال کامل تکرار کند.

certificate pinning کجا درست است و کجا فاجعه می‌شود؟

pinning یعنی محدود کردن اعتماد به یک کلید یا CA مشخص به‌جای کل trust store، برای دفاع در برابر صدور نادرست توسط یک CA معتبر.

در وب اشتباه است: HPKP از Chrome 72 حذف شد چون یک اشتباه پیکربندی، سایت را تا پایان max-age برای همه غیرقابل‌دسترس می‌کرد. جانشین آن Certificate Transparency است — لاگ‌های عمومی append-only که صدور غیرمجاز را قابل کشف می‌کنند بدون ریسک خودتخریبی.

در اپ موبایل و کلاینت ماشین‌به‌ماشین هنوز منطقی است چون هر دو سر را کنترل می‌کنی، به سه شرط: pin روی کلید عمومی نه گواهی، حداقل دو pin (فعلی + پشتیبان آفلاین)، و مسیر به‌روزرسانی سریع. بدون این‌ها pinning فقط یک بمب ساعتی برای روز چرخش کلید است.

GPG و X.509 هر دو کلید عمومی را به هویت وصل می‌کنند. تفاوت اصلی چیست؟

مدل اعتماد. X.509 سلسله‌مراتبی است: تعداد محدودی root CA در trust store و اعتماد از بالا به پایین. مقیاس‌پذیر و خودکار، ولی هر CA یک نقطه‌ی شکست است و یک CA دچار نفوذ می‌تواند برای هر دامنه‌ای گواهی صادر کند. OpenPGP افقی است: هیچ مرکزی وجود ندارد و هر کاربر خودش تصمیم می‌گیرد کدام کلید را باور کند، اغلب بر پایه‌ی امضای کسانی که به آن‌ها اعتماد دارد. نقطه‌ی شکست مرکزی ندارد ولی onboarding و کشف کلید سخت است — و به همین دلیل در وب هرگز نگرفت.

تفاوت کاربردی: TLS داده در حال حرکت را محافظت می‌کند و GPG داده در حال سکون را — فایل، ایمیل، artifact، commit — با امضایی که سال‌ها بعد و مستقل از هر کانالی قابل تأیید است.

چطور یک artifact دانلودشده را واقعاً تأیید می‌کنی؟ آیا «Good signature» کافی است؟

کافی نیست. Good signature فقط می‌گوید امضا با کلید عمومی موجود در keyring من جور است، نه اینکه آن کلید مال پروژه است؛ مهاجمی که آینه‌ی دانلود را دارد فایل، امضا و کلید خودش را می‌گذارد.

روال درست: (۱) اثر انگشت کامل ۴۰ رقمی کلید انتشار را از کانالی مستقل از محل دانلود بگیر، (۲) کلید را وارد کن و با gpg --fingerprint مقایسه کن، (۳) gpg --verify file.asc file را اجرا کن و هشدار «کلید تأیید نشده» را آگاهانه با --lsign-key برطرف کن. هرگز به key ID کوتاه ۸ یا ۱۶ رقمی اعتماد نکن.

همین منطق درباره‌ی امضای commit هم صدق می‌کند: امضا فقط امضاکننده را اثبات می‌کند، نه فیلد Author را که متن آزاد است. بدون branch protection و فهرست نگهداری‌شده‌ی کلیدهای مجاز، امضا فقط یک نشان سبز تزئینی است.

جمع‌بندی

گواهی چیزی جز یک ادعای امضاشده نیست: «این کلید عمومی متعلق به این نام‌هاست». تمام PKI ماشینی است برای دنبال کردن آن امضا تا یک root مورد اعتماد؛ و بیشتر خرابی‌های واقعی نه در رمزنگاری، بلکه در زنجیره‌ی ناقص، تاریخ انقضا و trust store اشتباه رخ می‌دهند.

TLS همان اعتماد را به یک اتصال زنده تبدیل می‌کند: نسخه‌ی 1.3 با یک رفت‌وبرگشت، forward secrecy اجباری و حذف کامل الگوریتم‌های شکسته. mTLS همان مکانیزم را دوطرفه می‌کند و برای هویت workload عالی است — به شرطی که چرخش را حل کرده باشی.

در جاوا فقط دو مفهوم را نگه دار: keystore هویت من، truststore کسانی که باور دارم. PKIX path building failed تقریباً همیشه یعنی یکی از این دو ناقص است، و پاسخش هرگز یک TrustManager بی‌قید نیست.

روند صنعت روشن است: عمر گواهی‌ها به سمت ۴۷ روز و کمتر می‌رود، ابطال جای خود را به انقضای سریع می‌دهد، و هر چیز دستی به‌زودی می‌شکند. اتوماسیون صدور، reload خودکار و مانیتورینگ از بیرون سه ستون بقا هستند. و در کنار همه‌ی این‌ها GPG لایه‌ای را پوشش می‌دهد که TLS هرگز نمی‌پوشاند: اعتماد به محتوا مستقل از کانال — امضای release، تأیید artifact، و امضای commit و tag.

Behind that little padlock next to a URL sits a multi-layer trust machine that most engineers only know one layer deep. Nobody asks questions while it works — but the moment this line shows up in a production log:

PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target

the difference between a mid and a senior engineer becomes visible. One copies a TrustManager that "accepts everything"; the other knows within thirty seconds which link of the chain is missing.

This chapter takes the machine apart from the first screw: what a certificate actually is, the handshake, the daily tooling, automated rotation in Kubernetes, and finally the parallel universe of PGP/GPG, which solves the same problem with a completely different philosophy.

Roadmap
  1. Foundations: public/private keys, hashing, signatures. 2. The certificate: X.509 fields, SAN, EKU. 3. Chain of trust: root, intermediate, leaf, trust stores. 4. Issuance lifecycle: CSR, expiry, the global shortening of certificate lifetimes. 5. Revocation: CRL, OCSP, stapling. 6. TLS: 1.2 vs 1.3, cipher suites, forward secrecy, SNI, ALPN, resumption, 0-RTT. 7. mTLS. 8. Java: keystore/truststore, keytool, cacerts, SSLContext, the classic errors. 9. openssl cookbook. 10. Operations: pinning, CT, ACME, Kubernetes. 11. GPG: web of trust, subkeys, signing artifacts and commits, keyservers, revocation.

Part 1 — Three foundations

The mailbox with a slot

A mailbox with a slot in the door: anyone can drop a letter in (public key), but only the holder of the metal key can open it (private key). Now invert it: if the owner has a personal seal that only they possess, anyone who sees the seal can confirm the letter came from them — without being able to forge it. The first is encryption, the second is signing.

Asymmetric cryptography means a key pair where the private key cannot be derived from the public one. Encrypt with the public key so only the private key opens it; sign with the private key so anyone can verify with the public one. Today's algorithms: RSA (slow, large keys, still everywhere), ECDSA on P-256/secp256r1 (fast, small), and Ed25519 (the most modern; the new OpenPGP default).

Hashing is a one-way function turning any input into a fixed-length fingerprint (SHA-256 = 256 bits). Flipping one input bit changes the whole output, and finding two inputs with the same hash (a collision) must be infeasible. MD5 and SHA-1 are both broken; that is why the JDK has disabled SHA-1 handshake signatures by default since version 25.

A digital signature = hash the document, "encrypt" that hash with the private key, put the result next to the document. Verification = hash it yourself, open the signature with the public key, compare. Equal hashes give you both integrity and authenticity.

All of PKI and all of GPG solve exactly one sentence: "whose public key is this really?" PKI answers "because a trusted authority vouched for it"; PGP answers "because people I trust vouched for it".


Part 2 — What a certificate really is

The passport

A passport is a piece of paper claiming "this photo and this name belong to a real person", with a government stamp on it. The border officer does not trust you; they trust the stamp, because they have a reference copy. A TLS certificate is exactly that: a document saying "this public key belongs to api.example.com", with a CA's signature on it.

A certificate is a standard X.509 v3 structure encoded with ASN.1/DER. Base64-wrap that DER between -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- and you get PEM — what you see inside .crt and .pem files.

Field Meaning Practical note
Serial Number Unique ID at the issuer Needed to revoke or escalate to the CA
Signature Algorithm e.g. sha256WithRSAEncryption If you see SHA-1, the certificate is dead
Issuer Issuer DN Must match the Subject of the certificate above it
Validity notBefore / notAfter The source of most production incidents
Subject Holder DN (CN=...) No longer used for hostname matching
SubjectPublicKeyInfo The public key plus its algorithm The thing actually being signed
Subject Alternative Name The real list of names/IPs The only source of truth for hostnames
Basic Constraints CA:TRUE/FALSE and pathlen What separates a CA from a leaf
Key Usage digitalSignature, keyCertSign What the key may be used for
Extended Key Usage serverAuth, clientAuth, codeSigning mTLS does not work without clientAuth
AKI / SKI Issuer key ID / own key ID How the chain is assembled
CRL Distribution Points Where the revocation list lives May be absent entirely
Authority Information Access OCSP URL and issuer certificate URL caIssuers can rescue an incomplete chain
SCT List Certificate Transparency receipts Browsers reject public certificates without them
# Fetch a server's certificate and read all of it
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -text

# Only what you need 90% of the time
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
`CN` is dead — look at the SAN

For years the domain went into Subject CN. Since roughly 2017 every major browser completely ignores CN for hostname matching and reads only subjectAltName. If you mint an internal certificate with just -subj "/CN=api.internal", browsers, modern curl and modern Java clients will reject it with a hostname mismatch. Always add -addext "subjectAltName=DNS:api.internal" as well.

A self-signed certificate means Issuer == Subject. Cryptographically it is perfectly valid and TLS works fine with it; the only problem is that nobody has a reason to trust it unless it is manually added to a trust store. Teams mint self-signed certificates for internal services and add an exception in each client; three months later they have forty certificates with no way to revoke or rotate anything. The right move is one internal CA: distribute its root once into every trust store, then issue a leaf per service — after that, rotating leaves requires zero client changes.


Part 3 — The chain of trust

A CEO does not sign every small contract; they delegate to deputies who sign instead. The counterparty sees the deputy's signature, checks the deputy's power of attorney, and arrives at the CEO's signature, of which they hold a reference copy. The root CA is the CEO, the intermediate is the deputy, and the server certificate is the contract.

  • Root CA: self-signed, key in an offline HSM, 15–25 year lifetime, and present in the OS/browser/JDK trust store.
  • Intermediate CA: signed by the root, online, issuing millions of certificates. A compromise burns only the intermediate, not the root.
  • Leaf: your server certificate, CA:FALSE, unable to sign anything.

Chain of trust from root to leaf — زنجیره از root تا برگ:

flowchart TD
  R["Root CA<br/>self-signed, offline HSM<br/>in OS/JDK trust store"]
  I["Intermediate CA<br/>online, signs millions<br/>sent by the server"]
  L["Leaf certificate<br/>SAN = api.example.com<br/>CA:FALSE"]
  K["Server private key<br/>never leaves the host"]
  R -->|signs| I
  I -->|signs| L
  L -.-> K

Golden rule: the server must send the leaf plus every intermediate, and must not send the root. The client uses AKI/Issuer to build the chain until it reaches a root in its own trust store.

The world's most classic TLS bug: the missing intermediate

The server sends only the leaf. It works in a desktop browser (which cached the intermediate or fetches it via AIA) but breaks in curl, in Java, in mobile apps and in backend services. The symptoms are unable to get local issuer certificate from openssl and PKIX path building failed from Java. The definitive test:

openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null
# You should see two or more CERTIFICATE blocks and, at the end: Verify return code: 0 (ok)

The fix: in nginx, ssl_certificate must point at the fullchain, not just cert.pem.

Environment Default trust store
Debian/Ubuntu /etc/ssl/certs/ca-certificates.crt (update-ca-certificates)
RHEL/Fedora /etc/pki/tls/certs/ca-bundle.crt (update-ca-trust)
JDK $JAVA_HOME/lib/security/cacerts (default password changeit)
Python/requests the certifi package
Node.js built-in list plus NODE_EXTRA_CA_CERTS
Java does not read the OS trust store

This one burns hours of debugging: you add the corporate CA with update-ca-certificates, curl works, and the Java application still throws PKIX path building failed because the JVM has its own cacerts. Some distributions keep the two in sync via a ca-certificates-java package, but official JDK Docker images usually do not.


Part 4 — CSR, issuance and expiry

A CSR is a signed request saying "here is my public key, here are the names I want, please sign". The CSR is signed with your own private key so the CA can confirm you actually hold it.

Certificate issuance flow — فرایند صدور گواهی:

sequenceDiagram
  participant S as Server / Ops
  participant CA as Certificate Authority
  participant DNS as DNS / HTTP endpoint
  S->>S: generate private key (never leaves)
  S->>S: build CSR (public key + SANs), self-sign it
  S->>CA: submit CSR
  CA->>DNS: domain control validation (HTTP-01 / DNS-01)
  DNS-->>CA: challenge satisfied
  CA->>CA: apply policy, add SCT, sign with intermediate key
  CA-->>S: leaf certificate + chain
  S->>S: install cert + chain, reload server

Three CSR truths beginners get wrong: (1) the private key is never in the CSR and must never be sent to the CA — if a provider generates the key for you, that key is no longer private. (2) The CA is not obliged to honour what is in the CSR; in practice everything except the public key and the SANs is decided by the CA. (3) Every key rotation needs a fresh CSR.

# 1) A modern private key (ECDSA P-256)
openssl ecparam -name prime256v1 -genkey -noout -out api.key
chmod 600 api.key

# 2) CSR with a SAN — note: -noenc replaces the old -nodes in OpenSSL 3.x
openssl req -new -key api.key -out api.csr \
  -subj "/CN=api.example.com/O=Example Inc" \
  -addext "subjectAltName=DNS:api.example.com,DNS:www.api.example.com"

# 3) Always read the CSR before sending it
openssl req -in api.csr -noout -text -verify

SAN, wildcards and picking a strategy

DNS:api.example.com is that exact name. DNS:*.example.com covers exactly one label: it matches a.example.com, does not match a.b.example.com, and does not match the apex example.com. For IPs you must write IP:10.0.0.5; putting an IP into a DNS entry does nothing.

Option Benefit The risk a senior should name
Single-name cert Smallest blast radius, independent rotation Many certificates → automation becomes mandatory
Multi-SAN cert One certificate for related services Leaks your internal domain list; everything rotates together
Wildcard No new issuance for a new subdomain One private key on every host; DNS-01 only
Short-lived (6-day) Revocation becomes essentially irrelevant Automation must be flawless
mTLS from a private CA Full control, no public CA cost You own rotation, CRLs and HSMs

Wildcards are tempting, but they mean the same private key gets copied to every machine serving any subdomain — from the load balancer down to that experimental service nobody patches. With ACME, issuing per name is essentially free. Keep wildcards for cases where subdomains are genuinely dynamic (multi-tenant naming) and on-demand issuance is impractical.

Expiry: the biggest recurring incident class in engineering

Public certificate lifetimes are shrinking fast. Per CA/Browser Forum ballot SC-081v3 (April 2025):

From Maximum public TLS certificate lifetime
Today 398 days
15 March 2026 200 days
15 March 2027 100 days
15 March 2029 47 days

Let's Encrypt has gone further: its shortlived profile, valid for 160 hours (just over six days), became generally available in January 2026, and it has announced that the default classic profile drops to 64 days in February 2027 and to 45 days in February 2028.

If renewal is manual, you own a scheduled outage

At 47-day lifetimes, "a calendar reminder" is not a process. You need three things: an inventory of every certificate with its expiry, automated renewal (ACME/cert-manager/Vault), and alerting independent of the issuing tool that connects to the port directly — because the common failure is not that issuance fails, it is that issuance succeeds while the service never reloads and keeps serving the old certificate.

If you keep that inventory in a database, the alerting query looks like this:

SELECT common_name, environment, not_after,
       (not_after::date - CURRENT_DATE) AS days_left
FROM   cert_inventory
WHERE  not_after < CURRENT_DATE + INTERVAL '21 days'
ORDER  BY not_after;
Dialect difference

In PostgreSQL subtracting two date values yields an integer number of days and INTERVAL is explicit; in Oracle subtracting two DATE values yields a fractional number of days, so TRUNC is required and adding a plain number to SYSDATE means days. If the Oracle column is a TIMESTAMP, the result becomes an INTERVAL DAY TO SECOND rather than a number — a classic trap.


Part 5 — Revocation: CRL, OCSP and stapling

Suppose a server's private key leaks while the certificate still has 40 days left. How do we tell the world it is no longer valid?

  • CRL: the CA publishes a signed list of revoked serial numbers (URL in CRL Distribution Points). It is bulky and must be re-downloaded regularly.
  • OCSP: the client asks a responder about one certificate and gets good/revoked/unknown. That is a round trip on the critical path plus a privacy leak — the CA learns which IP is visiting which site.
  • OCSP Stapling: the server itself fetches a fresh signed response from the CA and "staples" it into the handshake (the status_request extension). The client never contacts the CA.

OCSP stapling flow — جریان stapling:

sequenceDiagram
  participant C as Client
  participant S as Server
  participant O as OCSP responder (CA)
  Note over S,O: periodically, off the critical path
  S->>O: OCSP request for its own serial
  O-->>S: signed response (good, valid ~7 days)
  C->>S: ClientHello + status_request extension
  S-->>C: ServerHello + Certificate + stapled OCSP response
  C->>C: verify signature and freshness locally
Revocation barely works in practice — and the industry has accepted it

Most clients are soft-fail: if the responder is unreachable, the connection is accepted anyway. An attacker who controls the traffic only has to drop the response. Let's Encrypt shut its OCSP service down entirely on 6 August 2025 (primarily for privacy reasons) and moved to CRLs. The industry's real answer to revocation is short lifetimes: a six-day certificate carries no revocation information at all, because it expires faster than any CRL refresh.

The "must-staple" extension (OID 1.3.6.1.5.5.7.1.24) turns soft-fail into hard-fail: "if you do not see a stapled response, abort". Powerful but dangerous — a stapling failure then takes your site fully offline instead of merely slowing it down.

server {
    listen 443 ssl;
    http2 on;
    server_name api.example.com;

    ssl_certificate     /etc/ssl/api/fullchain.pem;   # leaf + intermediates
    ssl_certificate_key /etc/ssl/api/privkey.pem;

    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;                    # in TLS 1.3 the client chooses
    ssl_session_cache   shared:SSL:10m;
    ssl_session_tickets off;                          # unrotated ticket keys break forward secrecy

    ssl_stapling        on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/ssl/api/chain.pem;   # only to verify the OCSP response

    add_header Strict-Transport-Security "max-age=63072000" always;
}

Part 6 — TLS: the handshake, step by step

Two people in a crowded café want to talk privately, having never met: (1) verify who the other is via their passport, (2) agree on a shared secret language, (3) build a shared password neither of them ever transmitted in full, (4) speak only that language from now on. The handshake is exactly those four steps.

TLS 1.2 handshake (two round trips before data) — دست‌دادن TLS 1.2:

sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: ClientHello (versions, cipher suites, SNI, ALPN, random)
  S-->>C: ServerHello (chosen suite, random)
  S-->>C: Certificate (leaf + intermediates)
  S-->>C: ServerKeyExchange (ECDHE params, signed) + ServerHelloDone
  C->>S: ClientKeyExchange (ECDHE public share)
  C->>S: ChangeCipherSpec + Finished
  S-->>C: ChangeCipherSpec + Finished
  C->>S: Application data (HTTP request)

Cost: 2-RTT before the first byte of data. On a mobile link with 100 ms latency that is 200 ms of pure handshake, on top of TCP and DNS.

TLS 1.3 handshake (one round trip, encrypted early), RFC 8446 — دست‌دادن TLS 1.3:

sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: ClientHello + key_share (guessed group) + supported_versions + SNI/ALPN
  S-->>C: ServerHello + key_share
  Note over C,S: shared secret derived — everything below is encrypted
  S-->>C: {EncryptedExtensions, Certificate, CertificateVerify, Finished}
  C->>S: {Finished} + Application data
  S-->>C: {NewSessionTicket} for future resumption
Aspect TLS 1.2 TLS 1.3
RTT to first data 2 1 (0 with 0-RTT)
Forward secrecy Optional (only with ECDHE) Mandatory
Suite name TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_AES_128_GCM_SHA256
Signature algorithm negotiation Baked into the suite Independent (signature_algorithms)
Certificate on the wire Cleartext Encrypted
Resumption Session ID / ticket PSK only, via NewSessionTicket
RC4/3DES/CBC/RSA-kex Still negotiable Removed from the standard

TLS 1.3 got safer by deleting things: RSA key transport, static DH, compression, renegotiation and every non-AEAD suite are gone, so an entire family of attacks (ROBOT, CRIME, Logjam, FREAK) is simply not expressible any more.

Anatomy of a cipher suite

Read TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 piece by piece: ECDHE is the key exchange (that trailing E means ephemeral — a temporary key thrown away afterwards), RSA is the server's signature algorithm (i.e. the certificate key type), AES_128_GCM is the symmetric cipher in AEAD mode, and SHA256 is the hash used by the KDF. TLS 1.3 has exactly five suites and all are AEAD: TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_AES_128_CCM_SHA256, TLS_AES_128_CCM_8_SHA256.

Forward secrecy — why it matters

With TLS_RSA_WITH_... the client encrypted a random secret with the certificate's public key and sent it. So anyone who records traffic today and obtains the server's private key five years later can decrypt all of that old traffic. With ECDHE each connection derives its own ephemeral key that never travels on the wire; compromising the certificate key does not expose the past. TLS 1.3 makes this mandatory.

JDK 25 and 21.0.10 disabled the `TLS_RSA_*` suites

Starting with JDK 25 (September 2025) and the JDK 21.0.10 update (January 2026), the TLS_RSA_* suites — which have no forward secrecy — are disabled by default. If a connection to an old broker or service suddenly breaks after a JDK upgrade with no cipher suites in common or handshake_failure, this is why. The correct fix is moving the peer to ECDHE; rewriting jdk.tls.disabledAlgorithms is a painkiller that needs an expiry date.

SNI, ALPN, ECH and resumption

SNI sends the hostname in cleartext inside ClientHello so a server hosting a hundred domains on one IP knows which certificate to present; without it, HTTPS virtual hosting is impossible. ALPN, in the same message, declares that the client speaks h2/http/1.1/h3 — HTTP/2 over TLS does not start without it, and gRPC depends on it for the same reason. ECH is the next step: it encrypts ClientHello itself with a key published in DNS so even the SNI is hidden.

If a Java connection behind a proxy comes back with the wrong certificate, check SNI first: run openssl s_client once with -servername and once without; two different certificates means SNI is the problem.

Resumption lets a later connection reuse a PSK: Session ID (state kept on the server, painful across nodes) or Session Ticket (encrypted state held by the client, server stays stateless). If the ticket encryption key stays constant for years and then leaks, every past resumed session becomes decryptable — so rotate it frequently or set ssl_session_tickets off;.

0-RTT and replay attacks

In TLS 1.3 a client can send application data alongside its first message (early data). That data is not replay-protected: an attacker can capture the packet and resend it ten times. If it was POST /payments, that is ten transactions. Rule: 0-RTT only for idempotent requests. In nginx, enable ssl_early_data on; only when the application inspects the Early-Data: 1 header and rejects non-idempotent requests with 425 Too Early.


Part 7 — mTLS

In ordinary TLS only the server proves itself. In mTLS the server also demands a certificate from the client and runs the same chain validation on it.

mTLS handshake — both sides present certificates:

sequenceDiagram
  participant C as Client (has cert + key)
  participant S as Server (has cert + key)
  C->>S: ClientHello
  S-->>C: ServerHello + Certificate + CertificateVerify
  S-->>C: CertificateRequest (acceptable CAs, sig algs)
  C->>S: Certificate (client leaf + chain)
  C->>S: CertificateVerify (proves possession of client key)
  S->>S: verify chain against client-CA truststore + check EKU clientAuth
  S-->>C: Finished
Scenario Is mTLS right? Why
Human user in a browser No Certificate distribution on user devices is a nightmare; use OIDC
Service-to-service inside a mesh Yes Workload identity, automated issuance, short lifetimes
B2B partner, small fixed set Yes Bounded population, formal contract
Public API with thousands of consumers No Onboarding and rotation do not scale; use OAuth2
App to database/broker Usually yes Few, well-known clients; automatable rotation
mTLS is not a replacement for authorization

mTLS tells you who the peer is, not what it may do. The mature pattern: mTLS for workload authentication at the transport layer, plus a token (JWT/OAuth2) carrying the end-user's identity and permissions across services. The authorization model itself lives in the microservice security chapter; here, just note the two are complements, not competitors.

The big mTLS trap: synchronized rotation

If every client certificate is issued on the same day by the same script, they all die on the same day and the whole system stops at once. CA rotation is harder still: you need a transition window where both sides trust both CAs, then you move the leaves, and only then remove the old CA. Do it in the wrong order and you have locked yourself out of your own cluster.


Part 8 — The Java side

Wallet versus address book

A keystore is your wallet: it holds your own identity documents (certificate plus private key) and must be locked. A truststore is an address book: the list of parties you believe (CA certificates, no private keys at all). A client-only application often needs no keystore; a TLS server always needs one; in mTLS both sides need both.

Property JKS PKCS12 (.p12/.pfx)
Standard Sun/Oracle proprietary International standard (RFC 7292)
Portable across languages No Yes (openssl, .NET, browsers)
JDK default Up to JDK 8 JDK 9 onwards (JEP 229)
Today's recommendation Legacy compatibility only Always the default

keytool cheat sheet

Task Command
Key pair + self-signed cert keytool -genkeypair -alias api -keyalg EC -groupname secp256r1 -validity 365 -keystore ks.p12 -storetype PKCS12 -dname "CN=api.example.com" -ext "SAN=dns:api.example.com"
List contents keytool -list -v -keystore ks.p12
CSR from an existing key keytool -certreq -alias api -file api.csr -keystore ks.p12 -ext "SAN=dns:api.example.com"
Import the signed certificate keytool -importcert -alias api -file signed.crt -keystore ks.p12 -trustcacerts
Add a CA to a truststore keytool -importcert -alias corp-root -file corp-root.crt -keystore truststore.p12 -storetype PKCS12 -noprompt
Export to PEM keytool -exportcert -alias api -rfc -file api.pem -keystore ks.p12
Delete an entry keytool -delete -alias old-ca -keystore truststore.p12
Change the store password keytool -storepasswd -keystore ks.p12
Convert JKS to PKCS12 keytool -importkeystore -srckeystore old.jks -srcstoretype JKS -destkeystore new.p12 -deststoretype PKCS12
Grab a certificate from a server keytool -printcert -sslserver api.example.com:443
Read a certificate file keytool -printcert -file chain.pem
Show the JDK's active TLS settings keytool -showinfo -tls

Two classic keytool traps. First, when you import the CA-signed certificate, keytool must be able to build the whole chain; if the intermediate is neither in that keystore nor in cacerts you get Failed to establish chain from reply — import the intermediate first, or feed it a concatenated PEM chain. Second, the leaf must be imported under the same alias as the private key; under a different alias you create a separate trusted-certificate entry instead of completing the key entry, and the server keeps serving the old self-signed certificate.

cacerts and the corporate CA

# Is the CA already in the JDK's default truststore?
keytool -list -cacerts -storepass changeit | grep -i corp-root

# Add it; since JDK 9, -cacerts points straight at the default truststore
keytool -importcert -trustcacerts -noprompt -alias corp-root \
        -file corp-root.crt -cacerts -storepass changeit

Three ways to trust a private CA, best first: (1) a dedicated application truststore via -Djavax.net.ssl.trustStore=... — versionable, rotatable, no JDK surgery. (2) Adding it in the Dockerfile at build time — reproducible, but must re-run on every JDK upgrade. (3) Editing cacerts by hand on a server — gone on the next rollout; do not do this.

FROM eclipse-temurin:25-jre
COPY corp-root.crt /tmp/corp-root.crt
RUN keytool -importcert -trustcacerts -noprompt \
      -alias corp-root -file /tmp/corp-root.crt \
      -cacerts -storepass changeit \
 && rm /tmp/corp-root.crt

Building an SSLContext in code

public static SSLContext build(Path trustStore, char[] trustPass,
                               Path keyStore,   char[] keyPass) throws Exception {

    KeyStore ts = KeyStore.getInstance("PKCS12");
    try (InputStream in = Files.newInputStream(trustStore)) { ts.load(in, trustPass); }
    TrustManagerFactory tmf =
            TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(ts);

    KeyManager[] kms = null;                       // only needed for mTLS
    if (keyStore != null) {
        KeyStore ks = KeyStore.getInstance("PKCS12");
        try (InputStream in = Files.newInputStream(keyStore)) { ks.load(in, keyPass); }
        KeyManagerFactory kmf =
                KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(ks, keyPass);
        kms = kmf.getKeyManagers();
    }

    SSLContext ctx = SSLContext.getInstance("TLSv1.3");
    ctx.init(kms, tmf.getTrustManagers(), null);
    return ctx;
}

// HttpClient performs hostname verification for you
HttpClient client = HttpClient.newBuilder().sslContext(ctx).build();
A raw `SSLSocket` does not verify the hostname

The JDK's most dangerous default: HttpsURLConnection and java.net.http.HttpClient do hostname matching, but if you build a raw connection with SSLSocketFactory or SSLEngine, the JDK validates only the chain and never checks that the SAN matches the host you dialled — meaning any valid certificate from any CA for any domain is accepted. Fix it explicitly:

SSLSocket socket = (SSLSocket) ctx.getSocketFactory().createSocket("api.example.com", 443);
SSLParameters p = socket.getSSLParameters();
p.setEndpointIdentificationAlgorithm("HTTPS");   // without this line, no hostname check happens
socket.setSSLParameters(p);
socket.startHandshake();
The "trust everything" anti-pattern
// Never, not even "temporarily" — this turns TLS into unauthenticated encryption
TrustManager[] trustAll = { new X509TrustManager() {
    public void checkClientTrusted(X509Certificate[] c, String a) { }
    public void checkServerTrusted(X509Certificate[] c, String a) { }   // ← MITM welcome
    public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}};

This snippet should fail every code review; "temporary, just for testing" always reaches production. The correct alternative is adding your internal CA or mkcert root to a separate truststore.

The classic errors and their real fixes

Error message What it really means Fix
PKIX path building failed … The JVM could not reach a trusted root from the server certificate Server omits the intermediate (use fullchain) or the CA is not in the truststore (import it)
No name matching <host> found Chain is fine; the SAN does not match the hostname Fix the SAN; check SNI; put IPs in IP: not DNS:
CertificateExpiredException Certificate expired — or the machine clock is wrong Check date -u; in a container without NTP this error is misleading
no cipher suites in common No overlapping version or suite Inspect with -Djavax.net.debug=ssl:handshake; suspect the removal of TLS_RSA_*
Received fatal alert: bad_certificate The server rejected the client certificate (mTLS) Check the client chain, EKU=clientAuth, and the server's truststore
Failed to establish chain from reply The intermediate is missing while importing the leaf Import the intermediate first, or supply a PEM chain
UnrecoverableKeyException: Cannot recover key Store password differs from key password In PKCS12 keep both identical, or configure keyPassword separately
TrustAnchor … is not a CA certificate You imported a leaf as if it were a CA Import the real root/intermediate
# The fullest view of the handshake (verbose; test environments only)
java -Djavax.net.debug=ssl:handshake:verbose:trustmanager -jar app.jar
# Certification path building only
java -Djava.security.debug=certpath -jar app.jar
# Turn on revocation checking (off by default)
java -Dcom.sun.net.ssl.checkRevocation=true \
     -Djdk.tls.client.enableStatusRequestExtension=true -jar app.jar

If your own TLS server runs on the JVM, OCSP stapling support is off by default and is enabled with -Djdk.tls.server.enableStatusRequestExtension=true (JEP 249). On the client side jdk.tls.client.enableStatusRequestExtension is on by default, but the result is not enforced until com.sun.net.ssl.checkRevocation=true.

Spring Boot: SSL bundles

Since Spring Boot 3.1, instead of scattering keystore paths across ten properties you define a named bundle; since 3.2 it can hot-reload:

spring:
  ssl:
    bundle:
      pem:
        server:
          reload-on-update: true            # watches the files and rebuilds the bundle
          keystore:
            certificate: "file:/etc/tls/tls.crt"
            private-key: "file:/etc/tls/tls.key"
          truststore:
            certificate: "file:/etc/tls/ca.crt"
      jks:
        partner:
          keystore:
            location: "classpath:client.p12"
            password: "${CLIENT_P12_PASSWORD}"
            type: "PKCS12"
server:
  ssl:
    bundle: "server"
Why `reload-on-update` solves a big problem

In Kubernetes, cert-manager writes the fresh certificate into the same Secret and kubelet updates the mounted files — but the JVM read them exactly once at startup. Without a reload, the pod serves an expired certificate until its next restart. reload-on-update: true watches the files and follows symlinks, which is essential for Kubernetes Secrets. Without Spring Boot you need the equivalent: an X509ExtendedKeyManager that re-reads from disk, or simply a pod rollout at renewal time.


Part 9 — The openssl cookbook

Goal Command
ECDSA P-256 key openssl ecparam -name prime256v1 -genkey -noout -out key.pem
RSA 2048 key openssl genrsa -out key.pem 2048
Ed25519 key openssl genpkey -algorithm ed25519 -out key.pem
Key + CSR in one shot openssl req -newkey rsa:2048 -noenc -keyout key.pem -out req.csr -subj "/CN=x"
Quick self-signed with SAN openssl req -x509 -newkey rsa:2048 -noenc -keyout key.pem -out cert.pem -days 90 -subj "/CN=x" -addext "subjectAltName=DNS:x"
Read a certificate openssl x509 -in cert.pem -noout -text
Dates only / SAN only openssl x509 -in cert.pem -noout -dates · ... -ext subjectAltName
SHA-256 fingerprint openssl x509 -in cert.pem -noout -fingerprint -sha256
Read a CSR openssl req -in req.csr -noout -text -verify
Probe a server openssl s_client -connect host:443 -servername host -showcerts
Force a TLS version openssl s_client -connect host:443 -tls1_2 (or -tls1_3)
Test ALPN openssl s_client -connect host:443 -alpn h2 -servername host
Test mTLS openssl s_client -connect host:8443 -cert client.crt -key client.key -CAfile ca.crt
Verify a chain offline openssl verify -CAfile root.pem -untrusted intermediate.pem leaf.pem
PEM → PKCS12 openssl pkcs12 -export -inkey key.pem -in cert.pem -certfile chain.pem -name api -out bundle.p12
PKCS12 → PEM openssl pkcs12 -in bundle.p12 -noenc -out all.pem
DER ↔ PEM openssl x509 -inform DER -in cert.der -out cert.pem
Throwaway test server openssl s_server -accept 8443 -cert cert.pem -key key.pem -www
Read a CRL openssl crl -in crl.der -inform DER -noout -text
Manual OCSP query openssl ocsp -issuer chain.pem -cert leaf.pem -url http://ocsp.example.com -text

Three combinations you will use constantly:

# 1) Does this private key match this certificate? (both outputs must be identical)
openssl x509 -noout -pubkey -in cert.pem | openssl sha256
openssl pkey  -pubout      -in key.pem   | openssl sha256

# 2) What chain is the server actually sending?
openssl s_client -connect api.example.com:443 -servername api.example.com \
  -showcerts </dev/null 2>/dev/null | grep -E "^(depth|verify|s:|i:)"

# 3) A complete private CA in 60 seconds (for dev and integration tests)
openssl req -x509 -newkey rsa:4096 -noenc -days 3650 -keyout ca.key -out ca.crt \
  -subj "/CN=Dev Root CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"

openssl req -newkey rsa:2048 -noenc -keyout svc.key -out svc.csr -subj "/CN=svc.local"

openssl x509 -req -in svc.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out svc.crt -days 30 -sha256 \
  -extfile <(printf "subjectAltName=DNS:svc.local,IP:127.0.0.1\nextendedKeyUsage=serverAuth,clientAuth\nbasicConstraints=CA:FALSE")

openssl verify -CAfile ca.crt svc.crt      # → svc.crt: OK

A version note: -nodes (short for "no DES") is deprecated since OpenSSL 3.0 and its official replacement is -noenc. Both mean "do not encrypt the private key" — so remember chmod 600 and correct ownership, and in production pull the key from a secret manager rather than a repository.


Part 10 — Pinning, Certificate Transparency and ACME

Pinning means "I accept only this public key (or this CA), even if another legitimate CA issues a perfectly valid certificate for the same domain". Its purpose is defending against mis-issuance by a CA. It has three levels: pinning the leaf certificate (breaks on every renewal — almost always wrong), pinning the leaf public key or SPKI (more stable, but it means you never rotate the key), and pinning the CA/intermediate (most stable, weakest protection).

# Compute an SPKI-style pin for a server
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null \
  | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der \
  | openssl dgst -sha256 -binary | openssl enc -base64
HPKP is dead, and pinning on the web is a mistake

The Public-Key-Pins header was removed in Chrome 72 because of its self-destruction risk — a site that loses its key becomes unreachable for everyone until max-age expires. Its successor Expect-CT was also removed once CT became mandatory in Chrome 107. On the web, the industry's answer is Certificate Transparency: every public certificate must be logged in append-only public logs and carry SCT receipts, so a domain owner can spot unauthorized issuance within minutes.

Pinning still makes sense in mobile apps and machine-to-machine clients, where you control both ends. Even there: always keep at least two pins (current key plus an offline backup) and a fast update path, otherwise pinning is just a time bomb set for rotation day.

ACME (RFC 8555) fully automates issuance: the client proves control of the domain and the CA issues — no humans, no forms.

ACME issuance with HTTP-01 challenge — صدور با ACME:

sequenceDiagram
  participant A as ACME client (certbot)
  participant CA as ACME server (Let's Encrypt)
  participant W as Your web server
  A->>CA: newOrder for api.example.com
  CA-->>A: challenge token + authorization URL
  A->>W: publish token at /.well-known/acme-challenge/TOKEN
  A->>CA: ready to be validated
  CA->>W: GET http://api.example.com/.well-known/acme-challenge/TOKEN
  W-->>CA: token + account key thumbprint
  CA-->>A: authorization valid
  A->>CA: finalize with CSR
  CA-->>A: certificate + chain
Challenge How it proves control Limitation
HTTP-01 A file on port 80 under /.well-known/acme-challenge/ Port 80 must be reachable from the internet; cannot issue wildcards
DNS-01 A TXT record on _acme-challenge.<domain> Needs DNS provider API access; the only wildcard path
TLS-ALPN-01 A special certificate on port 443 with ALPN acme-tls/1 Requires exclusive use of 443; suits reverse proxies
# Issue via webroot (the service keeps running)
certbot certonly --webroot -w /var/www/html \
  -d api.example.com -d www.api.example.com \
  --agree-tos -m ops@example.com --non-interactive

# Wildcards require DNS-01
certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cf.ini \
  -d "*.example.com" -d example.com

# Six-day certificate (the shortlived profile — Certbot 4.0 and later)
certbot certonly --preferred-profile shortlived --webroot -w /var/www/html -d api.example.com

# Dry-run renewal without burning rate limits, and reload after every success
certbot renew --dry-run
certbot renew --deploy-hook "systemctl reload nginx"
A successful renewal does not mean the service serves the new certificate

The most common ACME incident is not a failed renewal; it is a successful renewal where nginx or the JVM never re-reads the file and keeps serving the old certificate right up to expiry. Always wire a --deploy-hook (or reload-on-update, or a rollout), and monitor expiry from outside, on the real port rather than by reading a file on disk.

When building the automation, always start on staging — Let's Encrypt enforces rate limits and repeated failures will lock you out:

certbot certonly --server https://acme-staging-v02.api.letsencrypt.org/directory ...

Part 11 — Certificate rotation in Kubernetes

The standard pattern is cert-manager: a controller that understands Issuer/ClusterIssuer and Certificate resources, obtains the certificate, writes it into a kubernetes.io/tls Secret, and renews automatically before expiry.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ops@example.com
    profile: tlsserver          # ACME profile selection — cert-manager v1.18+
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: api-tls
  namespace: production
spec:
  secretName: api-tls            # tls.crt and tls.key land here
  duration: 2160h                # 90d
  renewBefore: 720h              # renew 30d before expiry
  privateKey:
    algorithm: ECDSA
    size: 256
    rotationPolicy: Always       # generate a new key on every renewal
  dnsNames:
    - api.example.com
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer

Up to cert-manager v1.17 the default rotationPolicy was Never — the certificate was renewed but the same old private key lived forever. From v1.18 the default is Always, so both key and certificate change at every renewal.

cert-manager rotation and pod reload — چرخش و بارگذاری مجدد:

flowchart LR
  C["Certificate CR<br/>renewBefore: 30d"] --> CM["cert-manager controller"]
  CM -->|ACME order| CA["Let's Encrypt"]
  CA -->|cert + chain| CM
  CM -->|writes| S["Secret kubernetes.io/tls<br/>tls.crt, tls.key"]
  S -->|projected volume| P["Pod filesystem /etc/tls"]
  P -->|file watcher / SIGHUP| APP["App reloads SSL context"]
  S -.->|read directly| ING["Ingress controller"]
The real weak point: the pod never re-reads the file

kubelet does update mounted Secret files (with up to about a minute of delay), but it does so with a symlink swap rather than writing into the same inode — so watchers that only put an inotify watch on the file itself never see the change. Three options: use a library that follows symlinks (Spring Boot's reload-on-update does); run a hook that sends SIGHUP when the Secret changes; or, simplest and most reliable, trigger a rollout at renewal time via an annotation carrying the certificate hash.

In service meshes, workload certificates typically live for hours or less and are held in the sidecar's memory — they never touch disk. Identity is not a DNS name but a SPIFFE ID such as spiffe://cluster.local/ns/production/sa/api, carried in a URI-type SAN. Mesh architecture belongs to the microservice runtime chapter; the point here is that the same X.509 chain sits underneath it.


Part 12 — GPG/PGP: same problem, different philosophy

The wax seal versus the notary office

In the X.509 world, to make your signature meaningful you go to a notary (a CA) and everyone trusts the notary. In the PGP world there is no notary: you make your own personal seal, and your friends stamp their seal onto yours. If I trust someone and they have signed your seal, I can trust your seal too. That horizontal network is the web of trust.

PGP is a standard (OpenPGP) and GnuPG/gpg is its most common free implementation; the current development branch is adding post-quantum encryption (Kyber/ML-KEM).

Aspect X.509 / TLS OpenPGP / GPG
Trust model Hierarchical; root CAs Horizontal; web of trust or manual trust
Identity Domain name (SAN) Name and email (User ID)
Main use Live connections (transport) Data at rest: files, email, artifacts, commits
Revocation CRL/OCSP published by the CA A revocation certificate published by the key owner
Expiry Mandatory and short Optional; extendable without changing the key
Key structure One key per certificate One primary key plus role-specific subkeys
Why still learn GPG?

Because TLS protects the channel, not the content. When you download a .tar.gz from a mirror, HTTPS only says "these bytes arrived unmodified from that server" — not "these bytes were actually produced by the project team". A GPG signature on the artifact closes that gap, which is why virtually every Linux distribution, package repository and serious open-source release depends on it. Channel security versus content security is a standard platform-interview question.

Key structure: primary and subkeys

flowchart TD
  P["Primary key (Ed25519)<br/>usage: Certify — keep offline"]
  S["Signing subkey<br/>usage: Sign"]
  E["Encryption subkey (Curve25519)<br/>usage: Encrypt"]
  A["Authentication subkey<br/>usage: Auth (SSH)"]
  U["User IDs — Name and email"]
  P -->|certifies| S
  P -->|certifies| E
  P -->|certifies| A
  P -->|binds| U

The primary key is your identity: it certifies your User IDs and subkeys and signs other people's keys. Subkeys do the day-to-day work. The big win: lose your laptop and you revoke and replace only the subkeys, while your identity and every signature ever collected on the primary survive. The senior pattern is to keep the primary offline (encrypted drive or a hardware token) and carry only subkeys on the working machine — gpg shows that state as sec#.

# Create a key; future-default today means ed25519 for signing + cv25519 for encryption
gpg --quick-generate-key "Jane Dev <jane@example.com>" future-default default 2y
gpg --quick-add-key <FINGERPRINT> ed25519 sign 1y      # a dedicated signing subkey

gpg --list-keys --keyid-format=long --with-subkey-fingerprints
gpg --list-secret-keys --keyid-format=long

gpg --armor --export jane@example.com > jane-public.asc          # to hand to others
gpg --armor --export-secret-subkeys jane@example.com > subs.asc  # working backup
gpg --import colleague-public.asc

gpg --quick-set-expire <FINGERPRINT> 2y '*'            # extend all subkeys
GPG key expiry is not TLS certificate expiry

If your GPG key expires you do not need a new key: just push the expiry date forward and republish the updated public key — every signature and all accumulated trust survive. The classic beginner mistake is seeing expired and generating a brand-new key, throwing away the whole web of trust. Here the expiry date is a dead-man switch: if you ever lose access to the key, it retires itself.

Encrypting, signing and verifying

# Encrypt for several recipients plus yourself (otherwise you cannot open it either)
gpg --encrypt -r bob@example.com -r audit@example.com -r jane@example.com \
    --output report.pdf.gpg report.pdf
gpg --decrypt report.pdf.gpg > report.pdf

gpg --symmetric --cipher-algo AES256 backup.tar     # symmetric, passphrase only
gpg --clear-sign announcement.txt                   # human-readable inline signature

# Detached signature — the release-artifact standard
gpg --armor --detach-sign app-1.4.0.tar.gz          # → app-1.4.0.tar.gz.asc
gpg --verify app-1.4.0.tar.gz.asc app-1.4.0.tar.gz

Detached signatures are the release standard because the file itself stays untouched: the same .tar.gz any tool can read, plus a small .asc beside it. Embed the signature inside the file and everyone is forced through gpg just to reach the content.

"Good signature" with a warning is only half the job

Good signature only says the signature matches the public key in your keyring. The not certified with a trusted signature warning means "but I have no idea whether that public key belongs to the project". An attacker who controls the mirror can serve their own file, their own signature and their own public key, and you will still see Good signature. The only way to close the loop is to obtain the full 40-hex-digit fingerprint from a channel independent of the download and then locally sign the key:

gpg --keyserver hkps://keys.openpgp.org --recv-keys <FULL_40_HEX_FINGERPRINT>
gpg --fingerprint <FULL_40_HEX_FINGERPRINT>    # compare against the independent source
gpg --lsign-key <FINGERPRINT>                  # local signature, never published

Never trust a short key ID (8 or 16 hex digits); manufacturing a key with a colliding short ID is cheap and practical.

Keyservers, WKD and revocation

The old SKS keyservers let anyone attach any signature to any key and never deleted anything, which led to "key poisoning" — keys with tens of thousands of junk signatures that broke gpg outright. That network is effectively abandoned. keys.openpgp.org is the new model: identity information is only searchable after email verification and with the owner's consent, and third-party signatures are stripped. WKD serves the public key directly from the person's own domain over HTTPS under /.well-known/openpgpkey/... — PGP borrowing trust from PKI.

gpg --keyserver hkps://keys.openpgp.org --send-keys <FINGERPRINT>
gpg --locate-external-keys jane@example.com     # via WKD and keyservers
gpg --refresh-keys                              # pick up revocations and expiry changes
The revocation certificate must exist *before* the incident

If you lose the private key or forget the passphrase, you can no longer produce a revocation certificate, and your dead key stays valid on keyservers forever. gpg 2.x automatically writes one to ~/.gnupg/openpgp-revocs.d/<FPR>.rev at key creation; print it or store it somewhere safe and separate from the key. Note the flip side: anyone holding that file can revoke your key — a trivial denial of service against your identity.

gpg --output revoke.asc --gen-revoke <FINGERPRINT>   # create it today, not on the bad day
gpg --import revoke.asc && gpg --keyserver hkps://keys.openpgp.org --send-keys <FINGERPRINT>

Signing git commits and tags

git config --global user.signingkey <SIGNING_SUBKEY_FPR>!   # the ! means "exactly this subkey"
git config --global commit.gpgsign true
git config --global tag.gpgsign true
export GPG_TTY=$(tty)                                       # otherwise no passphrase prompt

git commit -S -m "Fix token refresh race"
git tag -s v1.4.0 -m "Release 1.4.0"
git verify-commit HEAD && git verify-tag v1.4.0
git log --pretty="%h %G? %GS %s" -10    # %G? = G(ood) B(ad) U(ntrusted) N(one) E(rror)

Since Git 2.34 you can use an SSH key instead, with git config --global gpg.format ssh and user.signingkey ~/.ssh/id_ed25519.pub. It is simpler, but it has no expiry or revocation model and verification depends on a gpg.ssh.allowedSignersFile you maintain yourself. For signatures that must still verify years later, OpenPGP remains the more mature choice.

Note that a signature does not prove authorship, only who signed: the git Author field is free text anyone can set. A meaningful policy means branch protection requiring verified signatures plus a maintained list of allowed keys; without that, the signature is just a decorative green badge.

gpg cheat sheet

Task Command
Interactive key generation gpg --full-generate-key
Quick generation gpg --quick-generate-key "Name <mail>" future-default default 2y
Add a subkey gpg --quick-add-key <FPR> ed25519 sign 1y
List keys gpg --list-keys --keyid-format=long · gpg --list-secret-keys
Fingerprint gpg --fingerprint <FPR>
Export public / secret gpg --armor --export <FPR> · gpg --armor --export-secret-keys <FPR>
Import gpg --import pub.asc
Encrypt / decrypt gpg -e -r bob@example.com -o f.gpg f · gpg -d f.gpg > f
Symmetric encryption gpg -c --cipher-algo AES256 f
Detached / inline signature gpg --armor --detach-sign f · gpg --clear-sign f
Verify gpg --verify f.asc f
Locally sign a key gpg --lsign-key <FPR>
Set trust level gpg --edit-key <FPR> then trust
Extend expiry gpg --quick-set-expire <FPR> 2y '*'
Generate a revocation cert gpg --output rev.asc --gen-revoke <FPR>
Send / receive from keyserver gpg --keyserver hkps://keys.openpgp.org --send-keys <FPR> · --recv-keys <FPR>
Locate via WKD gpg --locate-external-keys mail@example.com
Delete keys gpg --delete-keys <FPR> · gpg --delete-secret-keys <FPR>
Non-interactive use in CI gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 ...
GPG in CI

A pipeline has neither a terminal nor a human to type a passphrase. The safe pattern: create a CI-only signing subkey with a short lifetime (never the primary), store it in a secret manager, import it in the job, and wipe the temporary GNUPGHOME afterwards. If it leaks you revoke only that subkey.

export GNUPGHOME=$(mktemp -d)
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
echo "$GPG_PASSPHRASE" | gpg --batch --yes --pinentry-mode loopback \
    --passphrase-fd 0 --armor --detach-sign dist/app-1.4.0.tar.gz
rm -rf "$GNUPGHOME"

Part 13 — The senior operations playbook

  • Keep an inventory. Every certificate, its owner, where it is deployed and when it expires. If you cannot say within thirty seconds which certificate expires next, expiry will ambush you.
  • Monitor from the outside. A probe that connects to the real port catches both expiry and the far more common "renewed but never reloaded" failure.
  • Use two alert thresholds: soft at 30% of remaining lifetime (ticket), hard at 10% (page). On a 47-day certificate that is 14 and 5 days.
  • Keep private keys out of the repository. If a key ever entered git history it is burned; it must be revoked and replaced, not merely deleted from the commit.
  • Rehearse rotation. Once a quarter, rotate the CA in staging. A team that has never practised CA rotation will be doing it for the first time on incident day.
  • Take clocks seriously. A container without NTP produces exactly the errors that look like certificate problems; date -u is the first command of any TLS investigation.
  • Move algorithms forward. Today that means ECDSA P-256 or Ed25519 for new certificates, TLS 1.2 as the floor and 1.3 as the default. The next step is post-quantum: the hybrid X25519MLKEM768 key exchange is enabled by default from OpenSSL 3.5 (April 2025), and the JDK has shipped ML-KEM and ML-DSA since version 24. "Harvest now, decrypt later" is a genuine threat to long-lived data.

Part 14 — Interview questions

What exactly is a certificate, and what does it prove?

An X.509 structure binding a public key to a set of names in the SAN, with a CA's signature over that claim. It contains nothing secret and is entirely public.

It proves: "the holder of the corresponding private key controlled these names at issuance time, and a CA verified that". It does not prove that the site is malware-free, that the company is legitimate, or that your data is handled safely on the other side. For DV certificates — the overwhelming majority on the internet — the only thing validated is domain control.

You hit `PKIX path building failed` in production. Walk me through it.

First I separate an incomplete server chain from an incomplete client truststore, using openssl s_client -connect host:443 -servername host -showcerts. If I only see one CERTIFICATE block, the server is not sending the intermediate → the fix is replacing cert.pem with fullchain.pem and reloading.

If the chain is complete but openssl also reports unable to get local issuer, the root is missing from the system. If openssl and curl both succeed and only Java fails, it is almost always the CA missing from cacerts, because the JVM does not use the OS trust store. I confirm with -Djava.security.debug=certpath and then import into a dedicated application truststore rather than hand-editing cacerts, which disappears at the next rollout.

Red flag in a candidate's answer: "I'd write a TrustManager that accepts everything".

What is the difference between a keystore and a truststore? Which does a client need?

A keystore holds your own identity: certificate plus private key. A truststore holds the CA certificates you believe, with no private keys. Format-wise both can be a PKCS12 file; the difference is the role, not the file type.

A plain HTTPS client only needs a truststore (usually the default cacerts suffices); a TLS server always needs a keystore; in mTLS both sides need both. A common mistake is dumping CA certificates into the same keystore that holds the server key — it works, but server-key rotation and CA-list rotation are two completely different cycles and should stay separate.

What did TLS 1.3 change relative to 1.2?

Speed: the handshake went from 2-RTT to 1-RTT, because the client guesses the key-exchange group and sends key_share up front in ClientHello; with a PSK, 0-RTT becomes possible.

Security by deletion: RSA key transport, static DH, renegotiation, compression and all non-AEAD suites were removed; the suite count dropped from hundreds to five, and signature-algorithm negotiation was split out of the suite. A whole family of attacks (ROBOT, CRIME, Logjam, FREAK) is no longer expressible.

Privacy: everything after ServerHello is encrypted, including the server certificate, which travelled in cleartext under 1.2. The remaining exposure is SNI, which ECH is designed to close.

Explain forward secrecy and why `TLS_RSA_*` was removed.

In TLS_RSA_WITH_... suites the client encrypted a pre-master secret with the certificate's public key, tying the whole session's security to the server's long-term key. Record the traffic today, obtain that key years later, and all the past traffic decrypts.

With ECDHE every session derives its own ephemeral key pair which never travels on the wire and is discarded afterwards; the long-term key is only used to sign, never to transport a secret. The practical detail: JDK 25 and the 21.0.10 update disable TLS_RSA_* by default, which is exactly why connections to legacy systems suddenly fail with no cipher suites in common.

Compare CRL, OCSP and stapling. What would you recommend today?

A CRL is a signed list of revoked serials — bulky and always slightly stale. OCSP is a live query about one certificate — fresher, but it adds a round trip on the critical path and tells the CA who is visiting which site. Stapling moves that response to the server, which attaches it during the handshake: no added latency and no privacy leak.

The root problem for all three: clients are soft-fail, so an attacker only needs to block the response. Recommendation: enable stapling because it is cheap, but do not treat it as a serious security control. The industry's real answer is short lifetimes — Let's Encrypt shut down its OCSP service in August 2025 and its six-day certificates carry no revocation information at all.

Why is `CN` no longer used for hostname matching, and what are wildcard limits?

CN is part of the Distinguished Name and is just a string; subjectAltName is a typed, multi-valued extension (DNS, IP, URI). Since about 2017 browsers ignore CN for validation, and public CAs are required to repeat any CN name in the SAN.

A wildcard covers exactly one label: *.example.com matches api.example.com, does not match a.b.example.com, and does not match the apex example.com — the apex must be listed separately in the SAN. In ACME, wildcards can only be issued via DNS-01. The architectural risk: one shared private key on every host serving any subdomain.

When is mTLS the right choice, and what does it not solve?

It fits when you control both ends and the client population is automatable: service-to-service inside a cluster, apps connecting to databases and brokers, and a defined set of B2B partners. It does not fit browser users or public APIs with thousands of consumers, because distributing and rotating certificates on devices you do not control is impractical.

mTLS is transport-layer authentication, not authorization; knowing the peer is payment-service says nothing about what it may access. The main operational trap is rotation: certificates issued together die together, and CA rotation requires a transition window where both sides trust both CAs.

An internal service certificate expired at 2 a.m. and ten services went down. What do you do?

Contain: the fastest recovery is usually issuing a fresh certificate from the same CA and reloading. If the CA is unreachable, a short-lived certificate from an internal CA beats disabling TLS validation in clients — that turns an outage into a security incident.

Diagnose: why did monitoring not warn? Usually either there was none, or it read a file on disk while the service was serving something else.

Prevent: automated issuance, automated reload after renewal, an external probe on the real port with two thresholds, a named owner per certificate, and a quarterly recovery drill. The detail that makes the answer senior: with lifetimes heading to 47 days by 2029, a manual process is mathematically unsustainable.

What is 0-RTT in TLS 1.3 and what is the risk?

Using a PSK left over from a previous session, the client sends application data alongside its very first handshake message, giving zero round trips of latency.

The risk is that this data is not replay-protected, because no fresh exchange has yet guaranteed message uniqueness; an attacker can replay the packet repeatedly. The rule is: idempotent requests only — in practice cacheable GETs. The server must flag early data (in HTTP, the Early-Data: 1 header) and the application must reject any non-idempotent request with 425 Too Early so the client retries on the fully established connection.

Where is certificate pinning right, and where is it a disaster?

Pinning restricts trust to a specific key or CA instead of the whole trust store, defending against mis-issuance by a legitimate CA.

On the web it is wrong: HPKP was removed in Chrome 72 because one configuration mistake made a site unreachable for everyone until max-age expired. Its successor is Certificate Transparency — append-only public logs that make mis-issuance detectable without any self-destruction risk.

In mobile apps and machine-to-machine clients it still makes sense, under three conditions: pin the public key rather than the certificate, keep at least two pins (current plus an offline backup), and have a fast update path. Without those, pinning is just a time bomb set for rotation day.

GPG and X.509 both bind a public key to an identity. What is the core difference?

The trust model. X.509 is hierarchical: a bounded set of root CAs in the trust store, trust flowing top-down. It scales and automates well, but every CA is a single point of failure — one compromised CA can issue for any domain. OpenPGP is horizontal: there is no centre, and each user decides which keys to believe, often based on signatures from people they already trust. There is no central point of failure, but onboarding and key discovery are hard — which is why it never took over the web.

The functional difference: TLS protects data in motion, GPG protects data at rest — files, email, artifacts, commits — with a signature that is verifiable years later and independent of any channel.

How do you actually verify a downloaded artifact? Is "Good signature" enough?

It is not. Good signature only says the signature matches a public key already in my keyring, not that the key belongs to the project; an attacker controlling the mirror serves their own file, signature and key.

The correct routine: (1) obtain the full 40-hex-digit fingerprint of the release key from a channel independent of the download, (2) import the key and compare with gpg --fingerprint, (3) run gpg --verify file.asc file and deliberately resolve the "key not certified" warning with --lsign-key. Never trust a short 8- or 16-digit key ID.

The same logic applies to commit signing: a signature proves who signed, not the Author field, which is free text. Without branch protection and a maintained list of allowed keys, the signature is only a decorative green badge.

Wrap-up

A certificate is nothing more than a signed claim: "this public key belongs to these names". All of PKI is the machinery for following that signature up to a trusted root — and most real failures happen not in the cryptography but in incomplete chains, expiry dates and the wrong trust store.

TLS turns that trust into a live connection: 1.3 with one round trip, mandatory forward secrecy and the wholesale removal of broken algorithms. mTLS makes the same mechanism mutual and is excellent for workload identity — provided you have solved rotation.

In Java, hold on to just two ideas: the keystore is who I am, the truststore is who I believe. PKIX path building failed almost always means one of those two is incomplete, and the answer is never a permissive TrustManager.

The industry's direction is unambiguous: lifetimes are heading to 47 days and below, revocation is being replaced by fast expiry, and anything manual will break soon. Automated issuance, automated reload and external monitoring are the three pillars of survival. Alongside all of that, GPG covers the layer TLS never will: trust in the content independent of the channel — signing releases, verifying artifacts, and signing commits and tags.