Ders 13 / 21
İmaj Güvenliği
İmaj güvenliği üç parçada ölçülür: üst katmanda silinen bir dosyanın alt katmanda gerçekte kaç bayt olarak durduğu gerçek dizinlerde sayılır, bilinen bir kusur kümesinde koşan tarama için yakalanan, kaçan ve yanlış alarm sayıları çıkarılır, imzalama gerçek bir anahtar çiftiyle bir zincir olarak kurulur ve doğrulamanın atlandığı her adımda neyin kabul edildiği sayılır.
İçindekiler
Önceki ders depoyu ölçtü ve bir soruyla bitti: özel imajın dokuz katmanı genel havuzda okunabiliyorsa, o katmanların içinde ne var? Şimdiye kadar katmanlara birer bayt yığını olarak bakıldı. Bu ders içeri bakar ve konuyu kapatır.
Üç ölçü vardır ve üçü de aynı sorunun parçalarıdır: durur hâldeki bir çıktının içinde ne olduğu nasıl bilinir? Birincisi imajın kendi yapısından gelen bir sızıntıdır, ikincisi bilinen kusurları bulma girişimidir, üçüncüsü çıktının kimden geldiğinin doğrulanmasıdır.
IM43. Katmanlar gerçek dizinlerdir ve birleşim görünümü alttan üste kurulur; bir dosyanın
silinmesi üst katmanda .sil- önekli bir işaret dosyasıyla gösterilir. IM44. Sır değerleri
kurgudur; gerçek bir sır kullanılmaz.
Katmanda Kalan Sır
M22/K01 bir sırrın kaç ayrı yoldan dışarı sızabildiğini taramıştı; o tarama burada tekrarlanmaz. Buradaki olgu tek ve katmana özgüdür: bir katman değişmez bir nesnedir. Bir dosyayı silen yönerge yeni bir katman üretir; alttaki katman olduğu gibi durur, çünkü onun özeti başka imajlarda da geçer. Silme, görünümü değiştirir; baytları değiştirmez.
// olcum-agi/sizinti.mjs — ust katmanda silinen dosya alt katmanda duruyor mu (gercek dizinler) import { mkdirSync, rmSync, writeFileSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join, dirname } from "node:path"; const KOK = join(process.env.TMPDIR ?? "/tmp", "olcum-agi-katman"); rmSync(KOK, { recursive: true, force: true }); // Kurgu sir degerleri. Gercek bir sir kullanilmaz. const SIR_A = "kurgu-erisim-anahtari-A1B2C3D4E5F6"; const SIR_B = "kurgu-veritabani-parolasi-Z9Y8X7"; const govde = (sir, dolgu) => Buffer.concat([Buffer.from(`${sir}\n`), Buffer.alloc(dolgu, "-")]); const KATMANLAR = [ { ad: "k1-temel", yaz: { "bin/toplayici": 4096, "lib/sifreleme": 8192 } }, { ad: "k2-bagimlilik", yaz: { "lib/istemci": 6144, "lib/kuyruk": 3072 } }, { ad: "k3-uygulama", yaz: { "app/toplayici.js": 5120, "app/gizli/erisim.anahtar": govde(SIR_A, 1664), "app/gecici/kurulum.log": govde(SIR_B, 2048) }, ayniKatmandaSil: ["app/gecici/kurulum.log"] }, // katman kapanmadan once kaldirilir { ad: "k4-yapilandirma", yaz: { "app/ayarlar.json": 512 }, ustKatmandaSil: ["app/gizli/erisim.anahtar"] }, // ust katmanda silme isareti birakilir ]; // Katmanlar gercek dizinlere yazilir; silme isareti ".sil-" onekli bos bir dosyadir. for (const k of KATMANLAR) { for (const [yol, deger] of Object.entries(k.yaz)) { if (k.ayniKatmandaSil?.includes(yol)) continue; const tam = join(KOK, k.ad, yol); mkdirSync(dirname(tam), { recursive: true }); writeFileSync(tam, typeof deger === "number" ? Buffer.alloc(deger, "x") : deger); } for (const yol of k.ustKatmandaSil ?? []) { const tam = join(KOK, k.ad, dirname(yol), ".sil-" + yol.split("/").pop()); mkdirSync(dirname(tam), { recursive: true }); writeFileSync(tam, ""); } } const dolas = (kok, alt = "") => readdirSync(join(kok, alt), { withFileTypes: true }) .flatMap((g) => g.isDirectory() ? dolas(kok, join(alt, g.name)) : [join(alt, g.name)]); // Birlesim gorunumu: alttan uste; silme isareti gorunen dosyayi kaldirir. const gorunum = new Map(); for (const k of KATMANLAR) { for (const yol of dolas(join(KOK, k.ad))) { const ad = yol.split("/").pop(); if (ad.startsWith(".sil-")) gorunum.delete(join(dirname(yol), ad.slice(5))); else gorunum.set(yol, statSync(join(KOK, k.ad, yol)).size); } } const diskte = KATMANLAR.flatMap((k) => dolas(join(KOK, k.ad)) .filter((y) => !y.split("/").pop().startsWith(".sil-")) .map((y) => [k.ad, y, statSync(join(KOK, k.ad, y)).size])); const gorunumBayt = [...gorunum.values()].reduce((t, b) => t + b, 0); const diskBayt = diskte.reduce((t, [, , b]) => t + b, 0); const gizlenen = diskte.filter(([, y]) => !gorunum.has(y)); console.log(`birlesim gorunumu : ${gorunum.size} dosya, ${gorunumBayt} bayt`); console.log(`katmanlarda gercekte: ${diskte.length} dosya, ${diskBayt} bayt`); console.log(`gorunumde yok, katmanda duruyor: ${gizlenen.length} dosya, ` + `${gizlenen.reduce((t, [, , b]) => t + b, 0)} bayt`); for (const [ad, sir] of [["SIR_A", SIR_A], ["SIR_B", SIR_B]]) { const bulunan = diskte.filter(([k, y]) => readFileSync(join(KOK, k, y)).includes(sir)); console.log(`${ad}: ${bulunan.length} katman dosyasinda, ` + `${bulunan.reduce((t, [, , b]) => t + b, 0)} bayt` + (bulunan.length ? ` -> ${bulunan.map(([k, y]) => `${k}/${y}`).join(", ")}` : "")); }
birlesim gorunumu : 6 dosya, 27136 bayt katmanlarda gercekte: 7 dosya, 28835 bayt gorunumde yok, katmanda duruyor: 1 dosya, 1699 bayt SIR_A: 1 katman dosyasinda, 1699 bayt -> k3-uygulama/app/gizli/erisim.anahtar SIR_B: 0 katman dosyasinda, 0 bayt
Birleşim görünümü altı dosya ve 27.136 bayt gösterir. Diskte yedi dosya ve 28.835 bayt vardır.
Aradaki 1.699 bayt, üst katmanda silinmiş görünen anahtar dosyasıdır ve k3-uygulama
katmanında bütün olarak durmaktadır. Görünüme bakan bir denetim onu bulamaz; katmanı açan biri
doğrudan okur.
İki sırrın farkı ölçüyü tamamlar. SIR_B de yazılmış, ama aynı katman kapanmadan
kaldırılmıştır: hiçbir katman dosyasında geçmez, sıfır bayt. SIR_A bir üst katmanda
silinmiştir ve tam boyutuyla durur. Aradaki tek fark silmenin hangi katmanda yapıldığıdır ve
sonuç ikili bir farktır — 1.699 bayta karşı 0 bayt.
Bu, önceki dersin ölçümüyle birleştiğinde ağırlaşır. Depoda bir katman ancak hiçbir manifest onu referans etmiyorsa silinebilirdi; ölçümde düşen imajların on yedi katmanından üçü referanslı olduğu için diskte kalmıştı. Sır içeren bir katman da aynı kurala tabidir: onu referans eden başka bir imaj varsa, sırrı taşıyan imaj silinse bile baytlar depoda durur. Yalıtımın delindiği yer budur — çıktının içine alınan fark, çıktıdan çıkarılamıyor.
Kapatma yolu ölçümün içinde yazılıdır. SIR_B’nin sıfır baytı, değerin katman kapanmadan
kaldırılmasıyla elde edilmiştir: aynı yönerge içinde yazılıp silinen bir dosya katmana hiç
girmez. Daha sağlamı ise hiç yazmamaktır — değerin yalnız derleme adımı boyunca görünen bir
kaynaktan okunması ve çıktıya kopyalanmaması. İki yolun ölçülen sonucu aynıdır, 1.699 bayta
karşı 0 bayt; ama birincisi yönergelerin sırasına, ikincisi çıktının yapısına dayanır ve sıra
bozulduğunda birincisi sessizce geri döner.
Tarama Neyi Görür
İkinci ölçü, imajın içindeki bileşenleri bilinen bir kusur kümesiyle karşılaştırır. Tarama bir eşleştirmedir ve eşleştirmenin iki yönde de hatası vardır.
IM45. Kusur kümesi kurgudur: bir bileşenin açık kaydı olma olasılığı 0,30, kaydın düzeltmesinin
sürüme geri taşınmış olma olasılığı 0,25’tir; tohum 20260909 görünürdür. IM46. Görünürlük
adaya bağlıdır — paket yöneticisi olan bir temelde envanter okunur, olmayanda bileşen ikilinin
içine gömülü gelir ve sürümü sayılamaz.
// olcum-agi/tarama.mjs — bilinen kusur kumesinde tarama: yakalanan, kacan, yanlis alarm (model) const uretec = (tohum) => () => (tohum = (tohum * 1103515245 + 12345) % 2147483648) / 2147483648; const P_KUSUR = 0.30; // bilesenin kusur kumesinde acik bir kaydi var const P_YAMALI = 0.25; // kaydi var ama duzeltme surume geri tasinmis: acik degil // gorunurluk: bilesenin envanterde surumuyle okunabilme olasiligi. Paket yoneticisi // olmayan bir temelde envanter yoktur; bilesen ikilinin icine gomulu gelir. const ADAY = [["tam donanimli", 148, 0.92], ["kucultulmus", 31, 0.82], ["tek ikili", 6, 0.35]]; const tara = (bilesen, gorunurluk, tohum = 20260909) => { const r = uretec(tohum); const s = { bilesen, acik: 0, uyari: 0, yakalanan: 0, kacan: 0, yanlis: 0 }; for (let i = 0; i < bilesen; i++) { const kayit = r() < P_KUSUR, gorunur = r() < gorunurluk, yamali = r() < P_YAMALI; const acik = kayit && !yamali; // gercekten acik kusur const bulur = kayit && gorunur; // taramanin urettigi uyari if (acik) s.acik += 1; if (bulur) s.uyari += 1; if (acik && bulur) s.yakalanan += 1; if (acik && !bulur) s.kacan += 1; if (bulur && !acik) s.yanlis += 1; } return s; }; console.log("aday".padEnd(15) + "bilesen".padEnd(9) + "gorunurluk".padEnd(12) + "acik kusur".padEnd(12) + "uyari".padEnd(7) + "yakalanan".padEnd(11) + "kacan".padEnd(7) + "yanlis alarm"); const sonuc = []; for (const [ad, n, g] of ADAY) { const s = tara(n, g); sonuc.push([ad, s]); console.log(ad.padEnd(15) + String(n).padEnd(9) + `%${(100 * g).toFixed(0)}`.padEnd(12) + String(s.acik).padEnd(12) + String(s.uyari).padEnd(7) + String(s.yakalanan).padEnd(11) + String(s.kacan).padEnd(7) + String(s.yanlis)); } console.log(""); const yuzde = (a, b) => (b ? `%${(100 * a / b).toFixed(0)}` : "olcusuz"); for (const [ad, s] of sonuc) { console.log(`${ad.padEnd(15)} yakalama orani ${yuzde(s.yakalanan, s.acik)}, ` + `uyarilarin dogrulugu ${yuzde(s.yakalanan, s.uyari)}`); }
aday bilesen gorunurluk acik kusur uyari yakalanan kacan yanlis alarm tam donanimli 148 %92 31 37 29 2 8 kucultulmus 31 %82 7 7 6 1 1 tek ikili 6 %35 1 0 0 1 0 tam donanimli yakalama orani %94, uyarilarin dogrulugu %78 kucultulmus yakalama orani %86, uyarilarin dogrulugu %86 tek ikili yakalama orani %0, uyarilarin dogrulugu olcusuz
Tam donanımlı temelde 31 açık kusur vardır; tarama 37 uyarı üretir, bunların 29’u gerçektir, 8’i yanlış alarmdır ve 2 açık kusur kaçmıştır. Yanlış alarmın kaynağı, düzeltmesi sürüme geri taşınmış bir kayıttır: sürüm numarası kusurlu aralıkta görünür, kod değildir. Kaçmanın kaynağı ise envanterde okunamayan bileşendir.
Son satır bu dersin en sert sayısıdır. Tek ikili temelde tarama sıfır uyarı üretir, ama bir açık kusur vardır. Temiz bir rapor, kusursuz bir imaj demek değildir; sayılamayan bileşen sayılmamış olur. Önceki derste ölçülen ödünç burada ikinci kez görünür — küçük temel gerçekten daha az kusur taşır (148 bileşene karşı 6), ama taşıdığının görünürlüğünü de düşürür.
Taramanın bedeli sayılabilir: 37 uyarının 8’i boşa bakılmış uyarıdır ve her biri bir bileşenin sürümüyle kusur kaydının elle karşılaştırılmasını gerektirir. Yakalama oranını yükseltmenin yolu daha çok uyarı değil, envanterin okunabilir olmasıdır — bileşen listesinin imajın yanında ayrı bir belge olarak taşınması bu boşluğu kapatır ve boyutu birkaç kilobayttır.
İmzalama Zinciri
Üçüncü ölçü kimlik sorusudur. Tarama “içinde ne var” der; imza “bu gerçekten bizim ürettiğimiz mi” der. Zincir üç halkadır: içerik özeti, o özetin imzası ve çekme anındaki doğrulama.
IM47. İmzalama gerçek bir anahtar çiftiyle yapılır, ama anahtar her koşumda yeniden üretilir; bu yüzden basılan şey imzanın uzunluğu ve doğrulama sonucudur, imzanın kendisi değil. IM48. Çekmede indirilen katmanın özetinin yeniden hesaplanması isteğe bağlı bir adımdır.
// olcum-agi/imza.mjs — imzalama zinciri: ozet, imza, dogrulama (gercek anahtarla) import { generateKeyPairSync, sign, verify, createHash } from "node:crypto"; const ozetle = (b) => createHash("sha256").update(b).digest(); // Anahtar cifti her kosumda yeniden uretilir. Imza baytlari kosuma baglidir; bu yuzden // basilan sey imzanin uzunlugu ve dogrulama sonucudur, imzanin kendisi degil. const { publicKey, privateKey } = generateKeyPairSync("ed25519"); // Onceki dersin 17 ayri imaji: her manifest dort katman ozetinin listesidir. const manifest = (i) => Buffer.from([1, 2, 3, 4] .map((k) => ozetle(Buffer.from(`katman-${k}-${i}`)).toString("hex").slice(0, 12)).join("\n")); const MANIFEST = Array.from({ length: 17 }, (_, i) => manifest(i)); const IMZA = MANIFEST.map((m) => sign(null, ozetle(m), privateKey)); console.log(`imzalanan manifest ${MANIFEST.length}, imza uzunlugu ${IMZA[0].length} bayt, ` + `toplam ${MANIFEST.length * IMZA[0].length} bayt`); console.log(`depo 176200 bayt; imzalarin payi binde ` + `${(1000 * MANIFEST.length * IMZA[0].length / 176200).toFixed(1)}`); // Zincirin halkalari ayri ayri sinanir. const m0 = MANIFEST[0]; const kurcalanmis = Buffer.from(m0.toString().replace(/^.{12}/, "ffffffffffff")); const { publicKey: baskaAnahtar } = generateKeyPairSync("ed25519"); console.log("\ndogru manifest, dogru imza :", verify(null, ozetle(m0), publicKey, IMZA[0])); console.log("kurcalanmis manifest :", verify(null, ozetle(kurcalanmis), publicKey, IMZA[0])); console.log("baska anahtarla dogrulama :", verify(null, ozetle(m0), baskaAnahtar, IMZA[0])); // Katman baytinin kendisi: manifest dogru olsa bile inen bayt degisebilir. const katman = Buffer.alloc(7200, "u"); const beklenen = ozetle(katman).toString("hex"); const bozuk = Buffer.from(katman); bozuk[42] = 0x41; // tek bayt degistirildi console.log("inen katmanin ozeti tutuyor:", ozetle(bozuk).toString("hex") === beklenen); // 24 cekme, uc disiplin: neyi kabul ederdi? const CEKME = 24, KATMAN = 4; console.log("\ndisiplin".padEnd(36) + "kabul edilen sahte manifest".padEnd(29) + "dogrulanmayan katman"); for (const [ad, imzaBakilir, katmanBakilir] of [ ["imza dogrulanmiyor", false, false], ["yalniz imza dogrulaniyor", true, false], ["imza ve katman ozeti dogrulaniyor", true, true], ]) { console.log(ad.padEnd(36) + `${imzaBakilir ? 0 : CEKME}/${CEKME}`.padEnd(29) + `${katmanBakilir ? 0 : CEKME * KATMAN}/${CEKME * KATMAN}`); }
imzalanan manifest 17, imza uzunlugu 64 bayt, toplam 1088 bayt depo 176200 bayt; imzalarin payi binde 6.2 dogru manifest, dogru imza : true kurcalanmis manifest : false baska anahtarla dogrulama : false inen katmanin ozeti tutuyor: false disiplin kabul edilen sahte manifest dogrulanmayan katman imza dogrulanmiyor 24/24 96/96 yalniz imza dogrulaniyor 0/24 96/96 imza ve katman ozeti dogrulaniyor 0/24 0/96
İmzanın bedeli en küçük kalemdir: on yedi manifest için toplam 1.088 bayt, deponun binde 6,2’si. Doğrulamanın kendisi de bir özet karşılaştırmasından ibarettir.
Orta üç satır zincirin çalıştığını gösterir: doğru manifest doğru imzayla doğrulanır, manifestin ilk on iki karakteri değiştirildiğinde doğrulama düşer, imza başka bir anahtarla sınandığında da düşer. Dördüncü satır zincirin nerede bittiğini gösterir — indirilen katmanın tek baytı değiştirildiğinde özet tutmaz, ama bu ancak özet yeniden hesaplanırsa görülür.
Son tablo doğrulamanın atlandığı her adımın açtığı yolu sayar. Doğrulama yoksa yirmi dört çekmenin yirmi dördü değiştirilmiş bir manifesti kabul eder. Yalnız imza doğrulanıyorsa sahte manifest sıfıra iner, ama doksan altı katmanın doksan altısı özeti sınanmadan kabul edilir: manifest doğrudur, inen baytın manifeste uyduğu bilinmez. İki halka da sınandığında iki sayı da sıfırdır.
Zincirin dışında kalan bir şey vardır ve yalıtımın burada delindiği yer odur: imza “bu imaj güvenlidir” demez, “bu manifest bu anahtarla imzalanmıştır” der. Anahtarın kime ait olduğu, nerede saklandığı ve kimin imzalayabildiği modelin dışındadır. İmzalama, güvenin nereye taşındığını belirler; ortadan kaldırmaz.
Zincir önceki dersin izlenebilirlik merdivenine de bağlanır. Kaynak sürümü alanı imajın hangi kaynaktan geldiğini söyler, imza o beyanın üretildikten sonra değişmediğini söyler. Alan imzalanmamışsa, sonradan yazılabilir bir nottur; imza olmadan izlenebilirlik oranı bir kayıt disiplinini ölçer, bir güvenceyi değil.
Özet
- Bir katman değişmez bir nesnedir; silme görünümü değiştirir, baytları değiştirmez. Ölçümde birleşim görünümü 6 dosya ve 27.136 bayt gösterirken diskte 7 dosya ve 28.835 bayt vardır.
- Fark 1.699 bayttır ve üst katmanda silinmiş görünen anahtar dosyasının tamamıdır. Aynı katman kapanmadan kaldırılan ikinci sır ise hiçbir katman dosyasında geçmez: 1.699 bayta karşı 0 bayt.
- Sır içeren katman referanslıysa, sırrı taşıyan imaj silinse bile depoda durur; çıktının içine alınan fark çıktıdan çıkarılamaz.
- Tarama iki yönde de yanılır: tam donanımlı temelde 31 açık kusurun 29’u yakalanır, 2’si kaçar ve 37 uyarının 8’i yanlış alarmdır. Tek ikili temelde tarama 0 uyarı üretir ama 1 açık kusur vardır — sayılamayan bileşen sayılmamış olur.
- İmzalama ucuzdur: 17 manifest için 1.088 bayt, deponun binde 6,2’si. Doğrulama yoksa 24 çekmenin 24’ü sahte manifesti kabul eder; yalnız imza doğrulanıyorsa 96 katmanın 96’sı özeti sınanmadan kabul edilir. İmza güveni ortadan kaldırmaz, anahtara taşır.
Sonraki Adım
Bu konu boyunca çıktı küçüldü, katmanlara ayrıldı, paylaşıldı, adlandırıldı, tarandı ve imzalandı. Ölçülen her sayı — bayt, katman, referans, uyarı, imza — durur hâldeki bir nesne üzerineydi. Nesne diskte duruyordu ve hiçbir şey yapmıyordu.
Oysa bu çıktının varlık nedeni çalışmaktır. Çalışmaya başladığı anda içindeki dosyalar bir sürecin gördüğü dosyalar olur, katmanlar üst üste bindirilip yazılabilir bir yüzeye dönüşür, imzayla doğrulanan şey belleğe alınır. Yalıtımın hangi bölümü bu geçişten sağ çıkar? Salt okunur olduğu ölçülen katmanlar hâlâ salt okunur mu, imajın içine alınan farkın kaçı çalışma anında yeniden ortamdan gelir ve bir sürecin ne yapabileceğini artık ne belirler?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.