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