انتقل إلى المحتوى الرئيسي انتقل إلى الملاحة انتقل إلى التذييل

سلسلة التراخيص والتفويض

إصدار تراخيص مقاوم للتلاعب، والتوقيع المفوَّض للنشر المحلي، واكتشاف الشوكة.

سلسلة التراخيص والتفويض

تستخدم Rediacc سلسلة تجزئة مقاومة للتلاعب لإصدار التراخيص ونموذج شهادة التفويض لعمليات النشر المحلية. تشرح هذه الصفحة كيفية حماية النظام من التلاعب وهجمات الإعادة ومشاركة التراخيص.

لماذا سلسلة؟

كل ترخيص يُصدره خادم الحساب يُسجَّل في دفتر أستاذ للإلحاق فقط. كل إدخال مرتبط بالإدخال السابق عبر تجزئة SHA-256، مُشكِّلاً سلسلة. للسلسلة ثلاث خصائص تجعل التلاعب قابلاً للاكتشاف:

  1. أرقام التسلسل عالمية وتصاعدية لكل اشتراك. تخطي الإدخالات أو إعادة ترتيبها يكسر السلسلة.
  2. تجزئات السلسلة تربط كل إدخال بجميع الإدخالات السابقة. تعديل أي إدخال سابق يُبطل كل إدخال يليه.
  3. يخزن renet أعلى تسلسل شاهده لكل اشتراك. يُكتشف فوراً الخادم الذي يتراجع في تسلسله.

كيف يُصدَر الترخيص

عندما يطلب CLI تفعيل جهاز أو ترخيص مستودع، يقوم خادم الحساب بما يلي:

  1. يقرأ رأس السلسلة الحالي (آخر تسلسل + تجزئة) للاشتراك.
  2. يبني حمولة الترخيص برقم التسلسل التالي وتجزئة السلسلة السابقة مضمَّنَيْن.
  3. يوقّع الحمولة بـ Ed25519.
  4. يحسب chainHash = SHA256(prevChainHash + ":" + signedPayload).
  5. يُلحق الإدخال بدفتر الإصدار بشكل ذري. إذا تصادم طلبان متزامنان على نفس التسلسل، يُعيد الخاسر اكتساب التسلسل التالي ويُعيد التوقيع.
  6. يُعيد الكتلة الموقّعة مع تجزئة السلسلة إلى CLI.

sequence وprevChainHash موجودان داخل الحمولة الموقّعة (لا يمكن تعديلها دون إبطال التوقيع). chainHash موجود في المغلف (يُحسب بعد التوقيع لتجنب التبعية الدائرية).

كيف يتحقق renet

كل جهاز يشغّل Renet يخزن حالة السلسلة الأخيرة المعروفة لديه في {licenseDir}/chain-state.json. عند كل تحقق من الترخيص، يتحقق Renet من:

الفحصمعنى الفشل
توقيع Ed25519 صالحالترخيص مزوَّر أو مُعدَّل
sequence > lastKnownSequenceالخادم تراجع في السلسلة (هجوم إعادة تشغيل)
chainHash == SHA256(prevChainHash + ":" + payload)تم تعديل إدخال السلسلة
issuedAt >= lastKnownIssuedAtالتلاعب بالساعة (ضبط ساعة الخادم للخلف)

إذا فشل أي فحص، يُرفض الترخيص ويُبلَّغ عن سبب الفشل.

شهادات التفويض (محلي)

لعمليات النشر المعزولة عن الإنترنت أو المستضافة ذاتياً، يُصدر خادم الحساب الأعلى شهادة تفويض تُخوِّل خادماً محلياً للتوقيع على التراخيص بمفتاح Ed25519 الخاص به. تُقيّد الشهادة ما يمكن للخادم المحلي فعله.

هيكل الشهادة

تحتوي شهادة التفويض على:

  • subscriptionId - الاشتراك الذي تنطبق عليه هذه الشهادة
  • planCode وmaxMachines وmaxRepositorySizeGb وmaxRepoLicenseIssuancesPerMonth - حدود الخطة مضمَّنة
  • maxTotalIssuances - الحد الأقصى لرقم تسلسل السلسلة
  • delegatedPublicKey - المفتاح العام Ed25519 للخادم المحلي (SPKI base64)
  • genesisHash - نقطة البداية للسلسلة (استمرار من شهادة سابقة، أو “genesis”)
  • genesisSequence - تسلسل السلسلة عند وقت الإصدار. يُستخدم بواسطة /onprem/cert-upload للتحقق من أن الشهادة الجديدة ترتبط بإدخال معروف في دفتر الإصدار المحلي عندما تقدمت السلسلة أثناء النقل. اختياري للتوافق مع الإصدارات السابقة (يُعامل كـ 0 إذا كان مفقوداً).
  • validFrom وvalidUntil - نافذة الصلاحية (تحكمها سياسة الصلاحية أدناه)
  • موقّعة بواسطة مفتاح Ed25519 الرئيسي للخادم الأعلى

كيف يعمل التفويض

  1. يُولِّد مسؤول المؤسسة زوج مفاتيح Ed25519 على الخادم المحلي.
  2. يطلب المسؤول شهادة تفويض من الخادم الأعلى:
    POST /admin/delegation-certs
    { subscriptionId, validDays: 90, delegatedPublicKey: "MCowBQYDK2VwAyEA..." }
  3. يوقّع الخادم الأعلى الشهادة بمفتاحه الرئيسي ويُعيدها.
  4. يخزن الخادم المحلي الشهادة ومفتاحه الخاص، مستعداً للتوقيع على التراخيص.
  5. عندما يطلب CLI ترخيصاً من الخادم المحلي، يوقّع الخادم بمفتاحه المفوَّض ويضمِّن مرجعاً للشهادة.
  6. يُجري renet تحققاً على مستويين:
    • يتحقق من توقيع الشهادة مقابل مفتاح الخادم الأعلى الرئيسي المضمَّن.
    • يتحقق من توقيع الكتلة مقابل المفتاح المفوَّض من الشهادة.
    • يتحقق من أن blob.sequence <= cert.maxTotalIssuances.
    • يُطبق جميع فحوصات السلسلة القياسية.

لا يستطيع الخادم المحلي:

  • تزوير ترخيص خارج حدود خطة شهادة التفويض (يرفضه renet).
  • إصدار أكثر من maxTotalIssuances عمليات إجمالية (يرفض renet تجاوز التسلسل).
  • تعديل الشهادة (يكسر توقيع الخادم الأعلى).

سياسة الصلاحية

تُحسب نافذة صلاحية شهادة التفويض بواسطة مساعد سياسة مشترك (computeDelegationCertValidity()) يعمل على كل من الواجهة الخلفية للخادم الأعلى وواجهة بوابة العميل الأمامية. نفس المدخلات تُنتج دائماً نفس validUntil، حتى يتمكن العملاء من معاينة الصلاحية الفعلية في نافذة الإنشاء قبل الإرسال.

الإعدادات الافتراضية والحدود القصوى لكل خطة

الخطةالصلاحية الافتراضيةالحد الأقصى للخطة
COMMUNITY15 يوماً30 يوماً
PROFESSIONAL60 يوماً120 يوماً
BUSINESS90 يوماً180 يوماً
ENTERPRISE120 يوماً365 يوماً

الافتراضي هو ما تختاره نقطة نهاية الإنشاء عندما يحذف المُرسِل validDays. الحد الأقصى هو الحد الأعلى الذي يمكن للمُرسِل طلبه.

التجاوز لكل اشتراك

يمكن للمسؤولين تعيين قيمة delegationCertDefaultDays مخصصة على اشتراك محدد عبر صفحة تفاصيل الاشتراك في لوحة الإدارة. يستبدل التجاوز كلاً من الافتراضي والحد الأقصى لذلك الاشتراك - وهو مخرج طوارئ للعملاء الخاصين (مثل عقد مؤسسي يحتاج شهادة 200 يوم على خطة COMMUNITY). لا يزال مخطط Zod يُطبق نطاقاً مطلقاً 1..365.

الحد الأقصى الصلب: نهاية الاشتراك + 3 أيام سماح

بغض النظر عن حد الخطة الأقصى والتجاوز، كل شهادة محدودة بـ subscription.expiresAt + 3 days (الـ SUBSCRIPTION_CONFIG.gracePeriodDays الموجود). هذا يعني:

  • للاشتراكات الدائمة (expiresAt = null)، لا ينطبق حد انتهاء الصلاحية - فقط حد الخطة الأقصى.
  • لاشتراكات Stripe الشهرية المدفوعة، الحد هو تقريباً تاريخ الفوترة التالي + 3 أيام. عندما تُقدِّم Stripe expiresAt كل شهر، يتحرك الحد معها.
  • لاشتراكات الفترة التجريبية، الحد هو نهاية التجربة + 3 أيام.

الأيام الفعلية والسبب

كل استجابة إنشاء/تجديد تتضمن effectiveDays وreason حتى يرى المُرسِل بالضبط لماذا حصلت الشهادة على صلاحيتها:

السببالمعنى
plan_defaultلا طلب ولا تجاوز - استُخدم الافتراضي لكل خطة
subscription_overrideلا طلب - استُخدم التجاوز لكل اشتراك كافتراضي
requestedطلب المُرسِل قُبل ضمن جميع الحدود
plan_max_clampطلب المُرسِل تجاوز الحد الأقصى للخطة - تم التقليص
override_max_clampطلب المُرسِل تجاوز التجاوز لكل اشتراك - تم التقليص
subscription_cap_clampالهدف الصالح بخلاف ذلك سيتجاوز expiresAt + 3 days للاشتراك

تستخدم نافذة إنشاء بوابة العميل هذه الأسباب لعرض معاينة حية (“ستحصل على شهادة لمدة 18 يوماً. تم التقليص لأن الشهادة لا يمكن أن تتجاوز تاريخ انتهاء اشتراكك بأكثر من 3 أيام.”) حتى لا يُرسل العملاء بشكل أعمى.

عتبة التجديد التكيفية

تستخدم حلقة التجديد التلقائي للخادم المحلي عتبة تكيفية مستوحاة من Let’s Encrypt:

effectiveThresholdDays = min(env.RENEW_THRESHOLD_DAYS, ceil(certValidityDays / 3))

تتجدد شهادة COMMUNITY لمدة 15 يوماً عند بقاء 5 أيام. تتجدد شهادة BUSINESS لمدة 90 يوماً عند بقاء 14 يوماً (يُطبَّق الحد المكوَّن في البيئة). تتجدد شهادة ENTERPRISE لمدة 120 يوماً عند بقاء 14 يوماً. هذا يمنع الشهادات قصيرة العمر من إطلاق التجديد فوراً مع إعطاء الشهادات طويلة العمر هامش مريح.

إنفاذ الشهادة النشطة الواحدة

قد يمتلك الاشتراك شهادة تفويض نشطة واحدة على الأكثر في أي وقت (MAX_ACTIVE_DELEGATION_CERTS_PER_SUBSCRIPTION = 1).

لماذا واحدة؟

يُطبق كل تثبيت محلي maxRepoLicenseIssuancesPerMonth وmaxActivations وسلامة السلسلة مقابل دفتر إصدار محلي خاص به. لا يُزامن الخادم المحلي عدادات الاستخدام مع الخادم الأعلى - هذا هو الهدف الأساسي من التفويض القابل للعمل دون إنترنت.

لو امتلك اشتراك شهادات نشطة متعددة (شهادة لكل تثبيت)، كان كل تثبيت سيُطبق الحد بشكل مستقل:

  • اشتراك 500/شهر مع 3 شهادات نشطة يسمح بـ 1,500 إصدار/شهر عملياً.
  • ثلاث سلاسل متوازية، كل منها مرتبطة بـ genesis، دون تسوية تدقيق ممكنة.

لا يستطيع الخادم الأعلى اكتشاف هذا التجاوز لأن الخوادم المحلية مصمَّمة للعمل دون إنترنت. الشهادة النشطة الواحدة هي النموذج القابل للإنفاذ الوحيد. يجب على عملاء التثبيت المتعدد (إنتاج + تدريج + DR) شراء اشتراك واحد لكل تثبيت.

سلوك التصادم

يرفض POST /admin/delegation-certs وPOST /portal/delegation-certs الإنشاء الثاني بـ:

HTTP/1.1 409 Conflict
{
  "code": "DELEGATION_CERT_ALREADY_ACTIVE",
  "existingCertId": "...",
  "actions": {
    "renew": "POST /portal/delegation-certs/process-renewal-request (preserves chain)",
    "revokeAndCreate": "POST /portal/delegation-certs/{existingCertId}/revoke then retry create"
  }
}

تعرض بوابة العميل هذا بنافذة حوار مخصصة تشرح العواقب:

  • التجديد (موصى به) - يمدد السلسلة الموجودة. جميع تراخيص المستودع المُصدَرة سابقاً تظل تعمل.
  • الإلغاء والإنشاء - يتجاهل السلسلة الموجودة ويبدأ من جديد من genesis. تراخيص المستودع المُصدَرة سابقاً تصبح غير قابلة للتحقق بمجرد انتهاء validUntil للشهادة القديمة. استخدم هذا فقط عند الانتقال إلى خادم محلي جديد بمفتاح توقيع مختلف، أو للتعافي من مفتاح مخترق.

renew() هو التبديل الذري الذي يحافظ على الشهادة النشطة الواحدة وهو غير خاضع لفحص التصادم 409.

حد المعدل

حتى مع الشهادة النشطة الواحدة، قد يُكرر مُرسِل ضار revoke -> create -> revoke -> create لاستهلاك دورات توقيع المفتاح الرئيسي للخادم الأعلى. تحد كلتا نقطتَي نهاية الإنشاء بـ 10 محاولات لكل 24 ساعة متجددة لكل اشتراك عبر جدول rateLimits الموجود:

HTTP/1.1 429 Too Many Requests
Retry-After: 78234
{ "code": "DELEGATION_CERT_RATE_LIMITED", "retryAfterSec": 78234 }

يُزداد العداد عند كل محاولة بغض النظر عن النتيجة (حلقات البريد العشوائي للتصادم تخضع للحد أيضاً).

اكتشاف الشوكة

إذا شارك عميل شهادة تفويضه مع طرف آخر (أو شغّل خادمَيْن محليَّيْن من نفس الشهادة)، تتباعد السلاسل. يكتشف الخادم الأعلى هذا عند التجديد.

تدفق التجديد

  1. يستدعي مسؤول الخادم المحلي POST /admin/delegation-certs/renew مع رأس السلسلة الحالي:
    { subscriptionId, currentChainHash, currentSequence, delegatedPublicKey }
  2. يمشي الخادم الأعلى إدخالات السلسلة مقابل سجل دفتر الأستاذ الخاص به.
  3. إذا لم يتطابق currentChainHash مع سلسلة الخادم الأعلى المسجَّلة عند currentSequence، تُكتشف الشوكة:
    409 { code: 'CHAIN_FORK_DETECTED', divergedAtSequence: N }
  4. يُعيَّن genesisHash للشهادة الجديدة على تجزئة السلسلة الحالية، حتى تتمكن الأجهزة ذات حالة السلسلة القديمة من الاستمرار من حيث توقفت.

إذا تمت مشاركة الشهادة مع غير عميل:

  • يمكنهم استخدامها خلال فترة صلاحية الشهادة.
  • عند أول تجديد، يرى الخادم الأعلى سلسلة واحدة فقط (الشرعية).
  • genesisHash للشهادة الجديدة يتطابق فقط مع السلسلة الشرعية.
  • الأجهزة على السلسلة المشتركة سترفض التراخيص الجديدة فوراً لأن chainHash المخزَّن لديها لا يتصل بـ genesisHash الشهادة الجديدة.

التجديد المعزول عن الإنترنت

لعمليات التثبيت المحلي التي لا تملك وصول HTTPS صادر إلى الخادم الأعلى، تدفق التجديد كامل دون إنترنت. هناك ثلاث نقاط نهاية جديدة تُغلق الحلقة:

على الخادم المحلي (auth, root, requireElevated()):

  • GET /onprem/cert-current - تنزيل الشهادة الموقّعة المحملة حالياً (نسخ احتياطي، تدقيق، إعادة استيراد)
  • GET /onprem/renewal-request - توليد مانيفيست موقّع يحتوي على رأس السلسلة المحلية + المفتاح العام المفوَّض، موقّع بالمفتاح الخاص المحلي

على الخادم الأعلى (Admin أو بوابة بنطاق المؤسسة):

  • POST /admin/delegation-certs/process-renewal-request (جذر النظام عبر العملاء)
  • POST /portal/delegation-certs/process-renewal-request (مالك/Admin المؤسسة)

مانيفيست طلب التجديد

طلب التجديد عبارة عن مستند JSON صغير:

{
  "manifest": {
    "schemaVersion": 1,
    "generatedAt": "2026-04-15T12:00:00.000Z",
    "subscriptionId": "...",
    "currentChainHash": "...",
    "currentSequence": 42,
    "delegatedPublicKey": "MCowBQYDK2VwAyEA...",
    "currentCertValidUntil": "...",
    "currentCertPublicKeyId": "...",
    "currentCertId": null
  },
  "signature": "<base64 Ed25519>",
  "publicKeyId": "..."
}

يُحسب التوقيع على الترميز القانوني للمانيفيست (مفاتيح مُرتَّبة أبجدياً، ثم JSON.stringify) باستخدام المفتاح الخاص المحلي. يضمن هذا أن كلا الطرفين يحسبان بايتات متطابقة بغض النظر عن ترتيب بناء الكائن.

التحقق عند الخادم الأعلى

تُشغِّل processRenewalManifest() خمسة فحوصات:

  1. الشهادة النشطة موجودة لاشتراك المانيفيست. تُعيد 404 NO_ACTIVE_CERT بخلاف ذلك - يجب على العميل استخدام تدفق الإنشاء، وليس التجديد.
  2. المفتاح العام المفوَّض يتطابق مع الشهادة النشطة. تُعيد 400 DELEGATED_KEY_MISMATCH بخلاف ذلك - تحمي من إعادة التشغيل من خادم محلي مختلف.
  3. توقيع المانيفيست يتحقق مقابل delegatedPublicKey للشهادة النشطة. تُعيد 400 MANIFEST_SIGNATURE_INVALID بخلاف ذلك - يُثبت أن المانيفيست جاء من حامل المفتاح الخاص المحلي.
  4. عمر المانيفيست خلال 7 أيام (RENEWAL_MANIFEST_MAX_AGE_MS). تُعيد 400 MANIFEST_EXPIRED بخلاف ذلك - مرساة مكافحة الإعادة.
  5. ارتباط تجزئة السلسلة عند currentSequence للمانيفيست يتطابق مع دفتر الخادم الأعلى. تُعيد 409 CHAIN_FORK_DETECTED بخلاف ذلك - تحمي من السلاسل المتشعبة.

إذا نجحت جميع الفحوصات، تستدعي processRenewalManifest تدفق renew() الموجود، الذي يُنهي الشهادة القديمة ويُدرج شهادة جديدة بشكل ذري. وهو غير خاضع للشهادة النشطة الواحدة 409 على جانب الإنشاء لأنه تبديل ذري وليس إلغاء + إنشاء خطوتين.

تقدم التسلسل أثناء النقل

يلتقط مانيفيست طلب التجديد رأس السلسلة في لحظة التوليد. بينما المانيفيست في العبور (تسليم USB، بريد إلكتروني مشفر)، قد يستمر الخادم المحلي في إصدار تراخيص المستودع، مما يُقدِّم سلسلته المحلية.

عند رفع الشهادة الجديدة مجدداً إلى الخادم المحلي، يتحقق /onprem/cert-upload من أن genesisSequence للشهادة الجديدة لا يزال يرتبط بإدخال معروف في دفتر الإصدار المحلي:

  • إذا كان cert.genesisSequence > localHead.sequence - يُعيد 409 CHAIN_HEAD_BEHIND (الخادم الأعلى على سلسلة متشعبة).
  • إذا كان cert.genesisSequence > 0 وإدخال دفتر الأستاذ المحلي عند ذلك التسلسل لديه chainHash مختلف عن cert.genesisHash - يُعيد 409 CHAIN_FORK_ON_UPLOAD (السلسلة المحلية تباعدت).
  • بخلاف ذلك، تُقبل الشهادة. تستمر الإصدارات المستقبلية من localHead.sequence + 1.

هذا يعني أنه لا يُشترط تجميد الكتابة أثناء النقل. السلسلة تمتد بشكل طبيعي على كلا الجانبين. مشابه لكيفية تعامل تجديد شهادة X.509 مع الأرقام التسلسلية أثناء التشغيل.

التدقيق الدوري

يوفر الخادم الأعلى نقطة نهاية تدقيق للتحقق من سلامة السلسلة دون تجديد الشهادة:

POST /admin/delegation-certs/audit
{ subscriptionId, chainEntries: [{ sequence, chainHash }, ...] }

يمشي الخادم الأعلى الإدخالات ويُعيد إما { valid: true } أو { valid: false, divergedAtSequence: N, expected, actual }.

يجب على الخوادم المحلية استدعاء هذه النقطة دورياً (الافتراضي: أسبوعياً عبر متغير البيئة UPSTREAM_AUDIT_URL) لاكتشاف الشوكات مبكراً.

إثباتات التدقيق على مستوى الجهاز

يمكن لـ renet التحقق من استمرارية السلسلة محلياً باستخدام VerifyAuditProof. عندما يُجدِّد جهاز ترخيصه بعد فجوة طويلة، يمكن للخادم إعادة إدخالات السلسلة الوسيطة كإثبات. يمشي الجهاز الإثبات للتحقق من أن كل chainHash مشتق من prevHash + blobHash السابق عبر SHA-256، ملتقطاً أي تلاعب دون الاتصال بالخادم الأعلى.

أمان التزامن

لا يدعم D1 (قاعدة بيانات Cloudflare) المعاملات التفاعلية. إصدار تراخيص متزامن لنفس الاشتراك قد يتصادم على رقم التسلسل. يتعامل خادم الحساب مع هذا عبر:

  1. قراءة التسلسل التالي + تجزئة السلسلة السابقة.
  2. بناء الكتلة والتوقيع عليها بذلك التسلسل المضمَّن.
  3. إدراج إدخال دفتر الأستاذ بـ onConflictDoNothing.
  4. إذا أعاد الإدراج 0 صف مُغيَّر، فقد طالب طلب آخر بالتسلسل - أعِد اكتساب التسلسل وإعادة البناء وإعادة التوقيع ثم أعِد المحاولة.
  5. بعد 10 محاولات فاشلة، يفشل بخطأ.

التفصيل الحرج: إعادة المحاولة تُعيد التوقيع على الكتلة. إعادة المحاولة الساذجة التي تُحدِّث فقط إدخال دفتر الأستاذ ستترك الكتلة الموقّعة برقم تسلسل قديم، مما يكسر السلسلة.

نقل البريد الإلكتروني

يمكن لخادم الحساب إرسال رسائل بريد إلكتروني تعاملية (روابط سحرية، إعادة تعيين كلمة المرور، إشعارات أمنية) عبر وسيلتَي نقل قابلتَيْن للإضافة:

وسيلة النقلالتكوين
ses (الافتراضي)AWS_SES_ACCESS_KEY_ID وAWS_SES_SECRET_ACCESS_KEY وAWS_SES_REGION وAWS_SES_FROM
smtpEMAIL_TRANSPORT=smtp وSMTP_HOST وSMTP_PORT وSMTP_USER وSMTP_PASSWORD وSMTP_SECURE وSMTP_FROM

تعمل كلتا وسيلتَي النقل لعمليات النشر السحابية والمحلية. اختر ما يناسب بنيتك التحتية: AWS SES بحساب AWS الخاص بك، أو أي خادم SMTP (Microsoft Exchange أو Postfix أو SendGrid أو Mailgun وغيرها).

يُختار وسيلة النقل عند بدء التشغيل عبر متغير البيئة EMAIL_TRANSPORT. يستخدم SMTP تجميع الاتصالات والتحميل الكسول، لذا لا تتهيأ مكتبة عميل SMTP إلا إذا اخترت SMTP.

جميع قوالب البريد الإلكتروني وواجهة API العامة للبريد الإلكتروني متطابقة عبر وسيلتَي النقل.

الوثائق ذات الصلة