1. Giriş
Dijital imza, özgünlüğü ve bütünlüğü sağlamak için kullanılır. Bu makalede açıklandığı üzere, "Ön koşulların karşılandığı geçerli bir dijital imza, alıcıya mesajın bilinen bir gönderici tarafından oluşturulduğuna (özgünlük) ve mesajın iletim sırasında değiştirilmediğine (bütünlük) dair çok yüksek bir güvence sağlar."
Dijital imza, akıllı sözleşmelerde yaygın olarak kullanılmaktadır; örneğin izin listeli basım ve sipariş defteri tabanlı NFT pazaryerlerinde. Bunun nedeni, işlem maliyetlerini düşürmeye yardımcı olmasıdır (zincir dışı imzalama ve zincir üzerinde doğrulama). Ancak geliştiricilerin bu teknolojiyi yanlış kullanması, NFT pazaryerlerinde risklere yol açmaktadır. Bu blog yazısında, NFT ekosisteminde dijital imzaların yanlış kullanımını ele almak istiyoruz.
2. Uygulamalar
Dijital imza; NFT sözleşmelerinde izin listeli basım (yalnızca geçerli imzaya sahip kullanıcılar NFT basabilir) ve NFT pazarlarında sipariş doğrulama (yalnızca beklenen imzaya sahip siparişler yürütülebilir) için yaygın olarak kullanılmaktadır. Verinin imzalanması, gaz tasarrufu amacıyla zincir dışında gerçekleştirilir. Aşağıda bu iki kullanım senaryosunu açıklayacağız.
2.1. İzin Listeli Basım
"NFT basımı", blok zinciri üzerinde bir NFT oluşturma işlemidir. NFT projelerinin büyük çoğunluğu ürünlerini yaymak istediğinden, kullanıcıları izin listeli basım (ön satış vb. olarak da adlandırılır) ile teşvik etmeyi tercih ederler. Yer kazanan kişiler, token'ları daha düşük bir fiyattan (hatta ücretsiz olarak) basabilir. İzin listesindeki kullanıcıları genel (sıradan) kullanıcılardan ayırt etmek için dijital imza kullanılır. Aşağıda izin listeli basımın bir uygulama örneği verilmiştir.
function mint_approved(
vData memory info,
uint256 number_of_items_requested,
uint16 _batchNumber
) external {
...
require(verify(info), "Unauthorised access secret");
...
}
function verify(vData memory info) public view returns (bool) {
require(info.from != address(0), "INVALID_SIGNER");
bytes memory cat =
abi.encode(
info.from,
info.start,
info.end,
info.eth_price,
info.dust_price,
info.max_mint,
info.mint_free
);
bytes32 hash = keccak256(cat);
require(info.signature.length == 65, "Invalid signature length");
bytes32 sigR;
bytes32 sigS;
uint8 sigV;
bytes memory signature = info.signature; assembly {
sigR := mload(add(signature, 0x20))
sigS := mload(add(signature, 0x40))
sigV := byte(0, mload(add(signature, 0x60)))
} bytes32 data =
keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
);
address recovered = ecrecover(data, sigV, sigR, sigS);
return signer == recovered;
}
Bu kod parçacığı, Association NFT sözleşmesinden alınmıştır (güvenlik açığı içermektedir — bu kodu kopyalamayın). mint_approved() fonksiyonu, izin listeli basımı uygulamayı amaçlamaktadır: proje sahibi bir basım mesajı (info değişkeni) imzalar ve bu mesajı izin verilen kullanıcıya (NFT basabilecek kişiye) iletir. Ardından kullanıcı, imzalı değişkenle approved_mint fonksiyonunu çağırabilir. Sözleşme, mesajın proje tarafından imzalanıp imzalanmadığını doğrular (signer == recovered). Doğrulanması durumunda, fonksiyonu çağıran kişinin NFT basmasına izin verilir (ancak bu GÜVENLİ DEĞİLDİR; zira fonksiyonu çağıran kişinin izin listesindeki gerçek kişi olup olmadığına dair herhangi bir doğrulama yapılmamaktadır).
2.2. Sipariş Doğrulama
Sipariş doğrulama, NFT ekosisteminde dijital imzanın bir diğer uygulama alanıdır. NFT pazaryerleri, NFT'ler için alım satım işlevi sağladığından ekosistemde önemli bir rol üstlenir. Her NFT token'ı birbirinden farklı (non-fungible) olduğundan, otomatik piyasa yapıcı (AMM) ticaret politikasının NFT pazarlarında uygulanması güçtür. Bu nedenle OpenSea, LooksRare ve X2Y2 gibi NFT pazaryerlerinin büyük çoğunluğu, sipariş defteri ticaret modelini benimsemektedir.
Sipariş defteri ticareti oldukça basittir. Belirli bir fiyata varlık satmak isteyen bir yapıcı (maker) ve satıcının fiyatından varlığı satın almak isteyen bir alıcı (taker) bulunur. Bu durumda sipariş eşleşir. Aynı süreç, sipariş defteri tabanlı NFT pazaryerlerinde de geçerlidir. Tek fark, sipariş teklifinin sürecidir: NFT pazaryerleri, sipariş doğrulama için dijital imzaları kullanır. Şekil 1, sipariş defteri tabanlı pazaryerlerinden biri olan OpenSea'nın tüm ticaret sürecine ilişkin bir örneği göstermektedir.

Daha ayrıntılı belirtmek gerekirse, satıcı bir satış siparişi imzalar ve bunu OpenSea'nın sunucusunda saklar. Alıcı, imzalı satış siparişi bilgilerini OpenSea'nın sunucusundan alarak NFT pazarı sözleşmesini imzalı satış siparişini parametre olarak kullanarak çağırabilir. Pazar sözleşmesi, satıcının satış siparişini imzaladığını doğrulamak amacıyla siparişi doğrular (işlemi alıcı başlattığından) — bu, alıcının satıcının onayı olmadan bir varlık satın almasını engeller.
3. Güvenlik Olayları
Horton İlkesi, kriptografik sistemler için bir kural niteliğinde olup "Söyleneni değil, kastedileni doğrula" ya da "İmzaladığın şeyi kastet, kastettiğin şeyi imzala" şeklinde ifade edilebilir; eylemi tamamen ve kesin biçimde imzalamayı gerektirir. İmzanın kısmen veya hatalı olması, felakete yol açabilir.
3.1 Association NFT
Bölüm 2.1'deki NBA NFT sözleşmesini hatırlayalım. verify fonksiyonu standart bir imza doğrulaması gerçekleştirir; ancak KRİTİK bir bileşen eksiktir. İmza doğrulaması yalnızca mesajın proje tarafından imzalandığını güvence altına alır. Ancak, sözleşmeye imzayı sağlayan kişinin imzalı mesajdaki izin listesi kullanıcısıyla uyumlu olduğuna dair herhangi bir zorunluluk bulunmamaktadır. Sonuç olarak herhangi biri, doğrulamayı geçmek ve NFT basmak için aynı imzayı kullanabilir.
3.2 OpenSea
Bir diğer güvenlik sorunu OpenSea ile ilgilidir. 2022 yılının başında araştırmacılar, NFT alım satımının temel işlevselliğini uygulayan OpenSea pazaryeri sözleşmesinin (sürüm: wyvern 2.2) potansiyel bir güvenlik açığını kamuoyuyla paylaştı.
Wyvern protokolünde kullanıcılar, listelemeleri (satış teklifleri) veya teklifleri (alış teklifleri) zincir dışında hazırlar; tekliflerin imzaları ise zincir üzerinde doğrulanır. Wyvern teklifleri çok sayıda parametre içerir ve bu parametreler, teklifin özetini hesaplamak amacıyla tek bir bayt dizisinde bir araya getirilir. Ardından sözleşme, özetin imzasını doğrular. Parametrelerin bir araya getirilme yöntemi, parametreleri aşağıdaki yöntemlerle bir bayt dizisine basitçe paketler.
index = ArrayUtils.unsafeWriteAddress(index, order.target);
index = ArrayUtils.unsafeWriteUint8(index, uint8(order.howToCall));
index = ArrayUtils.unsafeWriteBytes(index, order.calldata);
index = ArrayUtils.unsafeWriteBytes(index, order.replacementPattern);
index = ArrayUtils.unsafeWriteAddress(index, order.staticTarget);
index = ArrayUtils.unsafeWriteBytes(index, order.staticExtradata);
index = ArrayUtils.unsafeWriteAddress(index, order.paymentToken);
Örneğin, parametreler 2 bileşenden oluşuyorsa: (address, bytes) ve parametreler (0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xc098") ise, birleştirilmiş baytlar 0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fc098 olur; yani yalnızca address + bytes. Oldukça basit ve anlaşılır görünüyor, değil mi?
Şimdi daha karmaşık bir örneği ele alalım; parametrelerin yapısı (address, bytes, bytes) şeklindedir.
1. parametre:
_(0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xab", "0xcdef")_.2. parametre:
_(0x9a534628b4062e123ce7ee2222ec20b86e16ca8f, "0xabcd", "0xef")_.
Birleştirilmiş baytlar şu şekildedir:
1. parametre:
_0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef_.2. parametre:
_0x0000000000000000000000009a534628b4062e123ce7ee2222ec20b86e16ca8fabcdef_.
İşte bu! Birbirinden farklı iki parametre aynı birleştirilmiş sonucu üretmektedir; bu da özetlerinin AYNI olduğu anlamına gelir ve tek bir imzanın iki farklı parametreyi doğrulayabileceği sonucunu doğurur.
Bunun nedeni, parametreler içinde değişken uzunluklu çok sayıda bileşenin bulunmasıdır. Bir saldırgan, değişkenlerin bir kısmını kesip önceki veya sonraki bileşenlere ekleyebilir. Ne yazık ki Wyvern sözleşmeleri, aşağıda görüldüğü üzere çok sayıda değişken uzunluklu parametreye sahiptir.
......
address target;
/* HowToCall. */
AuthenticatedProxy.HowToCall howToCall;
/* Calldata. */
bytes calldata;
/* Calldata replacement pattern, or an empty byte array for no replacement. */
bytes replacementPattern;
/* Static call target, zero-address for no static call. */
address staticTarget;
/* Static call extra data. */
bytes staticExtradata;
......
Bu güvenlik açığının etkisi, saldırganın (mümkün olduğu durumlarda) kurbanın hesaplarını bazı kötü amaçlı eylemleri gerçekleştirmek üzere kontrol edebilmesidir. Güvenlik açığının ayrıntılı bir analizi burada mevcuttur.
Bu bölümde bahsedilen her iki güvenlik olayı da Horton İlkesi'ni ihlal etmektedir. Daha açık bir ifadeyle, NBA sözleşmesi imzalı mesaja basım yapan kişiyi dahil etmemekte (ya da imzalı mesajda yer alan bilgilerin gerçek çağırıcıyla tutarlılığını denetlememekte), Wyvern sözleşmesi ise yapısal olmayan parametreleri imzalamakta; bu durum, parametrelerin sunumu (söylemi) değişmezken eylemin anlamının değiştirilebilmesine yol açmaktadır.
4. Öneriler
Horton İlkesi'ne uyun: söylediğinizi değil, kastettiğinizi imzalayın. İmza, kapsamlı ve doğru tüm gerekli bilgileri içermelidir.
- Doğrulanacak tüm bilgileri imzaya dahil edin. İmzalı mesajdaki verilerin çalışma zamanı değerleriyle tutarlılığını denetleyin (örneğin, imzalı mesajdaki hedef kullanıcı ile gerçek kullanıcı).
- İmzalanacak mesajın deterministik biçimde kodlanması gerekir; yani farklı yapılara sahip olup aynı kodlama sonucunu üreten mesajlar bulunmamalıdır.



