المقال الأصلي
https://medium.com/mycrypto/the-magic-of-digital-signatures-on-ethereum-98fe184dc9c7
تعد تواقيع التشفير جزءًا أساسيًا من blockchain. يتم استخدامها لإثبات ملكية العنوان دون الكشف عن مفتاحه الخاص. يستخدم هذا بشكل أساسي لتوقيع المعاملات ولكن يمكن استخدامه أيضًا لتوقيع الرسائل. ستجد في هذه المقالة شرحًا تقنيًا لكيفية عمل هذه التوقيعات ، في سياق Ethereum.
ماهو التوقيع التشفيري ؟
عندما نتحدث عن التوقيعات في التشفير ، فإننا نتحدث عن نوع من إثبات الملكية ، والصلاحية ، والنزاهة ، وما إلى ذلك ، على سبيل المثال ، يمكن استخدامها من أجل:
- إثبات أن لديك المفتاح الخاص لعنوان (مصادقة) ؛
- التأكد من عدم العبث برسالة (مثل البريد الإلكتروني) ؛
هذا يعتمد على الصيغ الرياضية. نأخذ رسالة إدخال ومفتاح خاص وسر عشوائي (عادةً) ، ونحصل على رقم كإخراج ، وهو التوقيع. باستخدام صيغة رياضية أخرى ، يمكن عكس هذه العملية بطريقة تجعل المفتاح الخاص والسر العشوائي غير معروفين ولكن يمكن التحقق منهما. هناك العديد من الخوارزميات لهذا الغرض ، مثل RSA و AES ، لكن Ethereum (و Bitcoin) يستخدمان خوارزمية Elliptic Curve Digital Signature أو ECDSA. لاحظ أن ECDSA ليست سوى خوارزمية توقيع. على عكس RSA و AES ، لا يمكن استخدامها للتشفير.
باستخدام معالجة نقطة المنحنى الإهليلجي ، يمكننا اشتقاق قيمة من المفتاح الخاص ، والتي لا يمكن عكسها. بهذه الطريقة يمكننا إنشاء توقيعات آمنة وغير قابلة للعبث. تسمى الوظائف التي تشتق القيم “وظائف trapdoor”:
وظيفة trapdoor هي وظيفة يسهل حسابها في اتجاه واحد ، ولكن من الصعب حسابها في الاتجاه المعاكس (إيجاد معكوسها) بدون معلومات خاصة ، تسمى “trapdoor”.
التوقيع والتحقق باستخدام ECDSA
تتكون توقيعات ECDSA من رقمين (أعداد صحيحة): r و s. يستخدم Ethereum أيضًا متغيرًا إضافيًا v (معرف الاسترداد). يمكن تدوين التوقيع كـ {r، s، v}.
لإنشاء توقيع ، تحتاج إلى توقيع الرسالة والمفتاح الخاص (dₐ) للتوقيع عليه. تبدو عملية التوقيع “المبسطة” كما يلي:
- احسب التجزئة أو الهاش (e) للرسالة المطلوب توقيعها.
- قم بإنشاء قيمة عشوائية آمنة لـ k.
- احسب النقطة (x₁، y₁) على المنحنى الإهليلجي بضرب k مع ثابت G للمنحنى الإهليلجي .
- احسب r = x₁ mod n. إذا كانت r تساوي صفرًا ، فارجع إلى الخطوة 2.
- احسب s = k⁻¹ (e + rdₐ) mod n. إذا كانت s تساوي صفرًا ، فارجع إلى الخطوة 2.
في Ethereum ، يتم حساب التجزئة عادةً باستخدام
Keccak256(“\x19Ethereum Signed Message:\n32” + Keccak256(message)) .
هذا يضمن أنه لا يمكن استخدام التوقيع لأغراض خارج Ethereum.
نظرًا لأننا نستخدم قيمة عشوائية لـ k ، فإن التوقيع الذي نحصل عليه يختلف في كل مرة. عندما لا تكون k عشوائية بشكل كافٍ ، أو عندما لا تكون القيمة سرية ، فمن الممكن حساب المفتاح الخاص باستخدام توقيعين مختلفين (“هجوم الخطأ”). ومع ذلك ، عند تسجيل رسالة في MyCrypto ، يكون الإخراج هو نفسه في كل مرة ، فكيف يمكن أن يكون هذا آمنًا؟ تستخدم هذه التوقيعات الحتمية معيار RFC 6979 ، الذي يصف كيف يمكنك إنشاء قيمة آمنة لـ k بناءً على المفتاح الخاص والرسالة (أو التجزئة).
يمكن دمج التوقيع {r، s، v} في تسلسل واحد طوله 65 بايت: 32 بايت لـ r و 32 بايت لـ s وواحد بايت لـ v. إذا قمنا بترميز ذلك كسلسلة سداسية عشرية ، فسننتهي بـ سلسلة بطول 130 حرفًا ، تستخدمها معظم المحافظ والواجهات. على سبيل المثال ، يبدو التوقيع الكامل في MyCrypto كما يلي:
{
“address”: “0x76e01859d6cf4a8637350bdb81e3cef71e29b7c2”,
“msg”: “Hello world!”,
“sig”: “0x21fbf0696d5e0aa2ef41a2b4ffb623bcaf070461d61cf7251c74161f82fec3a4370854bc0a34b3ab487c1bc021cd318c734c51ae29374f2beb0e6f2dd49b4bf41c”,
“version”: “2”
}
يمكننا استخدام هذا في صفحة “التحقق من الرسالة” على MyCrypto ، وسوف تخبرنا أن 0x76e01859d6cf4a8637350bdb81e3cef71e29b7c2 قام بتوقيع على هذه الرسالة.
قد تسأل: لماذا نضيف جميع المعلومات الإضافية ، مثل address ، msg ، version؟ ألا يمكنك فقط التحقق من التوقيع نفسه؟ حسنًا ، ليس حقًا. سيكون هذا بمثابة توقيع عقد ، ثم التخلص من أي معلومات في العقد ، والاحتفاظ بالتوقيع فقط. على عكس توقيعات المعاملات (سنتعمق أكثر في ذلك) ، فإن توقيع الرسالة هو مجرد توقيع.
من أجل التحقق من رسالة ، نحتاج إلى الرسالة الأصلية وعنوان المفتاح الخاص الذي وقعت به والتوقيع {r، s، v} نفسه. رقم الإصدار هو مجرد رقم إصدار عشوائي تستخدمه MyCrypto. في الإصدارات القديمة من MyCrypto كان يتم إضافة التاريخ والوقت الحاليين إلى الرسالة ، وإنشاء تجزئة لذلك ، والتوقيع على ذلك باستخدام الخطوات الموضحة أعلاه. تم تغيير هذا لاحقًا لمطابقة سلوك التوقيع الشخصي لطريقة JSON-RPC ، لذلك تم تقديم الإصدار “2”.
الطريقة (مرة أخرى “ببساطة”) لاستعادة المفتاح العام تبدو كما يلي:
- احسب التجزئة (e) لاسترداد الرسالة.
- احسب النقطة R = (x₁، y₁) على المنحنى البيضاوي ، حيث x₁ هي r لـ v = 27 ، أو r + n لـ v = 28.
- احسب u₁ = -zr⁻¹ mod n و u₂ = sr⁻¹ mod n.
- احسب النقطة Qₐ = (xₐ، yₐ) = u₁ × G + u₂ × R.
Qₐ هي نقطة المفتاح العام للمفتاح الخاص الذي تم توقيع العنوان به. يمكننا اشتقاق عنوان من هذا والتحقق مما إذا كان يتطابق مع العنوان المقدم. إذا كان التوقيع صحيحا.
معرّف الاسترداد (“v”)
v هو البايت الأخير من التوقيع ، وهو إما 27 (0x1b) أو 28 (0x1c). هذا المعرف مهم لأنه نظرًا لأننا نعمل مع منحنيات إهليجيه، يمكن حساب نقاط متعددة على المنحنى من r و s فقط. قد ينتج عن ذلك مفتاحان عامان مختلفان (وبالتالي عناوين) يمكن استردادهما. يشير حرف v ببساطة إلى أي واحدة من هذه النقاط يجب استخدامها.
في معظم عمليات التنفيذ ، يكون v هو 0 أو 1 داخليًا فقط ، ولكن تمت إضافة 27 كرقم تعسفي لتوقيع رسائل Bitcoin و إستخدمها Ethereum أيضًا.
منذ EIP-155 ، نستخدم أيضًا معرف السلسلة لحساب قيمة v. هذا يمنع هجمات الإعادة عبر سلاسل مختلفة: لا يمكن استخدام أي عملية موقعة لـ Ethereum مع Ethereum Classic ، والعكس صحيح. حاليًا ، يتم استخدام هذا فقط لتوقيع المعاملة ، ولا يتم استخدامه لتوقيع الرسائل.
المعاملات الموقعة
حتى الآن تحدثنا في الغالب عن التوقيعات في سياق الرسائل. المعاملات ، تمامًا مثل الرسائل ، موقعة أيضًا قبل إرسالها. بالنسبة لمحافظ الأجهزة مثل أجهزة Ledger و Trezor ، يحدث هذا على الجهاز نفسه. بالنسبة للمفاتيح الخاصة (أو ملفات تخزين المفاتيح ، عبارات الذاكرة) ، يتم ذلك مباشرة على MyCrypto. يستخدم هذا أسلوبًا مشابهًا جدًا لكيفية توقيع الرسائل ، ولكن يتم ترميز المعاملات بشكل مختلف قليلاً.
المعاملات الموقعة مشفرة بإستخدام RLP ، وتتكون من جميع معلمات المعاملة (nonce ، سعر الغاز ، حد الغاز ، إلى ، القيمة ، البيانات) والتوقيع (v ، r ، s). تبدو المعاملة الموقعة كما يلي:
0xf86c0a8502540be400825208944bbeeb066ed09b7aed07bf39eee0460dfa261520880de0b6b3a7640000801ca0f3ae52c1ef3300f44df0bcfd1341c232ed6134672b16e35fe024ae317e134672b16e35fe024ae317970dfa261520880de0b6b3a7640000801ca0f3ae52c1ef3300f44df0bcfd1341c232ed6134672b16e35fe024ae3179134673
إذا أدخلنا هذا في صفحةbroadcast signed transaction page في MyCrypto ، فسنرى جميع معلمات المعاملة:
تحتوي المجموعة الأولى من وحدات البايت للمعاملة الموقعة على معلمات المعاملة المشفرة RLP ، وتحتوي المجموعة الأخيرة من البايت على التوقيع {r، s، v}. يمكننا ترميز معاملة موقعة مثل هذا:
- تشفير معلمات المعاملة: RLP (nonce ، gasPrice ، gasLimit ، to ، value ، data ، chainId ، 0 ، 0).
- احصل على تجزئة Keccak256 للمعاملة غير الموقعة المشفرة RLP.
- قم بتوقيع التجزئة باستخدام مفتاح خاص باستخدام خوارزمية ECDSA ، وفقًا للخطوات الموضحة أعلاه.
- تشفير المعاملة الموقعة: RLP (nonce ، gasPrice ، gasLimit ، to ، value ، data ، v ، r ، s).
من خلال فك تشفير بيانات المعاملة RLP المشفرة ، يمكننا الحصول على معلمات المعاملة الأولية والتوقيع مرة أخرى.
لاحظ أن معرف السلسلة يتم ترميزه في المعلمة v للتوقيع ، لذلك لا نقوم بتضمين معرف السلسلة نفسه في المعاملة الموقعة النهائية. كما أننا لا نحدد أي عنوان المرسل “من” ، حيث يمكن استرداده من التوقيع نفسه. يستخدم هذا داخليًا على شبكة Ethereum من أجل التحقق من المعاملات.
توحيد الرسائل الموقعة
هناك عدة مقترحات لتعريف معمارية قياسية للرسائل الموقعة. حاليًا ، لم يتم الانتهاء من أي من هذه المقترحات ، ولا يزال تنسيق Personal_sign ، الذي تم تنفيذه لأول مرة بواسطة Geth ، هو الأكثر شيوعًا. ومع ذلك ، فإن بعض هذه المقترحات مثيرة للاهتمام للغاية.
شرحت بإيجاز كيفية إنشاء التوقيعات حاليًا:
“\ x19Ethereum Signed Message: \ n” + length (message) + message
عادةً ما يتم تجزئة الرسالة مسبقًا ، لذا يمكن أن يكون الطول ثابتًا 32 بايت:
“\ x19Ethereum Signed Message: \ n32” + Keccak256 (رسالة)
يتم بعد ذلك تجزئة الرسالة الكاملة (بما في ذلك البادئة) مرة أخرى ، ويتم توقيع هذه البيانات. يعمل هذا بشكل جيد لإثبات الملكية ولكنه قد يمثل مشكلة في مواقف أخرى. على سبيل المثال ، إذا قام المستخدم “أ” بتوقيع رسالة وإرسالها إلى العقد “س” ، فيمكن للمستخدم “ب” نسخ هذه الرسالة الموقعة وإرسالها إلى العقد “ص”. وهذا ما يسمى هجوم إعادة التشغيل (replay attack). EIP-191 و EIP-712 هي بعض المقترحات التي تهدف إلى حل هذه المشكلة (والمزيد).
EIP-191: معيار البيانات الموقع
يعد EIP-191 اقتراحًا بسيطًا للغاية: فهو يحدد رقم الإصدار وبيانات الإصدار الخاصة. يبدو التنسيق كالتالي:
0x19 <1 byte version> <version specific data> <data to sign>
تعتمد البيانات الخاصة بالإصدار (كما يوحي الاسم) على الإصدار الذي نستخدمه. حاليًا ، يحتوي EIP-191 على ثلاثة إصدارات:
- 0x00: البيانات ذات “المدقق المقصود”. في حالة وجود عقد ، يمكن أن يكون هذا هو عنوان العقد.
- 0x01: بيانات منظمة ، على النحو المحدد في EIP-712. هذا سوف سيتم شرحها بشكل أكبر.
- 0x45: رسائل موقعة منتظمة ، مثل السلوك الحالي للإشارة الشخصية.
إذا حددنا مدققًا مقصودًا (على سبيل المثال ، عنوان العقد) ، فيمكن للعقد إعادة حساب التجزئة باستخدام عنوانه الخاص. لن يعمل إرسال الرسالة الموقعة إلى مثيل مختلف من العقد ، لأنه لن يكون قادرًا على التحقق من التوقيع.
تم اختيار البادئة الثابتة 0x19 بايت ، بحيث لا يمكن أن تكون الرسالة الموقعة معاملة موقعة RLP مشفرة ، نظرًا لأن المعاملات المشفرة RLP لا تبدأ أبدًا بـ 0x19.
EIP-712: تجزئة البيانات المنظمة المكتوبة بواسطة Ethereum وتوقيعها
لا ينبغي الخلط بينه وبين ERC-721 ، معيار الرمز غير القابل للاستبدال ، EIP-712 هو اقتراح للبيانات الموقعة “المكتوبة”. هذا يجعل توقيع البيانات أكثر قابلية للتحقق ، من خلال تقديمها بطريقة يسهل على الإنسان قراءتها.
توقيع رسالة باستخدام MetaMask. واجهة المعاملة القديمة الموقعة (باستخدام personal_sign) على اليسار ، الواجهة الجديدة (باستخدام EIP-712) على اليمين.
يعرّف EIP-712 طريقة جديدة لاستبدال Personal_sign: eth_signTypedData (مع أحدث إصدار هو eth_signTypedData_v4). بالنسبة لهذه الطريقة ، يتعين علينا تحديد جميع الخصائص (على سبيل المثال ، to، amount ، و nonce) وأنواعها (على سبيل المثال ، address ، uint256 ، و uint256) ، بالإضافة إلى بعض المعلومات الأساسية حول التطبيق ، المسمى النطاق.
يحتوي النطاق على معلومات مثل اسم التطبيق والإصدار ومعرف السلسلة والعقد الذي تتفاعل معه و salt. يجب أن يتحقق العقد من هذه المعلومات ، للتأكد من أنه لا يمكن استخدام توقيع لتطبيق آخر. هذا يحل مشكلة هجوم إعادة التشغيل المحتمل الموصوف سابقًا.
تعريفات الرسالة كما هو موضح في الصورة أعلاه هي كما يلي:
{
types: {
EIP712Domain: [
{ name: ‘name’, type: ‘string’ },
{ name: ‘version’, type: ‘string’ },
{ name: ‘chainId’, type: ‘uint256’ },
{ name: ‘verifyingContract’, type: ‘address’ },
{ name: ‘salt’, type: ‘bytes32’ }
],
Transaction: [
{ name: ‘to’, type: ‘address’ },
{ name: ‘amount’, type: ‘uint256’ },
{ name: ‘nonce’, type: ‘uint256’ }
]
},
domain: {
name: ‘MyCrypto’,
version: ‘1.0.0’,
chainId: 1,
verifyingContract: ‘0x098D8b363933D742476DDd594c4A5a5F1a62326a’,
salt: ‘0x76e22a8ee58573472b9d7b73c41ee29160bc2759195434c1bc1201ae4769afd7’
},
primaryType: ‘Transaction’,
message: {
to: ‘0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520’,
amount: 1000000,
nonce: 0
}
}
كما ترى ، الرسالة مرئية على MetaMask نفسها ، ويمكننا أن نؤكد أن ما نوقعه هو في الواقع ما نريد القيام به. يطبق EIP-712 EIP-191 ، لذلك ستبدأ البيانات بـ 0x1901: 0x19 كبادئة مجموعة ، و 0x01 كبايت إصدار للإشارة إلى أنه توقيع EIP-712.
باستخدام Solidity ، يمكننا تحديد بنية لنوع المعاملة ، وكتابة وظيفة لتجزئة المعاملة:
struct Transaction {
address payable to;
uint256 amount;
uint256 nonce;
}
function hashTransaction(Transaction calldata transaction) public view returns (bytes32) {
return keccak256(
abi.encodePacked(
byte(0x19),
byte(0x01),
DOMAIN_SEPARATOR,
TRANSACTION_TYPE,
keccak256(
abi.encode(
transaction.to,
transaction.amount,
transaction.nonce
)
)
)
);
}
تبدو بيانات المعاملة أعلاه كما يلي ، على سبيل المثال:
0x1901fb502c9363785a728bf2d9a150ff634e6c6eda4a88196262e490b191d5067cceee82daae26b730caeb3f79c5c62cd998926589b40140538f456915af319370899015d824feda513cd370899015d824feda513
وهو يتألف من بايت EIP-191 ، وفاصل النطاق المجزأ ، ونوع المعاملة المجزأة ، وإدخال المعاملة. يتم تجزئة هذه البيانات مرة أخرى وتوقيعها. يمكننا بعد ذلك استخدام ecrecover للتحقق من التوقيع في عقد ذكي:
function verify (address signer, Transaction calldata transaction, bytes32 r, bytes32 s, uint8 v) public returns (bool) {
return signer == ecrecover(hashTransaction(transaction), v, r, s);
}
سيتم شرح ecrecover بالتفصيل في القسم التالي. إذا كنت تبحث عن مكتبة بسيطة للعمل مع EIP-712 في JavaScript أو TypeScript ، فيرجى إلقاء نظرة على هذه المكتبة
للحصول على شرح كامل ومفصل لكيفية تنفيذ EIP-712 في عقد ذكي ، أوصي بهذه المقالة من MetaMask. لسوء الحظ ، لا تزال مواصفات EIP-712 مسودة ولا تدعمها العديد من التطبيقات حتى الآن. حاليًا ، يفتقر Ledger و Trezor إلى دعم EIP-712 ، مما قد يمنع اعتمادًا أوسع للمواصفات. قال ليدجر إنهم سيصدرون تحديثًا يضيف دعمًا لـ EIP-712 “قريبًا”.
التحقق من التواقيع باستخدام العقود الذكية
ما يجعل توقيعات الرسائل أكثر إثارة للاهتمام هو أنه يمكننا استخدام العقود الذكية للتحقق من توقيعات ECDSA. تحتوي Solidity على وظيفة مضمنة تسمى ecrecover (وهي في الواقع عقد مترجم مسبقًا على العنوان 0x1) والتي ستستعيد عنوان المفتاح الخاص الذي تم توقيع الرسالة به. يبدو تنفيذ العقد الأساسي (جدًا) كما يلي:
// SPDX-License-Identifier: MIT
pragma solidity 0.7.0;
contract SignatureVerifier {
/**
* @notice Recovers the address for an ECDSA signature and message hash, note that the hash is automatically prefixed with “\x19Ethereum Signed Message:\n32”
* @return address The address that was used to sign the message
*/
function recoverAddress (bytes32 hash, uint8 v, bytes32 r, bytes32 s) public pure returns (address) {
bytes memory prefix = “\x19Ethereum Signed Message:\n32”;
bytes32 prefixedHash = keccak256(abi.encodePacked(prefix, hash));
return ecrecover(prefixedHash, v, r, s);
}
/**
* @notice Checks if the recovered address from an ECDSA signature is equal to the address `signer` provided.
* @return valid Whether the provided address matches with the signature
*/
function isValid (address signer, bytes32 hash, uint8 v, bytes32 r, bytes32 s) external pure returns (bool) {
return recoverAddress(hash, v, r, s) == signer;
}
}
هذا العقد لا يفعل شيئًا أكثر من التحقق من التوقيعات وسيكون عديم الفائدة تمامًا من تلقاء نفسه ، حيث يمكن بالطبع التحقق من التوقيع بدون عقد ذكي أيضًا.
ما يجعل شيئًا كهذا مفيدًا هو أن المستخدم لديه طريقة غير موثوق بها لإعطاء عقد ذكي أوامر معينة دون إرسال معاملة. يمكن للمستخدم ، على سبيل المثال ، التوقيع على رسالة تقول ، “الرجاء إرسال 1 إيثر من عنواني إلى هذا العنوان.” يمكن للعقد الذكي بعد ذلك التحقق من الشخص الذي وقع على تلك الرسالة ، وتنفيذ هذا الأمر ، باستخدام معيار مثل EIP-712 و / أو EIP-1077. يمكن استخدام التحقق من التوقيع في العقود الذكية في تطبيقات مثل:
- عقود Multisig (على سبيل المثال ، Gnosis Safe) ؛
- منصات التبادلات اللامركزية
- معاملات ميتا ومرحلات الغاز (مثل Gas Station Networkد).
ولكن ماذا لو كنت تستخدم بالفعل محفظة عقد ذكية تريد توقيع رسالة منها؟ لا يمكننا ببساطة الوصول إلى المفتاح الخاص للعقد. يقترح ERC-1271 معيارًا من شأنه أن يسمح للعقود الذكية بالتحقق من صحة تواقيع العقود الذكية الأخرى. المواصفات بسيطة للغاية:
pragma solidity ^0.7.0;
contract ERC1271 {
bytes4 constant internal MAGICVALUE = 0x1626ba7e;
function isValidSignature(
bytes32 _hash,
bytes memory _signature
) public view returns (bytes4 magicValue);
}
يجب أن ينفذ العقد وظيفة isValidSignature ، والتي يمكنها تشغيل وظائف عشوائية مثل العقد أعلاه. إذا كان التوقيع ساري المفعول لعقد التنفيذ ،تقوم الدالة بإرجاع MAGICVALUE. يسمح هذا لأي عقد بالتحقق من توقيع العقد الذي ينفذ ERC-1271. داخليًا ، يمكن أن يجعل العقد الذي ينفذ ERC-1271 عدة مستخدمين يوقعون رسالة (في حالة عقد multisig على سبيل المثال) ، ويخزن التجزئة داخل نفسه. بعد ذلك ، يمكنه التحقق مما إذا كانت التجزئة المقدمة لوظيفة isValidSignature قد تم توقيعها داخليًا ، وما إذا كان التوقيع صالحًا لأحد مالكي العقد.
ختاماً
التوقيعات هي جزء أساسي من blockchain واللامركزية. ليس فقط لإرسال المعاملات ، ولكن أيضًا للتفاعل مع التبادلات اللامركزية وعقود multisig والعقود الذكية الأخرى. لا يوجد معيار واضح لتوقيع الرسائل حتى الآن ، وسيساعد الاعتماد الإضافي لمواصفات EIP-712 النظام البيئي على تحسين تجربة المستخدم ، بالإضافة إلى وجود معيار واحد لتوقيعات الرسائل.
إضافة تعليق