TLS-এর নিরাপত্তা-গ্যারান্টি — কী রক্ষা করে, কীভাবে ভাঙে
TLS Security Properties, Forward Secrecy, and Downgrade Attacks
TLS handshake-এর যান্ত্রিকতা আগের লেসনেই দেখা হয়েছে — আজকের প্রশ্ন সেই যন্ত্র আসলে কী প্রতিশ্রুতি দেয়, সেই প্রতিশ্রুতি কোথায় কোথায় ভাঙে (validation-skip, downgrade, Heartbleed), আর কেন ephemeral key exchange ছাড়া কোনো প্রতিশ্রুতিই ভবিষ্যতে টেকে না।
আগে এটা বুঝি
আগের নেটওয়ার্কিং লেসনে (networking/tls-handshake) পুরো TLS 1.3 handshake-টা ধাপে ধাপে দেখা হয়েছে — ClientHello-র key_share, ECDHE shared secret, HKDF দিয়ে traffic key তৈরি, certificate chain পাঠানো, Finished-এর transcript-MAC। সেই মেশিনটা এখানে আবার বানানো হবে না — আজকের প্রশ্ন সম্পূর্ণ ভিন্ন। মেশিনটা তো ঘোরে, কিন্তু ঠিক কী প্রতিশ্রুতি দেয় সেই ঘোরাটা? আর সেই প্রতিশ্রুতি বাস্তবে কোথায় কোথায় ভেঙেছে?
এই দুইটা প্রশ্নই আজকের কেন্দ্র। প্রথমটা দার্শনিক শোনালেও একেবারে বাস্তব — একটা padlock-icon দেখে একজন ডেভেলপার বা ব্যবহারকারী কী কী ধরে নিতে পারবেন, আর কী কী কখনোই ধরে নেওয়া উচিত না, সেই সীমারেখা আঁকা। আর দ্বিতীয়টা ইতিহাস — POODLE, Heartbleed-এর মতো নাম যা শুধু trivia না, প্রতিটাই TLS-এর একটা নির্দিষ্ট প্রতিশ্রুতি ঠিক কোথায় ভেঙেছিল তার case study।
মূল ধারণা
TLS ঠিক কী প্রতিশ্রুতি দেয় — আর কাকে
TLS handshake-এর “hood” আগের লেসনে দেখা গেছে; এখানে শুধু ফলাফলের টেবিলটা নিয়ে আসি, কারণ এটাই আজকের ভিত্তি:
| গ্যারান্টি | কাকে দেয় | mechanism |
|---|---|---|
| Confidentiality | দুই পক্ষ, symmetric key যাদের হাতে পৌঁছেছে | AEAD encryption (AES-GCM/ChaCha20-Poly1305) |
| Integrity | দুই পক্ষ | AEAD authentication tag + Finished-এর transcript MAC |
| Server authenticity | client-কে — “আমি যার সাথে কথা বলছি, সে দাবিকৃত domain-টাই” | certificate chain + hostname/SAN match |
| Client authenticity | ঐচ্ছিক — শুধু mTLS-এ, যেখানে client-ও নিজের certificate পাঠায় | client certificate + signature (rare, mostly service-to-service) |
লক্ষ্য করুন শেষ সারিটা — client authentication default-এ নেই। একজন ব্যবহারকারী browser খুলে একটা ব্যাংকের সাইটে ঢুকলে, ব্যাংকের server নিশ্চিত হয় সে “bank.example.com”-এর সাথেই কথা বলছে (server authenticity), কিন্তু bank-এর server TLS-স্তরে নিশ্চিত না যে client-এর ওপাশে সত্যিই সঠিক মানুষ বসে আছে — সেই কাজ application-স্তরের authentication (password, OTP, session token) করে, TLS না। mTLS একটা ব্যতিক্রম — সাধারণত internal microservice-থেকে-microservice communication-এ, যেখানে দুই পক্ষই একে অপরের identity গাণিতিকভাবে প্রমাণ করতে চায়।
এবার প্রশ্নটা উল্টে জিজ্ঞেস করি — TLS কী রক্ষা করে না:
| যা TLS রক্ষা করে না | কেন |
|---|---|
| Endpoint safety | Device-এ malware থাকলে, encryption শেষ হয় app-এর ভেতরে ঢোকার আগেই — plaintext তো app নিজেই handle করে |
| Server-side data-at-rest | TLS শুধু transit-এ সুরক্ষা দেয়; server-এর database-এ password কীভাবে store হচ্ছে তার সাথে TLS-এর কোনো সম্পর্ক নেই |
| Application-layer logic bugs | SQL injection, XSS, broken authorization — এগুলো encrypted channel-এর ভেতর দিয়েই ঘটে, TLS-এর দেখারও কথা না |
| Site-এর সততা (honesty) | একটা phishing domain-ও সম্পূর্ণ বৈধ certificate পেতে পারে — CA শুধু domain-control validate করে, মালিকের উদ্দেশ্য না |
| Metadata privacy | DNS lookup (DoH/DoT ছাড়া) আর SNI field আগের লেসনে দেখা গেছে — plaintext, তাই কোন domain-এ যাচ্ছেন তা observer-এর কাছে প্রায়ই দৃশ্যমান |
| Availability | TLS DDoS আটকায় না — একটা flooded connection-এর জন্মই হয় না, handshake শুরু হওয়ার সুযোগ পায় না |
ভেতরে কী ঘটছে
Certificate validation যেখানে ভাঙে
আগের লেসনে চেইন-অফ-ট্রাস্ট কীভাবে কাজ করে তার গণিত (root → intermediate
→ leaf, প্রতিটা ধাপে signature verify) দেখা হয়েছে — সেই toy PKI-র
browser_validate_chain() ফাংশনটা মনে করুন। আজকের প্রশ্ন — সেই validation
বাস্তবে কোথায় কোথায় স্কিপ হয়ে যায়, বা ঠিকভাবে চললেও কোথায় অপর্যাপ্ত।
ব্যর্থতা ১ — validation একেবারেই স্কিপ করা। সবচেয়ে সরল, সবচেয়ে
বিধ্বংসী ভুল। কোনো client লাইব্রেরি বা mobile app যদি
verify=False-জাতীয় flag দিয়ে certificate check বন্ধ করে দেয় (সাধারণত
development-এ self-signed cert নিয়ে ঝামেলা এড়াতে, তারপর production-এ থেকে
যায়), তাহলে পুরো authenticity-গ্যারান্টি শূন্য — একজন active MITM নিজের
তৈরি করা যেকোনো certificate নিয়ে হাজির হতে পারে, TLS session সম্পূর্ণ
স্বাভাবিক দেখাবে, কিন্তু ওপাশে attacker বসে থাকবে। এটা কোনো cryptographic
দুর্বলতা না — একটা একক if শর্ত মুছে ফেলার ভুল, যা পুরো লেসনের গণিতকে
অর্থহীন করে দেয়।
ব্যর্থতা ২ — expired certificate। Validity window (notBefore থেকে
notAfter) পার হয়ে গেলে certificate আর বৈধ না — তার signature গাণিতিকভাবে
এখনও সঠিক থাকতে পারে, কিন্তু “এতদিন পর্যন্ত বৈধ” শর্তটাই এখন মিথ্যা। এটা
নরম সমস্যা মনে হলেও operationally মারাত্মক — networking/tls-handshake
লেসনে Ericsson (২০১৬)-এর ঘটনার কথা আছে, যেখানে expired certificate একবারে
লক্ষাধিক subscriber-এর phone-data বন্ধ করে দিয়েছিল।
ব্যর্থতা ৩ — revoked certificate, কিন্তু client জানে না। এটাই সবচেয়ে কাঠামোগতভাবে কঠিন ব্যর্থতা, কারণ certificate ইস্যু হওয়ার পরে যদি তার private key leak হয় (বা CA ভুল domain-কে ইস্যু করে ফেলে), সেই certificate validity-শেষ হওয়ার অনেক আগেই বিপজ্জনক — অথচ গাণিতিকভাবে সবকিছু এখনও ঠিকঠাক verify করবে। দুইটা পুরনো প্রক্রিয়া এই সমস্যার সমাধানের চেষ্টা করেছে:
CRL (Certificate Revocation List)
CA পর্যায়ক্রমে পুরো revoked-certificate-এর তালিকা প্রকাশ করে
সমস্যা: তালিকা বড়, download খরচ, client কতটা fresh রাখবে তার নিশ্চয়তা নেই
OCSP (Online Certificate Status Protocol)
client প্রতিটা connection-এ CA-কে জিজ্ঞেস করে: "এই serial-number টা কি revoked?"
সমস্যা ১ — latency: প্রতিটা handshake-এ একটা extra network round-trip
সমস্যা ২ — privacy: CA জানতে পারে client কোন site-এ কখন visit করছে
সমস্যা ৩ — fail-open বনাম fail-closed: CA-সার্ভার unreachable হলে কী করবে?
fail-open (চালিয়ে যাও) attacker-এর জন্য trivially bypassable
(শুধু OCSP-request block করে দিলেই হলো), fail-closed (আটকাও)
মানে CA-র downtime = পুরো internet-এর সেই সাইটগুলো downOCSP stapling এই তিনটা সমস্যারই একটা ব্যবহারিক সমাধান — client নিজে CA-কে জিজ্ঞেস করে না, বরং server নিজেই পর্যায়ক্রমে CA থেকে একটা signed, timestamped “আমি revoked না” প্রমাণ (OCSP response) সংগ্রহ করে রাখে, আর TLS handshake-এর সময় সেটা certificate-এর সাথেই “staple” করে client-কে পাঠিয়ে দেয়। ফলাফল — client-কে আর আলাদা করে CA-কে জিজ্ঞেস করতে হয় না (latency আর privacy সমস্যা দুটোই মিটে গেল), আর যেহেতু response-টা নিজেই signed আর timestamped, client শুধু সেটা যাচাই করে দেখে “এই response কি সাম্প্রতিক আর বৈধ” — CA-সার্ভার নিজে reachable কিনা তার ওপর প্রতি-handshake নির্ভরতা নেই।
Certificate pinning — আরও কঠোর, কিন্তু দামী। ওপরের সবকিছুই ধরে নেয় trust store-এর সব root CA সমান বিশ্বাসযোগ্য। Certificate pinning সেই ধারণাটাকেই প্রশ্ন করে — একটা app hardcode করে রাখে “example.com-এর জন্য আমি শুধু এই নির্দিষ্ট public key (বা এই নির্দিষ্ট CA) মেনে নেব, বাকি সব root CA যতই trust store-এ থাকুক না কেন।” এটা DigiNotar-জাতীয় compromised-CA আক্রমণ থেকে অতিরিক্ত সুরক্ষা দেয় — একটা fraudulently-issued (কিন্তু গাণিতিকভাবে সম্পূর্ণ বৈধ) certificate-ও pinning ব্যর্থ করে দেয়, কারণ pin-করা key-র সাথে মেলে না।
উদাহরণ
Forward secrecy — একটা সংজ্ঞা যা ভবিষ্যতের দিকে তাকায়
এতক্ষণ সব আলোচনা “আজকের” নিরাপত্তা নিয়ে — একটা connection আজ কনফিডেনশিয়াল কিনা। কিন্তু একটা গভীরতর প্রশ্ন আছে: আজকের encrypted session, ভবিষ্যতেও কি নিরাপদ থাকবে? ধরুন একজন attacker আজ একটা TLS session-এর পুরো ciphertext record করে রাখল, decrypt করতে পারল না — কিন্তু ধৈর্য ধরে সেটা সংরক্ষণ করে রাখল। এক বছর পরে, কোনো bug বা আক্রমণে server-এর দীর্ঘমেয়াদী (long-term) private key leak হয়ে গেল। প্রশ্ন — সেই leaked key দিয়ে কি এক বছর আগের recorded session-টা এখন decrypt করা যাবে?
উত্তরটা নির্ভর করে handshake-এ কোন ধরনের key exchange ব্যবহার হয়েছিল তার ওপর — আর এই পার্থক্যটাই Perfect Forward Secrecy (PFS)-এর পুরো বিষয়বস্তু।
পুরনো মোড — static RSA key-transport (TLS 1.2-এর একটা cipher-suite পরিবার, TLS 1.3-এ সম্পূর্ণ বাদ)। এখানে client একটা random premaster secret বেছে নিয়ে সরাসরি server-এর long-term, স্থায়ী RSA public key দিয়ে encrypt করে পাঠায়। Session key derive হয় সেই premaster secret থেকে। লক্ষ্য করুন — সেই ciphertext decrypt করার একমাত্র উপায় server-এর ঠিক সেই একই static private key, যা বছরের পর বছর অপরিবর্তিত থাকে।
নতুন মোড — ephemeral (EC)DHE। এখানে client আর server প্রত্যেকেই প্রতিটা নতুন session-এর জন্য একটা সম্পূর্ণ নতুন, সাময়িক Diffie-Hellman keypair জন্মায়, public value বিনিময় করে shared secret বানায়, আর handshake শেষ হতেই সেই ephemeral private value মুছে ফেলে। Server-এর static (দীর্ঘমেয়াদী) key শুধু certificate-এর signature verify করতে ব্যবহৃত হয় — session key derive করতে না।
নিচের experiment-এ এই পার্থক্যটা সংখ্যায় চালিয়ে দেখানো হবে — একই “future compromise” আক্রমণ দুই মোডেই চেষ্টা করা হবে, একটায় সফল, একটায় ব্যর্থ।
- আজ: TLS session হলোattacker পুরো wire traffic record করে রাখল, এখনই decrypt করতে পারছে না
- static RSA key-transport মোডsession key = decrypt(ciphertext, server_static_private_key) — সরাসরি নির্ভরশীল
- ephemeral (EC)DHE মোডsession key = f(ephemeral_a, ephemeral_b) — server_static_private_key-র সাথে কোনো algebraic সম্পর্কই নেই
- কয়েক মাস পরে: server-এর static private key leak (Heartbleed-স্টাইল)attacker-এর হাতে এখন সেই key
- RSA মোডে ফলাফলleaked key দিয়ে সরাসরি decrypt(ciphertext) — recorded session আজ খুলে গেল
- DHE মোডে ফলাফলleaked static key দিয়ে ephemeral secret বের করার কোনো পথ নেই — ephemeral value আগেই মুছে গেছে, session চিরকাল অখণ্ড
ছবিতে
Downgrade attack — নেগোসিয়েশনকেই আক্রমণ করা
Certificate validation ধরে নেয় attacker কোনো একটা certificate জাল করার চেষ্টা করছে। Downgrade attack ভিন্ন কৌশল নেয় — certificate বা key নিয়ে কিছুই না করে, বরং handshake-এর শুরুতেই, দুই পক্ষ কোন protocol version ব্যবহার করবে সেই আলোচনাটাকেই কারচুপি করে।
স্বাভাবিক negotiation:
Client: "আমি TLS 1.3, 1.2, 1.1, 1.0 — সবগুলো জানি"
Server: "আমিও 1.3 জানি" → negotiated: TLS 1.3 (সবচেয়ে শক্তিশালী, দুই পক্ষই সমর্থন করে)
active MITM version-stripping:
Client পাঠায়: "আমি TLS 1.3, 1.2, 1.1, 1.0 — সবগুলো জানি"
MITM কেটে দেয়: "আমি শুধু TLS 1.0 জানি" ← attacker message-টাই বদলে দিল, দুই পক্ষই টের পেল না
Server ভাবে ক্লায়েন্ট শুধু TLS 1.0-ই জানে → negotiated: TLS 1.0 (দুর্বলতম উপলব্ধ)২০১৪-এ POODLE attack এই কৌশলটাই বাস্তবায়ন করেছিল, শুধু TLS 1.0 না — আরও পুরনো, ভাঙা SSL 3.0-এ। SSL 3.0-এ CBC-mode-এর একটা padding-oracle দুর্বলতা ছিল (block cipher mode-এর গত লেসনের ভাষায় — padding byte manipulate করে, response-এর timing/error দেখে ধীরে ধীরে plaintext বের করা)। সমস্যাটা এই না যে সব browser/server সরাসরি SSL 3.0 পছন্দ করত — বরং তখনকার browser-গুলোর একটা “compatibility fallback” আচরণ ছিল: modern version-এ handshake fail করলে (যা active MITM ইচ্ছাকৃতভাবে ঘটাতে পারত), browser চুপচাপ SSL 3.0-এ retry করত। MITM শুধু প্রতিটা আধুনিক handshake-attempt-কে ব্যর্থ করে দিত, browser বাধ্য হয়ে SSL 3.0-এ নামত, আর তখন padding-oracle attack চালানো যেত।
TLS 1.3-এর design-level জবাব — downgrade-protection sentinel। পুরনো
fallback-এর ফাঁদ থেকে শিক্ষা নিয়ে, TLS 1.3-সক্ষম একটা server যদি কোনো
কারণে (client-এর প্রকৃত সীমাবদ্ধতা হোক বা active-stripping হোক — server
নিজে জানে না কেন) TLS 1.3-এর নিচে negotiate করতে বাধ্য হয়, তাহলে সে
ServerHello.random-এর শেষ ৮ বাইটে একটা নির্দিষ্ট magic marker বসিয়ে দেয়
(RFC 8446 §4.1.3)। একটা client যে নিজে TLS 1.3 জানে আর offer করেছিল, সে এই
marker দেখে বুঝে ফেলে — “আমি তো 1.3 বলেছিলাম, তবু negotiated version নিচে,
আর marker-টাও আছে — এটা আসল সীমাবদ্ধতা না, কেউ পথে টেম্পার করেছে” —
আর handshake abort করে। কৌশলটা সুন্দর কারণ এর জন্য কোনো নতুন cryptographic
primitive লাগেনি — শুধু negotiation-প্রক্রিয়ার নিজের মধ্যেই একটা
tamper-evidence বসানো হয়েছে।
নিজে চালিয়ে দেখুন
Forward secrecy — সংখ্যায়: static RSA বনাম ephemeral DHE, ভবিষ্যতের key-leak-এর মুখে
# forward_secrecy.py — static RSA key-transport বনাম ephemeral DHE: forward secrecy সংখ্যায়
import random, math
random.seed(20260827) # deterministic
def is_prime(n, rounds=25):
if n < 2:
return False
for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37):
if n % p == 0:
return n == p
d, r = n - 1, 0
while d % 2 == 0:
d //= 2
r += 1
for _ in range(rounds):
a = random.randrange(2, n - 1)
x = pow(a, d, n)
if x in (1, n - 1):
continue
for _ in range(r - 1):
x = pow(x, 2, n)
if x == n - 1:
break
else:
return False
return True
def gen_prime(bits):
while True:
cand = random.getrandbits(bits) | (1 << (bits - 1)) | 1
if is_prime(cand):
return cand
def gen_rsa_keypair(bits=140):
p, q = gen_prime(bits), gen_prime(bits)
while p == q:
q = gen_prime(bits)
n = p * q
phi = (p - 1) * (q - 1)
e = 65537
while math.gcd(e, phi) != 1:
e += 2
d = pow(e, -1, phi)
return (n, e), (n, d)
print("=== দৃশ্য: server-এর একটাই দীর্ঘমেয়াদী (long-term) RSA keypair ===")
server_pub, server_priv = gen_rsa_keypair(bits=140)
n, e = server_pub
print(f"server public (n, e) = (n, {e})")
print(f"server private (n, d) = (n, {server_priv[1]})")
print()
print("=== মোড A: static RSA key-transport (TLS 1.2-স্টাইল non-(EC)DHE cipher suite) ===")
premaster_secret = 918273645918273645918273645918273645 # client নিজে বেছে নেয়, encrypt করে পাঠায়
ciphertext = pow(premaster_secret, e, n) # server-এর PUBLIC key দিয়ে encrypt — eavesdropper এই ciphertext-টাই তারের ওপর দেখে
print(f"client-chosen premaster_secret = {premaster_secret}")
print(f"wire-এ যা দেখা যায় (ciphertext) = {ciphertext}")
print("--- eavesdropper আজ এই ciphertext record করে রাখল, decrypt করতে পারল না ---")
print()
print(">>> মাসখানেক পরে: Heartbleed-স্টাইল বাগে server-এর PRIVATE key leak হয়ে গেল <<<")
_, d = server_priv
recovered_premaster = pow(ciphertext, d, n) # leaked private key দিয়ে সরাসরি decrypt
print(f"leaked private key দিয়ে decrypt করা premaster = {recovered_premaster}")
print(f"recovered_premaster == original premaster_secret : {recovered_premaster == premaster_secret}")
print("-> মাসখানেক আগের RECORDED session আজ পুরোপুরি খুলে গেল — NO forward secrecy")
print()
print("=== মোড B: ephemeral Diffie-Hellman (DHE/ECDHE-স্টাইল) — প্রতি session নতুন keypair ===")
# ছোট কিন্তু বাস্তব একটা DH গ্রুপ (demo readability-র জন্য); নীতিটা আসল ECDHE-র মতোই
P = 2**127 - 1 # Mersenne prime, toy DH modulus
G = 5 # generator
def dhe_session(rng):
a = rng.randrange(2, P - 1) # client-এর EPHEMERAL private — শুধু এই session-এর জন্য জন্মালো
b = rng.randrange(2, P - 1) # server-এর EPHEMERAL private — এটাও শুধু এই session-এর জন্য
A = pow(G, a, P) # wire-এ যায় — public
B = pow(G, b, P) # wire-এ যায় — public
shared_client = pow(B, a, P)
shared_server = pow(A, b, P)
assert shared_client == shared_server
return A, B, shared_client, a, b
rng = random.Random(777)
A1, B1, shared1, a1, b1 = dhe_session(rng)
print(f"session-এর ephemeral public values (wire-এ eavesdropper যা দেখে):")
print(f" A (client ephemeral pub) = {A1}")
print(f" B (server ephemeral pub) = {B1}")
print(f"উভয়ে independently derive করা shared secret = {shared1}")
print("--- handshake শেষ, a আর b এখন মুছে ফেলা হলো — কোথাও persist করা হয়নি ---")
print()
print(">>> একই ভবিষ্যৎ compromise: server-এর STATIC RSA private key (d) leak হয়ে গেল <<<")
print("প্রশ্ন: এই leaked d দিয়ে কি recorded (A1, B1) থেকে shared1 বের করা যায়?")
# attacker-এর হাতে যা আছে: A1, B1 (wire থেকে record করা), আর leaked RSA d (সম্পূর্ণ ভিন্ন keypair!)
# চেষ্টা: leaked d-কে যদি DH exponent হিসেবেই ব্যবহার করা হয় (attacker-এর একমাত্র "leverage")
attempted_with_leaked_key = pow(B1, d % (P - 1), P)
print(f"leaked RSA d-কে DH exponent হিসেবে ব্যবহার করে চেষ্টা = {attempted_with_leaked_key}")
print(f"আসল shared secret = {shared1}")
print(f"মিলল কি? {attempted_with_leaked_key == shared1}")
print("-> মেলেনি, মেলার কথাও না: RSA-এর (n,d) আর DH-এর (P,G,a,b) সম্পূর্ণ ভিন্ন গাণিতিক জগৎ,")
print(" কোনো algebraic সেতু নেই একটা থেকে আরেকটায় যাওয়ার। আর a1,b1 নিজেরাই তো আর কোথাও নেই —")
print(f" ephemeral private ছিল a1={a1 % 10**6}...(truncated), মেমরি থেকে মুছে ফেলা হয়েছিল বহু আগেই।")
print("-> RECORDED session আজও অখণ্ড secret — THIS is forward secrecy.")প্রত্যাশিত output (deterministic — fixed seed, হুবহু এটাই আসবে):
=== দৃশ্য: server-এর একটাই দীর্ঘমেয়াদী (long-term) RSA keypair ===
server public (n, e) = (n, 65537)
server private (n, d) = (n, 949494565488193816245992901836306878304361487986690869626906411305048986558339542577)
=== মোড A: static RSA key-transport (TLS 1.2-স্টাইল non-(EC)DHE cipher suite) ===
client-chosen premaster_secret = 918273645918273645918273645918273645
wire-এ যা দেখা যায় (ciphertext) = 1019917704547232985747791863339700791306870880699124183933634898200908201656111638848
--- eavesdropper আজ এই ciphertext record করে রাখল, decrypt করতে পারল না ---
>>> মাসখানেক পরে: Heartbleed-স্টাইল বাগে server-এর PRIVATE key leak হয়ে গেল <<<
leaked private key দিয়ে decrypt করা premaster = 918273645918273645918273645918273645
recovered_premaster == original premaster_secret : True
-> মাসখানেক আগের RECORDED session আজ পুরোপুরি খুলে গেল — NO forward secrecy
=== মোড B: ephemeral Diffie-Hellman (DHE/ECDHE-স্টাইল) — প্রতি session নতুন keypair ===
session-এর ephemeral public values (wire-এ eavesdropper যা দেখে):
A (client ephemeral pub) = 101514752845321082441675040262365088659
B (server ephemeral pub) = 1240798560331113092962339618518417002
উভয়ে independently derive করা shared secret = 156075397110015933774173141260053219708
--- handshake শেষ, a আর b এখন মুছে ফেলা হলো — কোথাও persist করা হয়নি ---
>>> একই ভবিষ্যৎ compromise: server-এর STATIC RSA private key (d) leak হয়ে গেল <<<
প্রশ্ন: এই leaked d দিয়ে কি recorded (A1, B1) থেকে shared1 বের করা যায়?
leaked RSA d-কে DH exponent হিসেবে ব্যবহার করে চেষ্টা = 71323048854273724170887485213844140819
আসল shared secret = 156075397110015933774173141260053219708
মিলল কি? False
-> মেলেনি, মেলার কথাও না: RSA-এর (n,d) আর DH-এর (P,G,a,b) সম্পূর্ণ ভিন্ন গাণিতিক জগৎ,
কোনো algebraic সেতু নেই একটা থেকে আরেকটায় যাওয়ার। আর a1,b1 নিজেরাই তো আর কোথাও নেই —
ephemeral private ছিল a1=148293...(truncated), মেমরি থেকে মুছে ফেলা হয়েছিল বহু আগেই।
-> RECORDED session আজও অখণ্ড secret — THIS is forward secrecy.সংখ্যাগুলোই বার্তা দেয়: দুইটা মোডেই আজকের session সমানভাবে “encrypted”
মনে হয়েছিল — attacker কোনোটাই আজ decrypt করতে পারেনি। পার্থক্যটা ফুটে
উঠল শুধু ভবিষ্যতের compromise-এ। Mode A-তে leaked key সরাসরি
pow(ciphertext, d, n)-এ বসিয়ে পুরনো secret ফেরত পাওয়া গেল — কারণ session
key mathematically derived ছিল সেই একই static key থেকে। Mode B-তে
leaked key দিয়ে কিছুই মেলেনি, কারণ shared1 কখনোই d-এর ওপর নির্ভর করেনি
— শুধু random, session-specific a1, b1-এর ওপর, যা handshake শেষ হতেই
হারিয়ে গেছে। এইজন্যই TLS 1.3 static RSA key-transport সম্পূর্ণ বাদ দিয়ে
(EC)DHE-কে বাধ্যতামূলক করেছে — forward secrecy এখন একটা optional
সুবিধা না, প্রোটোকলের ডিফল্ট আচরণ।
একই future compromise (server-এর static private key leak) দুইটা মোডে সম্পূর্ণ ভিন্ন পরিণতি দেয় — static RSA key-transport-এ leaked key দিয়ে সরাসরি recorded traffic decrypt হয়ে যায়, কিন্তু ephemeral DHE-তে সেই একই leaked key দিয়ে কিছুই বের করা যায় না, কারণ ephemeral private value আগেই মুছে ফেলা হয়েছে আর তার সাথে static key-র কোনো algebraic সম্পর্কই নেই।
নিজে বানান
downgrade_sim.py — active MITM version-stripping বনাম TLS 1.3-এর sentinel
- client_hello() লিখুন যা সমর্থিত version-এর একটা তালিকা রাখে
- mitm_strip_above() লিখুন — একটা active attacker-এর মতো ClientHello থেকে উঁচু version-অফার কেটে ফেলুন
- server_hello() লিখুন — client-এর (সম্ভবত tampered) offer থেকে সর্বোচ্চ মিলে যাওয়া version negotiate করুন, আর TLS-1.3-সক্ষম server negotiated version < 1.3 হলে ServerHello.random-এর শেষ বাইটগুলোয় sentinel বসান
- client_verify() লিখুন — client নিজে যদি TLS 1.3 offer করে থাকে, তবু negotiated version নিচে আর sentinel উপস্থিত থাকলে ABORT করুন
- তিনটা দৃশ্য চালান — honest negotiation, active MITM downgrade, আর genuinely-legacy client — আর দেখান শুধু মাঝেরটাতেই ABORT আসে
লক্ষ্য: শুধু “downgrade-protection কাজ করে” বলাই না, একটা active MITM-এর version-stripping trick আর TLS 1.3-এর sentinel-based পাল্টা কৌশল দুটোই কোডে বাস্তবায়ন করে দেখানো — আর দেখানো যে genuinely পুরনো client (যে নিজেই TLS 1.3 জানে না) মিথ্যা alarm তৈরি করে না, শুধু active tampering-ই ধরা পড়ে।
# downgrade_sim.py — active MITM version-stripping বনাম TLS 1.3-এর downgrade-protection sentinel
import hashlib
# ভার্সন-অর্ডার — POODLE-এর দুনিয়া (SSL 3.0) থেকে আজকের TLS 1.3 পর্যন্ত
ORDER = ["SSLv3", "TLS1.0", "TLS1.1", "TLS1.2", "TLS1.3"]
RANK = {v: i for i, v in enumerate(ORDER)}
# RFC 8446 §4.1.3 — TLS 1.3-সক্ষম server negotiated version < 1.3 হলে
# ServerHello.random-এর শেষ ৮ বাইটে এই magic sentinel বসায়, কারণ downgrade
# টা আসল client-limitation নাকি active-stripping — server নিজে জানে না।
SENTINEL_IF_NEGOTIATED_TLS12 = bytes.fromhex("444f574e4752440100")[:8]
SENTINEL_IF_NEGOTIATED_TLS11_OR_LOWER = bytes.fromhex("444f574e4752440000")[:8]
def client_hello(supported_versions):
return {"supported_versions": list(supported_versions)}
def mitm_strip_above(hello, ceiling):
"""Active attacker ClientHello থেকে ceiling-এর উপরের সব version-offer কেটে দেয়।"""
kept = [v for v in hello["supported_versions"] if RANK[v] <= RANK[ceiling]]
return {"supported_versions": kept}
def server_hello(seen_client_versions, server_max, server_is_tls13_capable):
negotiated = max(seen_client_versions, key=lambda v: RANK[v])
rnd = bytearray(hashlib.sha256(negotiated.encode()).digest())
if server_is_tls13_capable and negotiated != "TLS1.3":
if negotiated == "TLS1.2":
rnd[-8:] = SENTINEL_IF_NEGOTIATED_TLS12
else:
rnd[-8:] = SENTINEL_IF_NEGOTIATED_TLS11_OR_LOWER
return {"negotiated_version": negotiated, "random_tail": bytes(rnd[-8:])}
def client_verify(original_hello, srv_hello):
negotiated = srv_hello["negotiated_version"]
tail = srv_hello["random_tail"]
sentinel_seen = tail in (SENTINEL_IF_NEGOTIATED_TLS12, SENTINEL_IF_NEGOTIATED_TLS11_OR_LOWER)
client_offered_13 = "TLS1.3" in original_hello["supported_versions"]
if client_offered_13 and negotiated != "TLS1.3" and sentinel_seen:
return "ABORT", "client নিজে TLS1.3 offer করেছিল, তবু sentinel-সহ নিচু version এলো — downgrade ধরা পড়ল"
return "CONTINUE", f"negotiated {negotiated}, sentinel নেই বা প্রত্যাশিতই ছিল — স্বাভাবিক"
print("=== দৃশ্য ১: honest network, MITM নেই ===")
hello = client_hello(["TLS1.3", "TLS1.2", "TLS1.1", "TLS1.0"])
sh = server_hello(hello["supported_versions"], server_max="TLS1.3", server_is_tls13_capable=True)
verdict, why = client_verify(hello, sh)
print(f"client offered : {hello['supported_versions']}")
print(f"server negotiated : {sh['negotiated_version']}")
print(f"client verdict : {verdict} — {why}")
print()
print("=== দৃশ্য ২: active MITM — ClientHello থেকে TLS1.3/TLS1.2 কেটে দিল (POODLE-স্টাইল) ===")
original_hello = client_hello(["TLS1.3", "TLS1.2", "TLS1.1", "TLS1.0"])
tampered_hello = mitm_strip_above(original_hello, ceiling="TLS1.0")
print(f"client আসলে offer করেছিল : {original_hello['supported_versions']}")
print(f"server যা দেখল (MITM-tampered) : {tampered_hello['supported_versions']}")
sh2 = server_hello(tampered_hello["supported_versions"], server_max="TLS1.3", server_is_tls13_capable=True)
print(f"server negotiated : {sh2['negotiated_version']}")
print(f"ServerHello.random শেষ ৮ বাইট : {sh2['random_tail'].hex()}")
verdict2, why2 = client_verify(original_hello, sh2)
print(f"client verdict : {verdict2} — {why2}")
print()
print("=== দৃশ্য ৩: TLS 1.3 ছাড়াই legacy server (never learned the sentinel trick) ===")
legacy_hello = client_hello(["TLS1.2", "TLS1.1", "TLS1.0"])
sh3 = server_hello(legacy_hello["supported_versions"], server_max="TLS1.2", server_is_tls13_capable=False)
verdict3, why3 = client_verify(legacy_hello, sh3)
print(f"client offered : {legacy_hello['supported_versions']} (নিজেই TLS1.3 জানে না)")
print(f"server negotiated : {sh3['negotiated_version']}")
print(f"client verdict : {verdict3} — {why3}")প্রত্যাশিত output (deterministic — হুবহু এটাই আসবে):
=== দৃশ্য ১: honest network, MITM নেই ===
client offered : ['TLS1.3', 'TLS1.2', 'TLS1.1', 'TLS1.0']
server negotiated : TLS1.3
client verdict : CONTINUE — negotiated TLS1.3, sentinel নেই বা প্রত্যাশিতই ছিল — স্বাভাবিক
=== দৃশ্য ২: active MITM — ClientHello থেকে TLS1.3/TLS1.2 কেটে দিল (POODLE-স্টাইল) ===
client আসলে offer করেছিল : ['TLS1.3', 'TLS1.2', 'TLS1.1', 'TLS1.0']
server যা দেখল (MITM-tampered) : ['TLS1.0']
server negotiated : TLS1.0
ServerHello.random শেষ ৮ বাইট : 444f574e47524400
client verdict : ABORT — client নিজে TLS1.3 offer করেছিল, তবু sentinel-সহ নিচু version এলো — downgrade ধরা পড়ল
=== দৃশ্য ৩: TLS 1.3 ছাড়াই legacy server (never learned the sentinel trick) ===
client offered : ['TLS1.2', 'TLS1.1', 'TLS1.0'] (নিজেই TLS1.3 জানে না)
server negotiated : TLS1.2
client verdict : CONTINUE — negotiated TLS1.2, sentinel নেই বা প্রত্যাশিতই ছিল — স্বাভাবিকতিনটা দৃশ্য মিলে সেন্টিনেল-কৌশলের পুরো যুক্তি দেখায়। দৃশ্য ১-এ কোনো
tampering নেই, negotiated version-ও সর্বোচ্চ — কোনো marker লাগেওনি।
দৃশ্য ২-তেই আসল ঘটনা — MITM client-এর আসল offer (['TLS1.3', ...])
থেকে সবকিছু কেটে শুধু ['TLS1.0'] রেখে দিল, server তাই বাধ্য হয়ে TLS1.0
negotiate করল এবং sentinel বসাল (কারণ server নিজে TLS1.3 জানত) —
আর client_verify ঠিক এই সেন্টিনেল আর client-এর নিজের memory
(client_offered_13 = True) মিলিয়ে ধরে ফেলল, ABORT দিল। দৃশ্য ৩-এ
গুরুত্বপূর্ণ negative case — client নিজেই TLS 1.3 জানে না, তাই
client_offered_13 = False, ফলে negotiated TLS1.2 হলেও কোনো false-alarm
নেই — এই asymmetry-ই design-টাকে ব্যবহারযোগ্য রাখে, নাহলে প্রতিটা genuinely
legacy client-ই প্রতিদিন ABORT পেত।
নিজে বাড়ান:
- দৃশ্য ৩-এ যদি server টাও TLS 1.3-সক্ষম হতো (
server_is_tls13_capable=True) কিন্তু client সত্যিই শুধু TLS1.2 জানত (কোনো MITM নেই), sentinel কি বসত? বসলেও কেনclient_verifyতখনওCONTINUEদেবে? কোড চালিয়ে নিশ্চিত হন। mitm_strip_above-এ ceiling"TLS1.2"বসিয়ে দেখুন — negotiated version কী হয়, sentinel বসে কিনা, আর client detect করে কিনা।- একটা
mitm_strip_and_forge_sentinel()কল্পনা করুন যা negotiated version নিচে নামানোর পাশাপাশিrandom_tail-ও নিজের ইচ্ছামতো বসিয়ে দেয় (sentinel মুছে ফেলে)। এটা কি বাস্তবে সম্ভব? কেন — Finished message-এর transcript-MAC (আগের নেটওয়ার্কিং লেসনে দেখা) এখানে কী ভূমিকা রাখে?
বাস্তব সিস্টেমে
Heartbleed — একটা প্রোটোকল-ব্যর্থতা নয়, implementation-ব্যর্থতা
এতক্ষণের সব আক্রমণ প্রোটোকলের design-এ নজর দিয়েছে — validation-স্কিপ, key-exchange মোড, negotiation-কারচুপি। Heartbleed (CVE-2014-0160, ২০১৪-এ প্রকাশিত) সম্পূর্ণ ভিন্ন শ্রেণির — TLS প্রোটোকল নিজে ঠিক ছিল, ভুলটা ছিল OpenSSL-এর একটা নির্দিষ্ট implementation-এ।
TLS-এর “heartbeat” extension (RFC 6520) সংযোগ জীবিত আছে কিনা যাচাই করার
একটা ছোট ব্যবস্থা — client একটা payload পাঠায় “এই payload-টা N বাইট
লম্বা,” server ঠিক সেই N বাইট ফেরত পাঠায়। OpenSSL-এর বাগটা ছিল — server
client-এর দাবি করা N-টা বিশ্বাস করে নিত, নিজে verify করত না যে
প্রকৃতপক্ষে পাঠানো payload-এর সাথে N মিলছে কিনা। একজন attacker N = 65535
বলে মাত্র ১ বাইটের payload পাঠাতে পারত — server তখন সেই ১ বাইটের পরে
নিজের memory থেকে বাকি ৬৫৫৩৪ বাইট যা-ই থাকুক সেটা কেটে ফেরত পাঠিয়ে
দিত। এই “যা-ই থাকুক”-এর মধ্যে প্রায়ই আগের request-এর leftover data থাকত
— session key, এমনকি server-এর নিজের static private key-র টুকরোও।
এটাই security/memory-safety-buffer-overflow লেসনের বিষয় — out-of-bounds read, bounds-check না থাকার ক্লাসিক পরিণতি; আজ সেই মেকানিজম নতুন করে derive করা হচ্ছে না, শুধু TLS-এর প্রেক্ষাপটে তার প্রভাব দেখা হচ্ছে।
প্রতিক্রিয়া আর উত্তরাধিকার। দুর্বলতা প্রকাশের পরপরই বিশ্বব্যাপী লক্ষ লক্ষ server patch করতে হয়েছিল, আর — patch যথেষ্ট ছিল না — যেসব server-এর private key potentially leak হয়েছিল সেই সব certificate revoke করে নতুন করে ইস্যু করতে হয়েছিল (আগের অংশে দেখা সেই কাঠামোগতভাবে কঠিন revocation সমস্যাটাই এখানে বাস্তবে এক ধাক্কায় লক্ষ লক্ষ certificate-এ সংঘটিত হলো)। OpenSSL project নিজেই এরপর funding আর code-review প্রক্রিয়ায় বড় সংস্কার আনে (Core Infrastructure Initiative), কারণ এমন একটা ব্যাপকভাবে ব্যবহৃত library-তে সম্পদ আর review এতদিন যথেষ্ট ছিল না, তা স্পষ্ট হয়ে গিয়েছিল।
যে ভুলগুলো সবাই করে
“একটা সাইটে padlock/HTTPS আছে মানে সাইটটা বিশ্বাসযোগ্য, প্রতারণা করবে না।”
Padlock শুধু প্রমাণ করে channel-টা এনক্রিপ্টেড আর আপনি দাবিকৃত domain-এর
সাথেই কথা বলছেন — মালিকের সততা না। একটা phishing domain (যেমন
bank-secure-login.example) সম্পূর্ণ বৈধ certificate পেতে পারে, কারণ CA
শুধু domain-control validate করে (এই domain-টা সত্যিই যে request করেছে
তার নিয়ন্ত্রণে আছে কিনা), মালিকের উদ্দেশ্য যাচাই করে না। “কী রক্ষা করে,
কী করে না” টেবিলের ঠিক এই ঘরটাই সবচেয়ে বেশি ভুল বোঝা হয়।
“Certificate validation ঠিকভাবে হচ্ছে মানে connection-টা নিরাপদ, ভবিষ্যতেও নিরাপদ থাকবে।”
Certificate validation শুধু আজকের authenticity নিশ্চিত করে — server-টা সত্যিই দাবিকৃত domain-এর। এটা forward secrecy সম্পর্কে কিছুই বলে না। যদি key exchange static RSA-নির্ভর হয় (আজ বিরল কিন্তু ঐতিহাসিকভাবে প্রচলিত), তাহলে ভবিষ্যতে সেই একই server-এর private key leak হলে, আজকের একেবারে নিখুঁতভাবে validated session-ও পশ্চাদগতভাবে decrypt হয়ে যেতে পারে — experiment-এর Mode A ঠিক এটাই দেখিয়েছে।
“OCSP stapling শুধু একটা performance optimization — না থাকলেও নিরাপত্তার কোনো ক্ষতি নেই।”
Performance-লাভ (client-এর আলাদা CA-query এড়ানো) real, কিন্তু আসল নিরাপত্তা-প্রশ্নটা হলো fail-open বনাম fail-closed। Stapling ছাড়া plain client-side OCSP-এ, CA-সার্ভার unreachable হলে সিস্টেম সাধারণত fail-open করে (validation বাদ দিয়ে চালিয়ে যায়) — attacker শুধু OCSP-request block করে দিয়েই revocation-check নিষ্ক্রিয় করে দিতে পারে। Stapling এই আক্রমণ-সারফেস কমায়, তাই এটা নিছক গতি না, revocation-checking-কে আসলেই কার্যকর রাখার একটা প্রক্রিয়াগত সংশোধন।
“Heartbleed আসলে TLS প্রোটোকলের একটা ত্রুটি ছিল — নিরাপত্তা-ডিজাইনেই গলদ।”
Heartbleed প্রোটোকল-লেভেলে কিছুই ভাঙেনি — TLS heartbeat extension-এর সংজ্ঞা নিজে সঠিক। ভুলটা OpenSSL-এর একটা নির্দিষ্ট implementation-এ, একটা bounds-check মিসিং থাকায় — client-এর দাবি করা length সরাসরি বিশ্বাস করে নেওয়া (memory-safety-buffer-overflow লেসনের ভাষায় classic out-of-bounds read)। এই পার্থক্যটা গুরুত্বপূর্ণ কারণ সমাধানও ভিন্ন — প্রোটোকল redesign না, বরং safer implementation practice আর কোড-review।
বুঝেছেন কি না দেখুন
1TLS ঠিক কোন তিনটা গ্যারান্টি দেয়, আর কোন চারটা জিনিস কখনোই দেয় না তার
অন্তত দুইটা বলুন।
স্মরণ
তিনটা গ্যারান্টি: confidentiality (AEAD encryption), integrity (AEAD tag
- transcript MAC), আর server authenticity (certificate chain + hostname/SAN match); client authenticity ঐচ্ছিক (শুধু mTLS-এ)। যা দেয় না — এর মধ্যে যেকোনো দুইটা: endpoint safety, application-layer bugs (SQLi/XSS), সাইটের সততা (phishing domain-ও বৈধ cert পেতে পারে), metadata privacy (DNS/SNI), availability (DDoS)।
2OCSP client-side query-তে “fail-open বনাম fail-closed” একটা কঠিন
trade-off কেন — দুইটা পছন্দেরই আসল সমস্যা কী?
যুক্তি
Fail-open (CA-সার্ভার unreachable হলে validation বাদ দিয়ে চালিয়ে যাওয়া) attacker-এর জন্য trivially bypassable — শুধু OCSP-request block করে দিলেই revocation-check নিষ্ক্রিয় হয়ে যায়। Fail-closed (unreachable হলে connection আটকানো) মানে CA-র সাময়িক downtime সরাসরি সেই CA-নির্ভর সব সাইটকে down করে দেয় — availability, নিরাপত্তার বিনিময়ে। OCSP stapling এই দুইটার মাঝামাঝি একটা ব্যবহারিক সমাধান — server নিজে periodically signed proof সংগ্রহ করে রাখে, per-connection CA-নির্ভরতা কমে।
3Forward-secrecy experiment-এ Mode A (static RSA) আর Mode B (ephemeral
DHE) — দুইটাতেই attacker আজকের session আজ decrypt করতে পারেনি। তাহলে
“আজকের নিরাপত্তা” মাপলে দুইটা মোড কি সমান? পার্থক্যটা ঠিক কখন প্রকাশ পায়?
প্রয়োগ
আজকের মুহূর্তে, হ্যাঁ — attacker কোনোটাই decrypt করতে পারেনি, দুইটাই সমান “নিরাপদ” মনে হয়। পার্থক্যটা প্রকাশ পায় শুধু ভবিষ্যতে, যখন server-এর static private key কোনোভাবে leak করে — Mode A-তে সেই leaked key দিয়ে recorded ciphertext সরাসরি decrypt হয়ে যায় (session key mathematically সেই key থেকেই derived ছিল), Mode B-তে কিছুই বের করা যায় না কারণ session key ephemeral, session-specific মানের ওপর নির্ভরশীল ছিল যা আগেই মুছে গেছে। Forward secrecy তাই “আজকের” নিরাপত্তা-মেট্রিক না, বরং “ভবিষ্যতের compromise থেকে অতীতের রক্ষা”-র প্রশ্ন।
4POODLE attack-এ MITM কীভাবে দুই পক্ষকে SSL 3.0-এ negotiate করতে বাধ্য
করেছিল, যখন উভয় পক্ষই আসলে আধুনিক TLS version সমর্থন করত?
যুক্তি
তখনকার browser-গুলোর একটা “compatibility fallback” আচরণ ছিল — আধুনিক version-এ handshake ব্যর্থ হলে চুপচাপ পুরনো, দুর্বল SSL 3.0-এ retry করা। MITM প্রতিটা modern-version handshake-attempt ইচ্ছাকৃতভাবে ব্যর্থ করে দিত (উদাহরণ: connection reset), browser বাধ্য হয়ে fallback-এ নামত। দুই পক্ষের কেউই নিজেদের সমর্থিত version পাঠাতে ব্যর্থ হয়নি — MITM শুধু negotiation-প্রক্রিয়ার একটা legacy-compatibility feature-কে অস্ত্র বানিয়েছিল।
5Downgrade-sim build-এ দৃশ্য ৩-এ (genuinely-legacy client, TLS1.3 না
জানা) কোনো ABORT আসেনি, যদিও negotiated version TLS1.3-এর নিচে ছিল।
এটা কি sentinel-কৌশলের একটা দুর্বলতা, নাকি ইচ্ছাকৃত ডিজাইন?
প্রয়োগ
ইচ্ছাকৃত ডিজাইন, দুর্বলতা না। client_verify-এর শর্তে
client_offered_13 চেক আছে ঠিক এই কারণে — client নিজে যদি কখনো TLS1.3
offer-ই না করে থাকে, তাহলে negotiated version নিচে হওয়াটা প্রত্যাশিতই,
কোনো tampering-এর প্রমাণ না। এই asymmetry ছাড়া প্রতিটা genuinely-legacy
client প্রতিদিন false-positive ABORT পেত, আর ব্যবহারযোগ্যতা নষ্ট হতো —
sentinel শুধু তখনই সতর্ক করে যখন client নিজের memory-তে থাকা তথ্যের
(আমি তো 1.3 বলেছিলাম) সাথে server-এর response না মেলে।
6একটা fintech mobile app ডিজাইন করছেন যেটা নিজের backend API-এর সাথে
কথা বলে (থার্ড-পার্টি সাইট না)। Certificate pinning ব্যবহার করবেন
কিনা, করলে কীভাবে key-rotation হ্যান্ডল করবেন, আর কেন একটা সাধারণ
consumer browser-এর জন্য এই একই সিদ্ধান্ত ভিন্ন হতে পারে তা যুক্তি দিন।
ডিজাইন
Fintech app-এর জন্য pinning যুক্তিসঙ্গত — attacker profile উচ্চ-মূল্যের (financial data), আর app নিজের একটাই backend-এর সাথে কথা বলে বলে pin-set ছোট আর manage-যোগ্য। Key-rotation হ্যান্ডল করতে multiple pin একসাথে রাখা উচিত (current + next key), rotation-এর আগেই নতুন pin app-update-এ পাঠিয়ে দেওয়া, আর ideally remote-config দিয়ে pin-set dynamically আপডেট করার একটা emergency escape-hatch রাখা — নাহলে rotation মানেই পুরনো app-version-এর ব্যবহারকারীরা lockout হয়ে যাবে। সাধারণ consumer browser-এর জন্য এই সিদ্ধান্ত অর্থহীন কারণ browser লক্ষ লক্ষ ভিন্ন ভিন্ন domain visit করে — প্রতিটার জন্য আলাদা pin maintain করা অসম্ভব, তাই browser-রা সাধারণ chain-of-trust validation-এর ওপরই নির্ভর করে, নিজস্ব app-এর মতো একক backend-কেন্দ্রিক pinning না।
এরপর কী
এরপর কী
আজকের লেসন TLS-এর প্রতিশ্রুতির সীমানা এঁকেছে — কী রক্ষা করে, কোথায় validation ভাঙে, কেন ephemeral key exchange ছাড়া ভবিষ্যৎ অরক্ষিত, আর downgrade attack কীভাবে negotiation-প্রক্রিয়াকেই লক্ষ্য বানায়। TLS একটা বার্তাকে transit-এ সুরক্ষিত রাখে — কিন্তু সেই বার্তা যদি হয় একটা login form-এর password, তাহলে TLS-এর কাজ HTTPS-connection বন্ধ হতেই শেষ।
TLS ডেটাকে পথে সুরক্ষিত রাখে। কিন্তু একটা login form-এর password যখন নিরাপদে server-এ পৌঁছে যায়, তখন সম্পূর্ণ ভিন্ন একটা সমস্যা শুরু হয়: এমনভাবে সেটা store করতে হবে যাতে database breach হলেও সবার password ফাঁস না হয়ে যায়। সেই প্রশ্নই পরের লেসনের বিষয় — security/password-storage।
আরও পড়ুন
- The Heartbleed Bug — CVE-2014-0160 — Codenomicon / Neel Mehta (Google Security) — public disclosure, April 2014 · OpenSSL-এর heartbeat extension-এর bounds-check বাগ — এই লেসনের case-study-র মূল উৎস
- This POODLE Bites: Exploiting The SSL 3.0 Fallback — Bodo Möller, Thai Duong, Krzysztof Kotowicz — Google Security Advisory, 2014 · SSL 3.0 downgrade আক্রমণের মূল advisory — CBC padding oracle আর downgrade-এর সংযোগ
- RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3 — E. Rescorla — IETF, 2018 · §4.1.3-এ downgrade-protection sentinel, আর mandatory forward-secrecy (RSA key-transport বাদ) — আজকের লেসনের প্রোটোকল-ভিত্তি
- Internet X.509 Public Key Infrastructure — Online Certificate Status Protocol (OCSP) — RFC 6960 — S. Santesson, M. Myers, R. Ankney, A. Malpani, S. Galperin, C. Adams — IETF, 2013 · CRL-এর বিকল্প হিসেবে OCSP-এর সংজ্ঞা, latency/privacy trade-off-এর আনুষ্ঠানিক ভিত্তি