İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat