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

إنشاء معرّف لامركزي

تمنح هذه الصفحة المؤسسة acme معرّفًا لامركزيًا (DID) يستطيع أي شخص حلّه والتحقق منه دون أن يثق بـ OktoID. في النهاية، يكون لدى acme معرّف لامركزي بالشكل did:webvh:<scid>:acme.identity.local.oktoid.app، ويُقدَّم سجل المعرّف الخاص به على مضيفه الخاص، ويكون محلِّل مستقل قد جلب ذلك السجل وتحقّق من كل توقيع فيه.

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

ما هو سجل المعرّف، وما الذي يتحقق منه المحلِّل فيه، تجده في كيف يُحَلّ سجل did:webvh.

قبل أن تبدأ​

تحتاج إلى بيئة التشغيل المحلية وإلى المؤسسة acme من التثبيت وتسجيل الدخول، الخطوات من 1 إلى 6. اعمل في الطرفية نفسها: فهذه الصفحة تعيد استخدام CA وAPI وTOKEN وANCHOR وACME، والمساعدَين key وfield المعرفَين هناك.

لا يلزم قبول انضمام acme إلى شجرة الثقة أولًا. وإن قُبل انضمامها، كما في قبول انضمام مؤسسة ونشرها، فإن مستند المعرّف اللامركزي يذكر عنوان تكوين الكيان (entity configuration) الخاص بها، وذلك العنوان يجيب.

تُرسَل الطلبات إلى واجهة API العامة، api.local.oktoid.app، بالحساب الجذري، متصرّفًا نيابةً عن مؤسسة الارتكاز الجذرية. في بيئة التشغيل المحلية، يحمل الحساب الجذري SOLE_ADMIN، التي تشمل صلاحيات المعرّف اللامركزي الأربع: identifier.create وidentifier.read وidentifier.publish وidentifier.update. أما في التثبيت، فيحملها مسؤول المؤسسة من خلال ENTITY_ADMIN، وينشئ معرّف المؤسسة اللامركزي متصرّفًا نيابةً عن تلك المؤسسة. صفحة الحزم تعدّد ما تحمله كل حزمة.

ظهور 401 في أي خطوة يعني أن رمز الوصول انتهت صلاحيته؛ فعمره خمس دقائق. سجّل الدخول من جديد كما يبيّن الدرس التطبيقي السابق لهذا، ثم أعد الطلب.

1. أنشئ المعرّف اللامركزي​

IDENT=$(curl -sS --fail-with-body --cacert "$CA" "$API/identifiers" \
-H "Authorization: Bearer $TOKEN" \
-H "Oktoid-Acting-For: $ANCHOR" \
-H "Idempotency-Key: $(key)" \
-H 'Content-Type: application/json' \
-d "{\"organizationId\":\"${ACME:?create the organization first}\"}" \
| field id)
echo "${IDENT:?step 1 made no identifier: read the reply above}"

تجيب واجهة API بـ 202 مع المعرّف اللامركزي، ومع ترويسة Location تذكر أين يُقرأ: /api/v1/identifiers/<id>. قيمة status هي placing. وقيمة method هي webvh، وهي الافتراضية، وقيمة host هي acme.identity.local.oktoid.app، المشتق من الجزء الأخير من مسار المؤسسة. وليس له did بعد.

الجواب 202 لا 201، لأن الموجود حتى الآن هو السجل فقط. تضع المنصة المؤسسة أولًا على وكيل، وهو الخدمة التي تحفظ مفاتيحها على رمز عتادي وتوقّع نيابةً عنها. ويحدث ذلك في الخلفية.

يُتخذ هنا خياران، وكلاهما دائم لهذا المعرّف اللامركزي:

  • الطريقة. إغفال method يعطي webvh: سجل معرّف موقّع يُلحَق به فقط، فتستطيع الجهة المُتحقِّقة تمييز تدوير المفتاح من الاستبدال. و"method": "web" يعطي did:web: مستندًا واحدًا قابلًا للاستبدال، بلا تاريخ ولا سبيل للاسترداد، للجهات المُتحقِّقة التي لا تحلّ غيره. ويُقدَّم معرّف did:webvh أيضًا بصيغة did:web (الخطوة 5)، لذلك لا تحتاج معظم المؤسسات إلى web أبدًا. وقد لا يسمح التثبيت بـ web أصلًا.
  • المضيف. إغفال host يقدّم المعرّف اللامركزي على مضيف النشر الخاص بالمؤسسة. و"host": "id.acme.example" يذكر بدلًا من ذلك مضيفًا تتحكم فيه المؤسسة. ويجب أن يشير DNS الخاص به إلى المنصة قبل أن تنشر. وتحت نطاق المنصة نفسه، لا يُقبل إلا اسم تحت الاسم المختصر الخاص بالمؤسسة، مثل id.acme.local.oktoid.app.

للمؤسسة معرّف لامركزي واحد. وطلب ثانٍ لـ acme يُرفض بـ 409 و"code": "identifier.exists".

2. انتظر حتى يصبح جاهزًا​

اقرأ المعرّف اللامركزي حتى لا تعود قيمة status هي placing:

for i in $(seq 1 30); do
STATUS=$(curl -sS --fail-with-body --cacert "$CA" "$API/identifiers/${IDENT:?run step 1 first}" \
-H "Authorization: Bearer $TOKEN" \
-H "Oktoid-Acting-For: $ANCHOR" \
| field status)
echo "$STATUS"
[ "$STATUS" = placing ] || break
sleep 2
done

يطبع placing بضع مرات، ثم prepared.

القراءة هي ما يُمضي المعرّف اللامركزي قدمًا، فواصل القراءة. أول قراءة بعد وضع المؤسسة تطلب من الوكيل ثلاثة مفاتيح: مفتاح الإثبات الذي ستوقّع المؤسسة به البيانات الثبوتية، ومفتاحَي تحديث، أحدهما لتوقيع هذا الإصدار من سجل المعرّف والآخر مُلتزَم به للإصدار التالي. تنتظر القراءة ما يصل إلى ثانيتين لأجلها، وإلا أجابت بالمعرّف اللامركزي كما هو؛ والقراءة التالية تراها. والعميل الذي لا يقرأ أبدًا لا يحصل أبدًا على معرّف لامركزي جاهز.

prepared تعني أن المفاتيح موجودة وأن المستند قد أُنشئ. إذا انتهت الحلقة على placing، فالوكيل لم يجب بعد؛ نفّذ الحلقة من جديد. وإذا طبعت failed، فالمعرّف اللامركزي لا يمكن نشره: اقرأ failureReason الخاص به.

3. اقرأ ما سيُنشر​

PREVIEW=$(curl -sS --fail-with-body --cacert "$CA" "$API/identifiers/$IDENT/preview" \
-H "Authorization: Bearer $TOKEN" \
-H "Oktoid-Acting-For: $ANCHOR")
echo "$PREVIEW" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const p=JSON.parse(s);console.log("version",p.version);console.log("served at",p.servedAt.join(" "));console.log(JSON.stringify(JSON.parse(p.document),null,2));console.log("did.json:");console.log(JSON.stringify(JSON.parse(p.parallelDocument),null,2))})'

المعاينة هي المستند الدقيق الذي سيُوقَّع، على هيئة نص. يطبع الأمر المستند، ثم المستند الثاني الذي سيُقدَّم بجانبه. ويبدو الأول هكذا:

{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1"
],
"alsoKnownAs": [
"https://acme.identity.local.oktoid.app",
"did:web:acme.identity.local.oktoid.app"
],
"assertionMethod": [
"did:webvh:{SCID}:acme.identity.local.oktoid.app#key-1"
],
"authentication": [
"did:webvh:{SCID}:acme.identity.local.oktoid.app#key-1"
],
"id": "did:webvh:{SCID}:acme.identity.local.oktoid.app",
"verificationMethod": [
{
"controller": "did:webvh:{SCID}:acme.identity.local.oktoid.app",
"id": "did:webvh:{SCID}:acme.identity.local.oktoid.app#key-1",
"publicKeyMultibase": "<the assertion key>",
"type": "Multikey"
}
]
}

اقرأه بوصفه الشيء الذي توشك أن تجعله دائمًا:

  • {SCID} حرفي. المعرّف الذاتي التوثيق بصمةُ الإصدار الأول من سجل المعرّف، فلا يمكن أن يوجد قبل توقيع ذلك الإصدار. يكتب النشر قيمته حيث يقف {SCID} ولا يغيّر شيئًا آخر؛ وترفض المنصة النشر إن اختلف أي شيء آخر. والمعاينة تقول ذلك بنفسها: scid فيها null وscidPlaceholder فيها {SCID}.
  • مفتاح واحد. يذكر المستند مفتاح الإثبات ولا شيء غيره. لا يذكر خدمة ولا جهة تحكم أخرى. ومفاتيح التحديث ليست في المستند: فهي توقّع سجل المعرّف، لا البيانات الثبوتية.
  • alsoKnownAs يذكر معرّف الكيان الخاص بـ acme وصيغة did:web للمعرّف اللامركزي نفسه.
  • servedAt تذكر العنوانين اللذين سيكتبهما النشر: https://acme.identity.local.oktoid.app/.well-known/did.jsonl، أي سجل المعرّف، و.../.well-known/did.json، أي مستند did:web.

المستند الثاني، did.json، هو المستند نفسه مع المعرّف اللامركزي بصيغة did:web، وإضافة المعرّف بصيغة did:webvh إلى alsoKnownAs، والخدمتين اللتين تفرضهما الطريقة لكل معرّف did:webvh، وهما #files و#whois. ويذكر المفتاح نفسه.

تُدوَّن قراءة المعاينة على المعرّف اللامركزي: مَن قرأها، ومتى، وبصمة ما قرأه (previewedBy وpreviewedAt وpreviewedDigest). وهذا السجل هو ما تتحقق منه الخطوة 4.

4. انشره​

DID=$(curl -sS --fail-with-body --cacert "$CA" "$API/identifiers/$IDENT/publish" \
-X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Oktoid-Acting-For: $ANCHOR" \
-H "Idempotency-Key: $(key)" \
-H 'Content-Type: application/json' \
-d '{}' \
| field did)
echo "${DID:?step 4 published nothing: read the reply above}"

الجسم كائن JSON فارغ، {}. والطلب بلا جسم يُرفض بـ 400.

تجيب واجهة API بـ 200 مع المعرّف اللامركزي. status فيه published، وversion فيه 1، وأصبح له did هو did:webvh:<scid>:acme.identity.local.oktoid.app، الذي يطبعه الأمر.

يفعل النشر ما يلي، بهذا الترتيب، ويتوقف عند أول شيء يفشل:

  1. يتحقق من أن المستند المنتظر هو نفسه الذي عاينه شخص ما منذ أُنشئ. وإلا كان الجواب 409 مع "code": "identifier.not_previewed"؛ اقرأ المعاينة من جديد (الخطوة 3) ثم أعد المحاولة.
  2. يوقّع الوكيل الإصدار الأول من سجل المعرّف بمفتاح التحديث.
  3. تقارن المنصة ما وُقّع بما عُوين، وتتحقق من التوقيع ومن كل معامل في السجل نفسه. وأي اختلاف يُرفَض به النشر، ولا يُكتب شيء.
  4. يُكتب سجل المعرّف، ثم did.json.
  5. يُتلف مفتاح التحديث الذي وقّع. فالسجل قد التزم سلفًا بالمفتاح الذي سيوقّع الإصدار التالي، والمفتاح المستهلَك لا يقدر على توقيع شيء يقبله محلِّل.

5. حُلّه بمحلِّل لم تكتبه OktoID​

pnpm oktoid resolve-identifier "${DID:?run step 4 first}"

ينفّذ الأمر didwebvh-rs، وهو تطبيق الطريقة بلغة Rust من تطوير Decentralized Identity Foundation، في حاوية من حاويات بيئة التشغيل. ولا يشارك شيئًا من الشيفرة مع المكتبة التي توقّع بها OktoID. ويبني تشغيله الأول صورته، ويستغرق ذلك بضع دقائق.

يجلب سجل المعرّف عبر الحافة (edge)، كما يفعل أي عميل خارجي، ولا يثق بشيء سوى جذر التطوير الخاص ببيئة التشغيل. يعيد حساب SCID من الإصدار الأول، ويتحقق من كل بصمة وكل توقيع، ويتحقق من أن المفتاح الذي وقّع هو المفتاح الذي كان مُلتزَمًا به. ثم يطبع المعرّف اللامركزي، وversionId للإصدار الأخير، وversionTime الخاص به، والمستند، وفي موضع {SCID} الذي أظهرته المعاينة يظهر SCID الحقيقي:

did:webvh:<scid>:acme.identity.local.oktoid.app
versionId 1-<hash>
versionTime <when it was signed>
{
...
}

لا يخرج بالرمز 0 إلا إذا تحقّق سجل المعرّف كله. وأي محلِّل في أي مكان يطبّق did:webvh 1.0 يصل إلى الجواب نفسه من عنوان URL نفسه.

وتُحَلّ صيغة did:web أيضًا:

pnpm oktoid resolve-identifier did:web:acme.identity.local.oktoid.app

يطبع عنوان URL الذي اشتقّه من المعرّف اللامركزي، وهو https://acme.identity.local.oktoid.app/.well-known/did.json، ونوع المحتوى application/did+ld+json، والمستند. وهذا تحقق أضعف: فمستند did:web لا تاريخ له ولا توقيع، فلا يُتحقق إلا من محتواه.

لعرض سجل المعرّف الخام:

curl -sS --cacert "$CA" https://acme.identity.local.oktoid.app/.well-known/did.jsonl

سطر واحد، إصدار واحد. تتضمن parameters فيه "portable":true ومدخل nextKeyHashes: بصمة مفتاح التحديث الذي يجب أن يوقّع الإصدار التالي.

6. دوّر المفتاح​

التحديث يستبدل مفتاح الإثبات وينشر إصدارًا ثانيًا من سجل المعرّف. ويمر بالمعاينة والنشر نفسيهما:

curl -sS --fail-with-body --cacert "$CA" "$API/identifiers/$IDENT/update" \
-X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Oktoid-Acting-For: $ANCHOR" \
-H "Idempotency-Key: $(key)" \
-H 'Content-Type: application/json' \
-d '{}' \
| field status

يطبع prepared. فقد وُلّد مفتاح إثبات جديد، ومفتاح تحديث جديد ليُلتزم به. ويظل الإصدار 1 يُقدَّم بينما ينتظر هذا الإصدار.

الآن أعد الخطوة 3 والخطوة 4. version في المعاينة هو 2، ويحمل المستند SCID الحقيقي بدلًا من {SCID}، وينتهي معرّف المفتاح بـ #key-2. وبعد النشر، version في الجواب هو 2.

حُلّه من جديد:

pnpm oktoid resolve-identifier "$DID"

قد يظل الجواب هو الإصدار 1 مدة تصل إلى خمس دقائق. تخزّن الحافة سجل المعرّف مؤقتًا طوال المدة التي يسمح بها Cache-Control الخاص به، أي max-age=300، والمحلِّل الذي يسأل خلال تلك الفترة يحصل على النسخة التي قُدّمت سلفًا. ولا يزال الإصدار 1 سجل معرّف صالحًا: فهو الإصدار 2 دون سطره الأخير. انتظر، ثم حُلّه من جديد؛ عندها يبدأ versionId بـ 2-.

مفتاح الإثبات القديم لم يعد في المستند. وهو لا يُتلف: يبقى على الرمز العتادي للوكيل، مدرجًا على المعرّف اللامركزي بوصفه متقاعدًا (retiredAssertionKeyIds). وأي شيء وُقّع به قبل التدوير يُتحقق منه مقابل نصفه العام، الذي يبقى في الإصدار 1 من سجل المعرّف.

عندما يُرفض طلب​

كل رفض يأتي بصيغة application/problem+json مع code يسمّيه.

الحالةcodeما معناهما العمل
404لا يوجدالمعرّف اللامركزي أو المؤسسة غير موجود، أو يقع خارج المؤسسة التي تتصرّف نيابةً عنها. ويُجاب عن الحالتين بالطريقة نفسها عن قصدتحقّق من المعرّف (id) ومن Oktoid-Acting-For
409identifier.existsللمؤسسة معرّف لامركزي سلفًااقرأه: GET /api/v1/identifiers?organization=<id>
409identifier.not_previewedالنشر دون قراءة المعاينة الحاليةالخطوة 3، ثم الخطوة 4
409identifier.not_preparedلا شيء ينتظر أن يُنشرالخطوة 2، أو الخطوة 6 لإصدار جديد
409identifier.update_in_progressهناك تحديث ينتظر سلفًا أن يُعاين ويُنشر؛ ولا يجري إلا تحديث واحد في كل مرةالخطوة 3، ثم الخطوة 4، للتحديث المنتظر؛ ثم ابدأ الذي يليه
409identifier.host_unverifiedمضيف ذكرته لا يشير إلى المنصة بعداجعل DNS الخاص به يشير إلى المنصة، ثم انشر من جديد
409identifier.host_takenعند الإنشاء: مضيف النشر الخاص بالمؤسسة يقدّم سلفًا معرّفًا لامركزيًا آخر. عند النشر: نُشر معرّف لامركزي آخر على المضيف الذي ذكرته أولًا، وأصبح هذا المعرّف اللامركزي نهائيًا ولن يُنشر أبدًاعند الإنشاء، اذكر مضيفًا على نطاق المؤسسة نفسها. وعند النشر، أنشئ معرّفًا لامركزيًا جديدًا على مضيف آخر. ولا تُعد الطلب دون تغيير
409identifier.organization_inactiveالمؤسسة غير نشطةأعد تنشيط المؤسسة، ثم أنشئ المعرّف اللامركزي
400identifier.method_not_permittedهذا التثبيت لا يسمح بالطريقة التي طلبتهاأغفل method
400identifier.host_refusedالمضيف الذي ذكرته لا يجوز أن يقدّم معرّفًا لامركزيًا: فهو ليس اسم DNS بأحرف صغيرة (عنوان، أو منفذ، أو punycode غير صالح)، أو هو تحت نطاق المنصة نفسه لكنه ليس تحت الاسم المختصر الخاص بالمؤسسةأغفل host، أو اذكر مضيفًا على نطاق المؤسسة نفسها
400identifier.host_invalidالاسم المختصر للمؤسسة لا يشكّل اسم مضيف صالحًا تحت نطاق المنصة، فلا مضيف نشر لهااذكر مضيفًا بـ host، أو أعد تسمية الاسم المختصر للمؤسسة
502deadline.exceededخدمة من خدمات المنصة لم تجب في الوقت المناسب. والخلل من المنصة، لا من الطلبأعد المحاولة مع تأخير متدرّج وIdempotency-Key نفسه
502identifier.placement_pending وidentifier.agent_unavailable وبقية رموز الخدمة غير المتاحةالوكيل، أو خدمة أخرى تحتاجها الخطوة، لم يجب. ولم يُنشر شيءانتظر، وأعد المحاولة بـ Idempotency-Key نفسه
503module.unavailableالخدمة خلف المسار لا تعملانتظر، وأعد المحاولة بـ Idempotency-Key نفسه
500لا يوجدشيء لم تتوقعه المنصة، مثل مستند موقّع يختلف عن المعاينة. ولم يُنشر شيءأبلغ مُشغّل المنصة في هذا التثبيت عنه، مع correlationId من الجواب