מפרט הפרוטוקול של qub
qub הוא פרוטוקול להתחייבויות זמניות קריפטוגרפיות: מערכת לאטימת מילים לתאריך עתידי ולהוכחה, כאשר תאריך זה מגיע, מה בדיוק נאמר ומתי.
שלושה אבני בניין מאפשרים את זה. drand הוא משואת אקראיות מבוזרת — תאריך הגילוי נאכף על ידי הפיזיקה, לא על ידי רצונו הטוב של גורם כלשהו. אחסון ציבורי קבוע הוא מאגר ציבורי חסין שיבוש — אף גורם אינו יכול לערוך או למחוק qub לאחר שנאטם. ML-DSA-65 היא חתימה דיגיטלית פוסט-קוונטית — כל qub קשור לזוג מפתחות שהסוד שלו לעולם אינו עוזב את מכשיר המחבר.
יחד, אבני בניין אלה יוצרים אמירה שהיא נעולת-זמן, מעידה-על-שיבוש, וניתנת לייחוס — קבלה שערכה גדל ככל שמשתפרת יכולתו של העולם לזייף את העבר.
שאר המסמך הוא המפרט הנורמטיבי הנדרש למימושים תואמי-הדדיות.
מפרט פרוטוקול qub
| שדה | ערך |
|---|---|
| גרסה | 1.0 (גרסת פרוטוקול 0x01, גרסת עטיפה חיצונית 0x01) |
| תאריך | 2026-05-01 |
| סטטוס | טיוטה |
| נסקר עד | 2026-05-01 |
מסמך זה הוא מפרט הפרוטוקול הנורמטיבי עבור מערכת ההתחייבות הזמנית qub. הוא מגדיר מבני נתונים, כללי סריאליזציה, נוסחאות גזירה ונהלי אימות הנדרשים למימושים תואמי-הדדיות.
היקף: שכבת הפרוטוקול נייטרלית-שפה באופן מכוון — גוף ה-qub הוא טקסט גלוי / markdown / בתי pact אטומים, ורינדור מודע-לוקאל הוא באחריות הצופה (אפליקציית הרשת qub.social, iframe של <qub-embed>, לקוחות MCP וכו').
1. סימון ומוסכמות
| סימון | משמעות |
|---|---|
u8, u64, i64 |
מספרים שלמים ללא סימן / עם סימן ברוחב סיביות מוגדר |
[u8; N] |
מערך בתים באורך קבוע של N בתים |
Vec<u8> |
מערך בתים באורך משתנה |
Option<T> |
ערך מטיפוס T, או נעדר |
String |
מחרוזת טקסט UTF-8, מנורמלת ב-NFC |
| ` | |
SHA3-256(x) |
גיבוב NIST SHA3-256 של מחרוזת הבתים x (FIPS 202) |
ceil(x) |
פונקציית תקרה: המספר השלם הקטן ביותר ≥ x |
| CBOR | Concise Binary Object Representation (RFC 8949) |
| big-endian | הבית המשמעותי ביותר ראשון |
כל המספרים השלמים בבניית טרום-תמונות מקודדים כמערכי בתים big-endian ברוחב קבוע (i64 → 8 בתים, u8 → בית 1) אלא אם צוין אחרת.
כל חותמות הזמן הן שניות Unix ב-UTC.
2. מבני נתונים
2.1 ComposeQub (מצב יוצר בזיכרון)
לא מסורייל ל-CBOR. לא נכתב לאחסון קבוע. מקומי לאפליקציית היוצר.
ComposeQub {
draft_id: [u8; 16], // Random, generated locally
created_at: i64, // Unix seconds UTC
unlock_at: Option<i64>, // Unix seconds UTC; None while composing
visibility: u8, // 0x01 = public (only value in MVP)
content_type: u8, // 0x01 = text (only value in MVP)
plaintext: Vec<u8>, // UTF-8 qub body
sender_label: Option<String>, // Decorative display name; not authenticated
status: DraftStatus, // Composing | Sealed | Uploaded | Failed
}
2.2 QubEnvelope (מטען מפוענח)
מסורייל באמצעות CBOR קנוני (§3). מוצפן בתוך ה-SealedQub. זהו המבנה המוכיח את שלמות התוכן לאחר פענוח.
QubEnvelope {
version: u8, // Protocol major version (0x01 for v1)
qub_id: [u8; 32], // Derived (see §4.1)
content_type: u8, // Content type registry (see §6)
created_at: i64, // Unix seconds UTC
unlock_at: i64, // Unix seconds UTC
outcome_at: Option<i64>, // V1.1 — when reality renders judgment (verdict-uplift-plan §3.1)
sender_label: Option<String>, // Decorative; not authenticated in MVP
reply_to: Option<[u8; 32]>,// Parent qub_id for reply chains; not in qub_id preimage; not signed (see §9.3)
body: Vec<u8>, // Content payload (UTF-8 for text, CBOR for pact)
body_hash: [u8; 32], // SHA3-256(body) (see §4.2)
sig_alg: u8, // Signature algorithm (see §9.2)
author_signature: Option<Vec<u8>>, // Set when sig_alg != 0x00
author_pubkey: Option<Vec<u8>>, // Set when sig_alg != 0x00
cosigner_pubkey: Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
cosigner_signature: Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
}
בסיס (qub טקסט לא חתום): version = 0x01, content_type = 0x01, sig_alg = 0x00, כל שדות ה-Option נעדרים.
תצורות v1 אחרות: content_type = 0x03 (גוף pact, ראו §6.1); sig_alg = 0x01 (ML-DSA-65) עם author_signature ו-author_pubkey נוכחים (ראו §9.3); cosigner_pubkey ו-cosigner_signature נוכחים יחד עבור pacts חתומים-במשותף (ראו §9.7); reply_to מוגדר ל-qub_id של ה-qub האב עבור qubs של שרשרת תגובות (ראו §9.3 להשלכות היקף-החתימה).
2.3 SealedQub (פורמט קווי קנוני)
מסורייל באמצעות CBOR קנוני (§3). נכתב לאחסון קבוע. זהו הארטיפקט שעל-השרשרת.
SealedQub {
version: u8, // Protocol major version (0x01 for v1)
qub_id: [u8; 32], // Same as QubEnvelope.qub_id
visibility: u8, // 0x01 = public; v1 viewers reject other values
unlock_at: i64, // Unix seconds UTC
outcome_at: Option<i64>, // V1.1 — surfaced on the verdict-watch CTA
// before reveal; mirrors QubEnvelope.outcome_at;
// bound to qub_id via the §4.1 preimage.
drand_chain_id: String, // drand chain hash (hex string)
drand_round: u64, // Target drand round number
tlock_ciphertext: Vec<u8>, // tlock-encrypted QubEnvelope CBOR bytes
recipient_pubkey: Option<[u8; 32]>,// Reserved field; accepted by canonical CBOR
// but not interpreted by the v1 reference viewer
title: Option<String>, // Plaintext title surfaced on the viewer
// countdown before reveal. Bound to qub_id
// via title_hash (§4.1). 1..=100 NFC code
// points, no control characters.
}
2.4 RevealedQub (מצב אפליקציית הצופה)
לא מסורייל ל-CBOR. מקומי לאפליקציית הצופה. נבנה לאחר פענוח ואימות מוצלחים.
RevealedQub {
qub_id: [u8; 32],
arweave_tx_id: String,
visibility: u8,
content_type: u8,
created_at: i64,
unlock_at: i64,
outcome_at: Option<i64>, // V1.1 — מועבר הלאה מ-QubEnvelope.outcome_at / SealedQub.outcome_at; מניע את גוש מעקב-פסק-הדין בעמוד החשיפה (verdict-uplift-plan §5.1)
drand_chain_id: String,
drand_round: u64,
sender_label: Option<String>,
title: Option<String>, // Carried forward from SealedQub.title
reply_to: Option<[u8; 32]>,
body: Vec<u8>,
body_hash: [u8; 32],
body_hash_verified: bool,
author_signature: Option<Vec<u8>>,
author_pubkey: Option<Vec<u8>>,
signature_verified: Option<bool>,
cosigner_pubkey: Option<Vec<u8>>,
cosigner_signature: Option<Vec<u8>>,
cosigner_verified: Option<bool>,
}
3. פרופיל CBOR קנוני
כל סריאליזציה של SealedQub ו-QubEnvelope חייבת לעמוד בפרופיל זה. שני מימושים שניתן להם אותו מבנה לוגי חייבים להפיק בתים זהים.
3.1 כללי קידוד
| כלל | מפרט |
|---|---|
| תקן | RFC 8949 §4.2.1 (דרישות קידוד דטרמיניסטי בסיסיות) |
| סדר מפתחות במפה | ממוין לפי אורך בתים מקודד תחילה (קצר לפני ארוך), ואז לקסיקוגרפית (בית-אחר-בית עבור קידודים באותו אורך) |
| קידוד מספרים שלמים | הצורה הקצרה ביותר: 0–23 בבית הראשוני; 24–255 ב-2 בתים; 256–65535 ב-3 בתים; וכו' |
| קידוד אורך | אורכים מוגדרים בלבד. אין מערכים, מפות, מחרוזות בתים או מחרוזות טקסט באורך בלתי-מוגדר (additional info = 31 אסור). |
| תגיות | אין תגיות CBOR (טיפוס ראשי 6 אסור). |
| נקודה צפה | אין floats (טיפוס ראשי 7 ערכים 0xF9–0xFB אסורים). |
| מחרוזות טקסט | מקודדות ב-UTF-8, מנורמלות ב-NFC (Unicode Normalization Form C). |
| מחרוזות בתים | בתים גולמיים. אין קידוד base64 בשכבת ה-CBOR. |
| מפתחות כפולים | דחו עם שגיאה. מנתחים חייבים שלא לקבל בשקט מפתחות מפה כפולים. |
| ערכים פשוטים | רק true (0xF5), false (0xF4) ו-null (0xF6) מותרים. |
| שדות אופציונליים | שדות אופציונליים נעדרים מושמטים ממפת ה-CBOR לחלוטין (לא מקודדים כ-null). שדות אופציונליים נוכחים נכללים בסדר מפתחות ממוין. |
3.2 סדרי מפתחות קנוניים מאומתים
סדרי מפתחות אלה הם נורמטיביים. מימושים חייבים לפלוט מפתחות בדיוק בסדר זה. אסרציות ניפוי באגים מומלץ שיאמתו את הסדר בבנייות שאינן release.
QubEnvelope (גרסה 0x01, לא חתום, כל השדות האופציונליים נעדרים):
"body" (5 encoded bytes)
"qub_id" (7 encoded bytes)
"sig_alg" (8 encoded bytes)
"version" (8 encoded bytes)
"reply_to" (9 encoded bytes) ← only if present (reply chains)
"body_hash" (10 encoded bytes)
"unlock_at" (10 encoded bytes)
"created_at" (11 encoded bytes)
"outcome_at" (11 encoded bytes) ← only if present (V1.1 verdict mechanic)
"content_type" (13 encoded bytes)
"sender_label" (13 encoded bytes) ← only if present
"author_pubkey" (14 encoded bytes) ← only if present
"cosigner_pubkey" (16 encoded bytes) ← only if present (pact cosign)
"author_signature" (17 encoded bytes) ← only if present
"cosigner_signature" (19 encoded bytes) ← only if present (pact cosign)
גזירת סדר המפתחות של QubEnvelope: כל מפתח הוא מחרוזת טקסט CBOR. אורך מקודד = בית 1 כותרת + אורך המחרוזת (עבור מחרוזות מתחת ל-24 בתים). מיינו לפי האורך המקודד הכולל תחילה, ואז לקסיקוגרפית עבור מפתחות באותו אורך.
SealedQub (גרסה 0x01, ציבורי, ללא נמען):
"title" (6 encoded bytes) ← only if present
"qub_id" (7 encoded bytes)
"version" (8 encoded bytes)
"unlock_at" (10 encoded bytes)
"outcome_at" (11 encoded bytes) ← only if present (V1.1 verdict mechanic)
"visibility" (11 encoded bytes)
"drand_round" (12 encoded bytes)
"drand_chain_id" (15 encoded bytes)
"recipient_pubkey" (17 encoded bytes) ← only if present
"tlock_ciphertext" (17 encoded bytes)
PactTerms (גוף pact, content_type 0x03):
"notes" (6 encoded bytes) ← only if present
"terms" (6 encoded bytes)
"title" (6 encoded bytes)
"party_a" (8 encoded bytes)
"party_b" (8 encoded bytes)
"pact_version" (13 encoded bytes)
PactTerm (שורה של מערך ה-terms):
"key" (4 encoded bytes)
"value" (6 encoded bytes)
PartyIdentifier (מפת party_a / party_b):
"label" (6 encoded bytes)
"contact" (8 encoded bytes) ← only if present
3.3 הפניית קידוד בתים
| טיפוס | קידוד CBOR | דוגמה |
|---|---|---|
| גיבוב SHA3-256 (32 בתים) | 0x58 0x20 + 32 bytes |
body_hash, qub_id |
| חותמות זמן (i64) | טיפוס ראשי 0 (חיובי) או 1 (שלילי), הקידוד הקצר ביותר | שניות Unix |
| גרסה (u8, ערך 1) | 0x01 (בית בודד) |
|
| טיפוס תוכן (u8, ערך 1) | 0x01 (בית בודד) |
|
| sig_alg (u8, ערך 0) | 0x00 (בית בודד) |
|
| חתימת ML-DSA-65 (3,309 בתים) | 0x59 0x0C 0xED + 3,309 bytes |
author_signature, cosigner_signature |
| מפתח ציבורי ML-DSA-65 (1,952 בתים) | 0x59 0x07 0xA0 + 1,952 bytes |
author_pubkey, cosigner_pubkey |
4. גזירות נורמטיביות
4.1 qub_id
ה-qub_id מזהה ייחודית qub וקושר את ה-QubEnvelope ל-SealedQub. הוא נגזר באופן דטרמיניסטי מתוכן המעטפה.
qub_id = SHA3-256(
"QUB_ID_V2" || // domain separator: ASCII bytes [0x51 0x55 0x42 0x5F 0x49 0x44 0x5F 0x56 0x32] (9 bytes) + 0x00 padding (1 byte) = 10 bytes
version || // u8 (1 byte)
content_type || // u8 (1 byte)
created_at || // i64 big-endian (8 bytes)
unlock_at || // i64 big-endian (8 bytes)
outcome_at_or_zero || // i64 big-endian (8 bytes; 0 when outcome_at is absent)
drand_round || // u64 big-endian (8 bytes)
body_hash || // [u8; 32] (32 bytes)
title_hash // [u8; 32] (32 bytes; absent-sentinel = [0u8; 32])
)
// Total preimage: 108 bytes → 32-byte output
קידוד מפריד התחום: המחרוזת "QUB_ID_V2" היא 9 בתי ASCII. בית ריפוד 0x00 בודד מצורף כדי להגיע ל-10 בתים ליישור. מימושים חייבים להשתמש בדיוק ב-10 בתים אלה: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].
קידוד outcome_at: V1.1 הרחיב את הטרום-תמונה מ-92 ל-100 בתים כדי לקפל את השדה האופציונלי outcome_at לתוך הקשירה. outcome_at נעדר מקודד כ-8 בתי אפס; מאמתי הפרוטוקול דוחים outcome_at <= 0 בכל מקום כך שערך-סנטינל זה אינו יכול להתנגש עם ערך לגיטימי. ראו §3.2 (פורמט קווי) ואת tasks/verdict-uplift-plan.md שבעץ למנגנון הפסיקה שמניע שדה זה.
קידוד drand_round: V1.2 הרחיב את הטרום-תמונה מ-100 ל-108 בתים כדי לקפל את drand_round (סבב ה-drand היעד, §4.3) לתוך הקשירה, והעלה את מפריד התחום ל-QUB_ID_V2. זה קושר את סבב ה-timelock לתוך זהות ה-qub: שער אינו יכול לקשור מחדש את הצופן לסבב שונה (למשל סבב שכבר חלף) מזה שמשתמע מ-unlock_at המוצג. נוהל השחרור (§8) מאמת בנוסף שהסבב הצרוב בתוך משבצת הצופן של ה-tlock תואם ל-unlock_round(unlock_at), כך שזמן השחרור המוצג הוא באופן מוכח הסבב השולט בפענוח.
תכונות:
- שינוי כל שדה ב-QubEnvelope (גוף, חותמות זמן, טיפוס תוכן, גרסה) מפיק qub_id שונה.
- ה-qub_id מחושב לפני ההצפנה. גם QubEnvelope וגם SealedQub נושאים את אותו qub_id. הצופה מאמת שהם תואמים לאחר הפענוח.
- qub_id אינו תלוי ב-
sender_label,author_signatureאוauthor_pubkey. משמעות הדבר היא שאותו תוכן שנאטם באותו זמן מפיק את אותו qub_id ללא קשר למי שחותם עליו. - שינוי ה-
titleשל SealedQub (כשכל השאר קבוע) משנה אתqub_idדרךtitle_hash. גשר אינו יכול לפיכך להחליף את הכותרת הגלויה המוצגת בספירה לאחור מבלי לפסול את זהות ה-qub. - שינוי ה-
outcome_atשל SealedQub (כשכל השאר קבוע) משנה אתqub_idדרך הטרום-תמונה. גשר אינו יכול להחליף את תאריך הפסיקה הטרום-שחרור המוצג בספירה לאחור מבלי לפסול את זהות ה-qub.
4.2 body_hash
body_hash = SHA3-256(body)
כאשר body הוא מטען התוכן הגולמי מסוג Vec<u8>. עבור qubs טקסט, זהו גוף ה-qub המקודד ב-UTF-8.
4.2.1 title_hash
title_hash = SHA3-256(NFC(title).utf8_bytes) if title is present
title_hash = [0u8; 32] if title is absent
כאשר title היא הכותרת האופציונלית בטקסט גלוי המוצגת בספירה לאחור של הצופה לפני הגילוי (ראו §3.2). נרמול NFC רץ בזמן הגיבוב כך שהתקציר יציב על פני רצפי נקודות-קוד שקולים-ויזואלית. ערך הסנטינל של כולו-אפסים שמור למקרה הנעדר; מחרוזת ריקה נדחית בגבול ה-CBOR הקנוני כקידוד לא-קנוני של "נעדר" (הקידוד הקנוני משמיט את השדה לחלוטין).
4.3 מיפוי סבב-שחרור
drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
| פרמטר | מקור | דוגמה |
|---|---|---|
unlock_at |
שניות Unix UTC שנבחרו על ידי המשתמש | 1735689600 (2025-01-01 00:00:00 UTC) |
chain_genesis_time |
מידע שרשרת drand (genesis_time) |
1595431050 |
chain_period_seconds |
מידע שרשרת drand (period) |
30 |
פעולת ה-ceil() בוחרת את סבב ה-drand הראשון שזמן הגילוי שלו ≥ unlock_at. זה מבטיח שה-qub אינו הופך לבר-פענוח לפני זמן השחרור שנבחר.
מקרה קצה: אם (unlock_at - chain_genesis_time) מתחלק בדיוק ב-chain_period_seconds, התוצאה היא אותו סבב מדויק — ה-qub נפתח בדיוק בזמן הגילוי של אותו סבב.
אימות: unlock_at חייב להיות בעתיד בזמן האטימה. unlock_at חייב שלא להיות יותר מ-10 שנים מ-created_at (כדי להגביל סיכון תלות-drand ארוכת-טווח; הממשק מומלץ שיזהיר עבור תאריכי שחרור מעבר ל-2 שנים).
5. Newtypes של פורמט קווי
Newtypes של פורמט קווי מספקים בטיחות בזמן הידור נגד בלבול של בתי CBOR עם JSON, טקסט גלוי גולמי או קידודי בתים אחרים.
| טיפוס | מכיל | מיוצר על ידי | נצרך על ידי |
|---|---|---|---|
SealedQubCbor |
CBOR קנוני של SealedQub | serialize_sealed_qub() |
העלאת אחסון-קבוע, שליפת צופה |
QubEnvelopeCbor |
CBOR קנוני של QubEnvelope | serialize_qub_envelope() |
קלט הצפנת tlock, פלט פענוח tlock |
5.1 כללי בנייה
// Production code — only through CBOR serialisers:
let sealed = SealedQubCbor::from_encoded(cbor_bytes);
// There is deliberately NO From<Vec<u8>> implementation.
// You cannot accidentally wrap arbitrary bytes in a wire format type.
// Accessing raw bytes:
let bytes: &[u8] = sealed.as_bytes();
let bytes: Vec<u8> = sealed.into_bytes();
5.2 אימות בעת בנייה
from_encoded() מומלץ שיאמת שהקלט מתחיל בכותרת מפת CBOR תקפה. אימות מבני מלא מתרחש בזמן הניתוח, לא בזמן הבנייה, כדי להימנע מניתוח כפול.
6. מרשם טיפוסי תוכן
| ערך | טיפוס | גודל גוף מרבי | הערות |
|---|---|---|---|
0x00 |
שמור (לא תקף) | — | אסור לשימוש |
0x01 |
טקסט פשוט (UTF-8, Markdown מוגבל) | 50 KB בתשלום / 10 KB חינם | ראו §10 לכללי רינדור. החלוקה חינם / בתשלום נאכפת על ידי שירות ההעלאה; תקרת-הקשיחות ברמת הפרוטוקול היא 50 KB. |
0x02 |
שמור (עתידי) | — | מוקצה עבור טיפוס תוכן עתידי; לא תקף ב-v1. צופים חייבים לדחות אותו לפי הכלל שלהלן. |
0x03 |
Pact (הסכם דו-צדדי, גוף CBOR) | 100 KB | הגוף הוא PactTerms בקידוד CBOR קנוני (§6.1). חתימת מאשר-משותף לפי §9.7. |
0x04 |
פסק דין (ציון עצמי של היוצר, גוף CBOR) | 8 KB | הגוף הוא VerdictBody בקידוד CBOR קנוני (§6.2). נפלט אך ורק על ידי ה-intent המערכתי verdict. הקשר ל-qub ההורה נמצא בתג Arweave Parent-Tx-Id, ולא בגוף. ראו verdict-uplift-plan §3.4. |
צופים חייבים לדחות טיפוסי תוכן לא ידועים עם שגיאה ברורה הנראית למשתמש. צופים חייבים שלא לנסות לרנדר טיפוסים לא ידועים כטקסט.
6.1 גוף Pact (content_type = 0x03)
גוף pact הוא הקידוד הקנוני ב-CBOR של ערך PactTerms:
PactTerms {
pact_version: u8, // 0x01 for structured/v1
title: String, // ≤ 200 bytes, NFC
terms: Vec<PactTerm>, // ≤ 20 rows
party_a: PartyIdentifier, // initiator
party_b: PartyIdentifier, // counter-signer
notes: Option<String>, // ≤ 5,000 bytes, NFC; absent key if none
}
PactTerm { key: String (≤ 100), value: String (≤ 2,000) } // NFC on both sides
PartyIdentifier{ label: String (≤ 100), contact: Option<String (≤ 320)> }
סדרי מפתחות CBOR קנוניים לכל שלוש המפות ניתנים ב-§3.2. סך ה-CBOR המסורייל של ה-pact חייב שלא לעבור 100 KB (תואם ל-§6).
מבחין סכמה. השורה הראשונה ב-terms עבור pact מסוג structured/v1 חייבת להיות { key: "pact_schema", value: "structured/v1" }. שורות ללא סמן זה הן pacts "מותאמים אישית" ואינן מקבלות אימות מובנה או רינדור מודע-סכמה.
משבצות אישור מוקפאות. pacts מסוג structured/v1 נושאים בדיוק ארבע שורות אישור תחת המפתחות הבאים:
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
ה-value לכל אחת הוא אחת משמונה מחרוזות אנגלית מוקפאות שנבחרות לפי הזוג (role, kind), כאשר role ∈ { seller, buyer, provider, client } ו-kind ∈ { standard, capacity }. המחרוזות עצמן הן נתוני פרוטוקול נורמטיביים — חתימות ה-ML-DSA-65 של שני הצדדים מתחייבות לבתים המדויקים דרך body_hash. הן אינן מתורגמות; הגוף החתום הוא נייטרלי-שפה. כל שינוי ניסוח מצריך גרסת סכמה חדשה (structured/v2).
שמונה המחרוזות, החיפוש שלהן (acknowledgement_for(role, kind)) והרציונל לכל אחת מקובעים על ידי מימוש-ההפניה. מימושים תואמים חייבים לפלוט ערכי אישור זהים-בתים; בדיקות body-hash של SHA3-256 על golden-fixture המכסות את כל ארבעת צירופי התפקידים תופסות כל סטייה.
סדר תצוגה בצופה. מחרוזות האישור מכילות ביטויים כגון "described above", המניחים ששורות התיאור / ההיקף מרונדרות לפני האישורים. צופים חייבים לרנדר את מערך ה-terms בסדר CBOR; שינוי סדר שובר את סמנטיקת הפרוזה.
איש קשר של הצד שכנגד. כאשר ה-contact של צד B הוא כתובת דוא"ל תקפה, שירות העלאת ה-qub שולח אוטומטית דוא"ל הזמנה לסקירה / חתימה-משותפת בזמן ההכנה וקושר את החתימה-המשותפת הסופית לאימות אותה כתובת (§9.7). pacts שאיש הקשר של צד B שלהם נעדר עדיין יכולים להיחתם במשותף, אך רק דרך ערוץ מחוץ-לפס — השירות מסרב לבקשות חתימה-משותפת שאינן יכולות להפיק סמן אימות-דוא"ל תואם בן 15 דקות.
6.2 גוף פסק דין (content_type = 0x04)
גוף פסק דין הוא הקידוד הקנוני ב-CBOR של ערך VerdictBody:
VerdictBody {
verdict_version: u8, // 0x01 for structured/v1
outcome: u8, // 1=Right · 2=Partial · 3=Wrong · 4=Unfalsifiable
reflection: Option<String>, // ≤ 2,000 bytes NFC; "what changed, what did you learn"
evidence_url: Option<String>, // ≤ 2,048 bytes; HTTPS only; absent key when omitted
}
סדר מפתחות CBOR קנוני:
"outcome" (8 encoded bytes)
"reflection" (11 encoded bytes) ← only if present
"evidence_url" (13 encoded bytes) ← only if present
"verdict_version" (16 encoded bytes)
סך בתי ה-CBOR המסורייל של פסק הדין חייב שלא לעבור 8 KB (תואם לשורה במרשם לעיל).
Enum של תוצאה. הבית על ה-wire הוא נייטרלי-intent; ארבע הקטגוריות Right / Partial / Wrong / Unfalsifiable מכסות את מרחב התוצאות של כל intent נושא-פסק-דין. תוויות ספציפיות-ל-intent ("צדקתי" / "עמדתי בזה" / "סופק" / "אושר" ל-Right וכו') הן עניין רינדור בצד הצופה הנפתר מול ה-intent של ה-qub ההורה — שכבת ה-wire נשארת נייטרלית בשפה וב-intent. ערכים מחוץ ל-1..=4 חייבים להידחות בעת הפענוח.
קישור להורה. qub פסק דין אינו נושא את ההפניה להורה בגוף שלו. מזהה הטרנזקציה של Arweave של ה-qub ההורה נפלט בזמן ההעלאה כתג אחסון Parent-Tx-Id (§7, שכבת תגי האחסון). כך הגוף נשאר הצהרת הערכה-עצמית חתומה ועצמאית; שרשרת הביקורת ("צדק במה?") נקבעת דרך חיפוש תג Arweave.
בטיחות כתובת הראייה (נורמטיבי). כאשר evidence_url קיים, מאמתים (בצד ההרכבה, בצד ה-wire, בקצה ה-Worker) חייבים לאכוף את הבאים:
- HTTPS בלבד. המחרוזת חייבת להתחיל ברצף הבתים
https://. כל סכמה אחרת —http,ftp,javascript,data,fileוכו' — נדחית. - תקרת אורך. ≤ 2,048 בתים (גבול URL מעשי בדפדפנים).
- בדיקת NFC + קודפוינטים עוינים. אותו כלל כמו ל-
titleול-reflection— קודפוינטים של bidi-override / zero-width / tag-block / BOM / C0 / C1 נדחים. ההגדרה תואמת לפונקציה הראסטיתcrate::handle::contains_hostile_text_codepointולפונקציית ה-TS שב-workers/api/src/utils/unicode.ts::isHostileCodepoint(יש לשמור על סנכרון מלא). - ללא רווחים, ללא תווי בקרה של ASCII. רווחים / DEL / בתים מתחת ל-
0x20בכל מקום בכתובת נדחים — סוגר את וקטור ההזרקה של\n/\tשכלל ה-bidi אינו מכסה. - קטע מארח לא-ריק. כל מה שבין
https://לבין ה-/,?או#הראשון חייב להיות לא-ריק.
ללא משיכת תוכן בצד השרת. ה-Worker אסור לו לעבור פרוקסי, למשוך או להציג תצוגה מקדימה של הכתובת. הפרוטוקול מאחסן מחרוזת; הרינדור מתבצע בצד הצופה עם rel="nofollow noopener noreferrer" target="_blank" ועם מארח גלוי המוצג לצד טקסט הקישור.
רפלקציה. טקסט רפלקציה אופציונלי שכותב היוצר ("מה השתנה, מה למדת"). אותו אימות NFC ובדיקת קודפוינטים עוינים כמו ל-title. קלט ריק / שמכיל רק רווחים מתכווץ ל"נעדר" בזמן הבנייה.
גרסת סכמה. v1 תומך ב-verdict_version = 0x01 בלבד. גרסאות סכמה עתידיות מעלות את הבית הזה ונוחתות יחד עם גרסת פרוטוקול חדשה לפי §12.
7. פרוטוקול האטימה
רצף האטימה המלא. כל שלב הוא נורמטיבי.
1. User composes plaintext and metadata in ComposeQub.
2. Validate:
a. body is non-empty.
b. body size ≤ max for content_type and user tier (see §6).
c. unlock_at is in the future.
d. unlock_at ≤ created_at + 10 years.
e. content_type is a known, supported value.
3. Compute body_hash = SHA3-256(body).
4. Set created_at = current Unix seconds UTC.
5. Select drand chain. Load chain_genesis_time and chain_period_seconds, and
compute drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds).
(Computed here, before qub_id, because drand_round is bound into the qub_id
preimage — §4.1, V1.2.)
6. Compute qub_id (see §4.1), folding in drand_round from step 5.
7. Construct QubEnvelope with all fields.
8. Serialise QubEnvelope using canonical CBOR → bytes B.
Assert: serialised output matches canonical profile (§3).
9. Compute C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Construct SealedQub with tlock_ciphertext = C, and matching qub_id, version,
unlock_at, drand_chain_id, drand_round.
12. Serialise SealedQub using canonical CBOR → SealedQubCbor.
12a. Generate K = 32 random bytes (CSPRNG) and N = 12 random bytes (CSPRNG).
Compute W = wrap_sealed_qub(SealedQubCbor, qub_id=qub_id, key=K, nonce=N)
per §13. The bytes uploaded to permanent storage are the OuterWrapper CBOR W,
never the bare SealedQubCbor. K leaves the device only as the URL
fragment in step 16.
13. Display seal-time disclosure. User confirms.
14. Validate upload eligibility via the qub upload service (bot-detection, entitlement, rate limits).
15. Submit W (the OuterWrapper bytes) to the qub upload service; the service
signs and uploads to permanent storage. The service is byte-blind to the inner
SealedQubCbor and never receives K.
16. Receive arweave_tx_id from the service. Construct delivery URL as
`<origin>/c/<arweave_tx_id>#<base64url(K)>` (or `<origin>/s/<short_code>#<base64url(K)>`
when a short code is allocated). Browsers do not transmit URL fragments
to servers, so K is never observed by qub.social or any storage gateway.
שכבת תגיות אחסון (מחוץ-לפס). שירות העלאת ה-qub מצרף קבוצה קטנה במכוון של תגיות עסקת אחסון לצד המטען העטוף. Content-Type=application/octet-stream נדרש נורמטיבית. שירות-ההפניה מצרף בנוסף שלוש תגיות אופציונליות כאשר היוצר בוחר לחשוף אותן: Intent (כוונת חיבור מאומתת-מול-רשימת-היתר — למשל, quote, reply, commitment), Author (טביעת-אצבע מפתח ה-§9.3 של היוצר כ-64 תווי hex קטנים) ו-Parent-Tx-Id (מזהה עסקת האחסון של ה-qub האב עבור שרשראות תגובות, 43 תווי base64url).
תגית ה-Author היא בהצטרפות-מרצון לכל qub: אפליקציית היוצר של ההפניה מצרפת אותה רק כאשר המשתמש מאפשר במפורש ייחוס ציבורי בזמן האטימה. כאשר המתג כבוי — ברירת המחדל — לא נכתבת תגית Author וה-qub אינו מיוחס בשרשרת: שום דבר באחסון הקבוע אינו קושר את ההעלאה לכינוי, דוא"ל או qubs אחרים של היוצר. כאשר המתג דלוק, טביעת האצבע של Author מתפענחת ל-@handle שבחר היוצר דרך שרשרת האישורים של §9.5. יחסי שרשרת-תגובות ו-Intent אינם מזהים. העטיפה החיצונית (§13) מגינה על ה-גוף הפנימי מפני קורלציית טקסט-מוצפן — מונעת מקוצֵר לזהות ולפענח-בכמות העלאות בצורת-qub לאחר שסבב ה-drand שלהן מתפרסם.
שירות-ההפניה במכוון אינו מצרף תגיות App-Name, App-Version או Type: כל מסנן בעל-ערך-יחיד כזה יחזיר את כל קורפוס ה-qub לשאילתת GraphQL, מה שאינו עקבי עם היקף החסיון בגוף-בלבד של העטיפה.
מאמת תואם חייב שלא להיות תלוי בשום תגית אחסון לאימות צד-שלישי של §11; גיבוב הגוף / qub_id / החתימה מתחייבים רק ל-CBOR הפנימי, לעולם לא לקבוצת התגיות.
8. פרוטוקול השחרור
רצף השחרור המלא. כל שלב הוא נורמטיבי.
1. Viewer opens delivery URL. Extract arweave_tx_id from path AND
K = base64url_decode(fragment) from the URL fragment. If the fragment
is absent or malformed → display "this URL is missing its decryption
key" and stop; the viewer MUST NOT contact the storage gateway
without K, since fetching wrapped bytes the viewer cannot decrypt
serves no purpose and only leaks the access attempt.
2. Check denylist. If tx_id is denylisted → display block message. Stop.
3. Fetch OuterWrapper bytes from permanent storage (with multi-gateway fallback).
3a. Unwrap: parse the bytes as OuterWrapper (§13), verify the wrapper
`version` byte is `0x01`, and compute SealedQubCbor =
unwrap_sealed_qub(OuterWrapper, key=K). Any AEAD authentication
failure (wrong K, tampered ciphertext, swapped qub_id-as-AAD,
swapped nonce) → display "this URL's decryption key does not match
the stored qub" and stop. Authentication failures are
indistinguishable to the viewer per §13.5.
4. Parse SealedQubCbor → SealedQub.
5. Validate: SealedQub.version is known (0x01). Reject unknown versions.
6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
6a. Round-binding check (V1.2). Recompute expected_round =
ceil((SealedQub.unlock_at - chain_genesis_time) / chain_period_seconds).
Reject unless SealedQub.drand_round == expected_round AND the round baked
into the tlock ciphertext stanza (read via the age/tlock header, no signature
required) == expected_round. The stanza round is the one that actually gates
decryption; without this check a malicious creator could bind the ciphertext
to an already-past round while displaying a future countdown, so anyone
reading the stored bytes could decrypt before unlock_at. Implementations with
no chain identity (test mocks) skip this check.
7. Once current time ≥ SealedQub.unlock_at:
a. Fetch drand round signature for SealedQub.drand_round from drand network.
b. Compute B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
8. Parse B → QubEnvelope.
9. Validate QubEnvelope.version is known.
10. Verify: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
Fail → integrity error.
11. Verify: QubEnvelope.qub_id == SealedQub.qub_id.
Fail → integrity error.
12. Verify: QubEnvelope.unlock_at == SealedQub.unlock_at.
Fail → integrity error.
13. Verify: QubEnvelope.content_type is known and renderable.
Known values: 0x01 (text), 0x03 (pact). Unknown → display error.
14. If QubEnvelope.sig_alg != 0x00 → verify author signature (see §9.4).
15. If cosigner_pubkey or cosigner_signature present → verify cosigner (see §9.7).
16. Render content using appropriate renderer (see §10 for text, §6 for pact).
17. Construct RevealedQub for display.
9. חתימת מחברוּת
9.1 רציונל
qubs מאוחסנים באחסון קבוע. חתימות מחברוּת חייבות להישאר בלתי-ניתנות-לזיוף ללא הגבלת זמן, ולכן v1.0 משתמש בסכמת ה-ML-DSA-65 הפוסט-קוונטית (FIPS 204) במקום סכמה קלאסית שאבטחתה עשויה להתדרדר במהלך משך החיים הקבוע של ה-qub.
9.2 מרשם אלגוריתמים
sig_alg |
סכמה | גודל מפתח | גודל חתימה |
|---|---|---|---|
0x00 |
ללא חתימה (לא חתום) | — | — |
0x01 |
ML-DSA-65 (FIPS 204) | 1,952 בתים | 3,309 בתים |
צופים חייבים לדחות ערכי sig_alg לא ידועים.
9.3 בניית טרום-תמונה חתומה
sig_input = SHA3-256(
"QUB_AUTHOR_SIG_V1" || // domain separator (17 bytes)
version || // u8 (1 byte)
qub_id || // [u8; 32] (32 bytes)
body_hash || // [u8; 32] (32 bytes)
unlock_at || // i64 big-endian (8 bytes)
0x00 // u8 (1 byte): MUST be 0x00 in v1.0
)
// Total preimage: 91 bytes → 32-byte hash
signature = Sign(author_secret_key, sig_input)
מפריד תחום: "QUB_AUTHOR_SIG_V1" הוא 17 בתי ASCII: [0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31]. ללא ריפוד.
בית סוגר: הבית ה-91 של הטרום-תמונה חייב להיות 0x00. מימוש-ההפניה חושף זאת כקבוע ORG_ID_PRESENT_INDIVIDUAL = 0x00 ב-crates/qub-core/src/signing.rs; צופים המשחזרים sig_input לאימות חייבים לפלוט את אותו בית.
היקף חתימה — מה מכוסה ומה לא. sig_input מתחייב לארבעה שדות מעטפה: version, qub_id, body_hash, unlock_at (בנוסף למפריד התחום הקבוע ובית ה-org_id_present). שלושה מתוך ארבעה אלה הם אינווריאנטים מבניים: qub_id עצמו נגזר מ-version, content_type, created_at, unlock_at, outcome_at, drand_round ו-body_hash דרך הטרום-תמונה של §4.1, כך שכל שינוי בשדות אלה מפיק qub_id שונה ומפסל את החתימה באופן טרנזיטיבי. המשטח המאומת-ישירות הוא לפיכך:
| שדה | מאומת על ידי החתימה | כיצד |
|---|---|---|
version |
✓ | קלט ישיר ל-sig_input |
qub_id |
✓ | קלט ישיר |
body_hash |
✓ | קלט ישיר |
unlock_at |
✓ | קלט ישיר |
content_type |
✓ | באופן טרנזיטיבי, דרך טרום-תמונת qub_id |
created_at |
✓ | באופן טרנזיטיבי, דרך טרום-תמונת qub_id |
outcome_at |
✓ | באופן טרנזיטיבי, דרך טרום-תמונת qub_id |
drand_round |
✓ | באופן טרנזיטיבי, דרך טרום-תמונת qub_id (V1.2) |
body |
✓ | באופן טרנזיטיבי, דרך body_hash = SHA3-256(body) |
author_pubkey |
— (משתמע) | המפתח שאימת את החתימה הוא המחבר, בהגדרה |
sender_label |
✗ | טקסט-תצוגה-בלבד; ניתן לשינוי ללא שבירת חתימה |
reply_to |
✗ | מצביע שִרשור; ניתן לשינוי ללא שבירת חתימה |
cosigner_pubkey / cosigner_signature |
— | חתומים באופן עצמאי על אותו sig_input (ראו §9.7) |
drand_chain_id, tlock_ciphertext, visibility |
— | שדות SealedQub חיצוניים, לא בתוך המעטפה — מכוסים על ידי האינווריאנטים המבניים שלהם (עקביות סבב / שרשרת) אך לא על ידי חתימת המחבר. (drand_round נקשר כעת באופן טרנזיטיבי דרך הטרום-תמונה של qub_id — ראו לעיל.) |
השלכות אבטחה של שדות לא-מאומתים.
- גורם עם גישת כתיבה לבתים המאוחסנים יכול להחליף את
sender_label("Alice" → "Mallory") מבלי לפסול את חתימת המחבר. ה-author_pubkeyבתוך המעטפה נשאר עוגן הזהות האמיתי — צופים חייבים לגזור את זהות התצוגה מ-author_pubkey(דרך שכבת האישורים של §9.5) ולא לבטוח ב-sender_label. - שדה
reply_toניתן בדומה לעריכה לאחר החתימה. מכיוון ש-qub_idהוא ממוען-תוכן, תוקף אינו יכול להפנותreply_toליעד לא-קיים, אך הוא יכול בשקט לְשנות-הורה של תגובה ל-qub קיים אחר.
מימושים המציגים sender_label או reply_to למשתמשי קצה חייבים להציג את הזהות המאומתת (טביעת-אצבע מפתח, אישור) כאות הזהות הראשי, לא את התווית.
9.4 נוהל אימות
1. Read sig_alg from QubEnvelope.
2. If sig_alg == 0x00 → unsigned. No verification. Display "unsigned qub."
3. If sig_alg is unknown → reject. Display "unrecognised signature scheme."
4. Extract author_signature and author_pubkey. If either is absent → integrity error.
5. Reconstruct sig_input using fields from QubEnvelope (same formula as §9.3).
6. Verify(author_pubkey, sig_input, author_signature).
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."
אימות חתימה הוא הפעולה היקרה ביותר (במיוחד ML-DSA-65). מומלץ שיבוצע לאחר שכל הבדיקות הזולות יותר (גיבוב, qub_id, unlock_at) עברו.
9.5 אישורי זהות
אישורי זהות — מיפוי של author_pubkey לתביעות זהות הניתנות לזיהוי אנושי כגון כינוי qub, כתובת דוא"ל, כינוי רשת חברתית או אישור passkey — הם שיפור מתקדם בצד-הצופה ואינם נדרשים לאימות חתימה. צופים המפענחים אישורים לזהות תצוגה חייבים להחיל את הקדימוּת:
handle > email > social > fingerprint
חלופת טביעת-האצבע היא ה-hex הקטן של SHA3-256(author_pubkey); היא תמיד זמינה עבור כל qub חתום. צופים רשאים לקצר אותה לתצוגה — צופה-ההפניה מרנדר qub: ואחריו ארבעת הבתים הראשונים והאחרונים (qub:<8 hex>…<8 hex>).
מאמת תואם יכול להשלים כל בדיקה ב-§9.4 ללא יצירת קשר עם ה-API של qub, ללא רשת מעבר לאחסון קבוע ו-drand, וללא שום חיפוש בצד-השרת. פענוח אישורים הוא שלב נפרד בעל-מאמץ-מיטבי המבוצע רק לאחר שאימות החתימה הצליח.
9.6 השפעת גודל
| Ed25519 | ML-DSA-65 | |
|---|---|---|
| חתימה | 64 בתים | 3,309 בתים |
| מפתח ציבורי | 32 בתים | 1,952 בתים |
| סך הכל לכל qub | 96 בתים | 5,261 בתים |
| הפרש עלות אחסון (ב-~$5/MB) | ~$0.0005 | ~$0.026 |
עבור qub טקסט של 500–2,000 בתים, ML-DSA-65 משלש בערך את הגודל המאוחסן. העלות המוחלטת זניחה.
9.7 אימות מאשר-משותף (הסכמים דו-צדדיים של pact)
עבור הסכמים דו-צדדיים (content_type = 0x03), שכבת חתימה שנייה מוכיחה ששני הצדדים הסכימו לאותם תנאים.
שדות מעטפה:
cosigner_pubkey: מפתח ציבורי ML-DSA-65 של החותם-שכנגד (צד B).cosigner_signature: חתימה על אותוsig_inputכמו המחבר (§9.3).
שני השדות חייבים להיות נוכחים יחד או שניהם נעדרים. אם בדיוק אחד נוכח, צופים חייבים לדווח על שגיאת שלמות.
נוהל אימות:
1. If cosigner_pubkey absent and cosigner_signature absent → no cosigner. Done.
2. If exactly one is present → integrity error.
3. Verify cosigner_pubkey != author_pubkey (prevent self-cosigning).
Fail → display "cosigner pubkey must differ from author."
4. Reconstruct sig_input using the same formula as §9.3.
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Success → display "co-signed by [cosigner fingerprint]."
7. Failure → display "co-signature verification failed."
תכונות:
- המאשר-המשותף חותם על
sig_inputזהה לזה של המחבר — שני הצדדים מתחייבים לאותםqub_id,body_hashו-unlock_at. - גזירת
qub_id(§4.1) אינה כוללת שדות מאשר-משותף. הוספת מאשר-משותף למעטפה קיימת אינה משנה את ה-qub_id. - pact יכול להיות חתום-מחבר בלבד (התחייבות חד-צדדית), מאשר-משותף-בלבד (לא שכיח), או שניהם (הוכחה דו-צדדית מלאה).
שער קישור-דוא"ל (תפעולי). כאשר pact בהכנה נושא איש קשר דוא"ל של צד B (§6.1), שירות העלאת ה-qub חייב לסרב לבקשת החתימה-המשותפת אלא אם קיים סמן אימות-דוא"ל קצר-מועד התואם גם למזהה ההכנה וגם לגיבוב הדוא"ל-המנורמל של אותו איש קשר. הסמן נכתב על ידי /api/v1/auth/verify כאשר אסימון הקישור-הקסם נושא staging_id והכתובת המאומתת תואמת ל-SHA-256(normalise_email(party_b.contact)) — כאשר normalise_email(addr) משמר את אותיות החלק-המקומי וממיר לאותיות קטנות רק את חלק התחום (לפי RFC 5321 §2.3.11), ו-SHA-256 כאן הוא גיבוב NIST FIPS 180-4 (שונה מ-SHA3-256 שבשימוש בגזירות §4) — ופג 900 שניות (15 דקות) לאחר ההנפקה. זהו שער אנטי-התחזות תפעולי, לא חלק מהוכחת ה-qub שעל-השרשרת — מאמת צד-שלישי המשחזר את §11 זקוק רק לאחסון קבוע ול-drand, ללא שום חיפוש בצד-השרת. הסמן קיים בצד-השרת בלבד ולעולם אינו חלק מהגוף החתום.
השפעת גודל (מחבר ML-DSA-65 + מאשר-משותף):
| רכיב | גודל |
|---|---|
| חתימת מחבר | 3,309 בתים |
| מפתח ציבורי של מחבר | 1,952 בתים |
| חתימת מאשר-משותף | 3,309 בתים |
| מפתח ציבורי של מאשר-משותף | 1,952 בתים |
| סך תקורת קריפטו | 10,522 בתים |
| הפרש עלות אחסון | ~$0.05 |
10. רינדור וחיטוי Markdown
סעיף זה הוא קריטי-לאבטחה. הצופה מרנדר qubs טקסט (content_type = 0x01) באמצעות תת-קבוצה מוגבלת של Markdown.
10.1 אלמנטים מותרים
- כותרות:
#עד####(ללא#####או######) - הדגשה: מודגש (
**), נטוי (*), קו-חוצה (~~) - רשימות: ממוספרות (
1.) ולא-ממוספרות (-,*) - ציטוטי בלוק (
>) - קוד: מקטעים בשורה (```) ובלוקים מגודרים (`````)
- קווים אופקיים (
---) - מעברי שורה (שני רווחים סוגרים או שורה ריקה)
- פסקאות
10.2 אלמנטים אסורים
| אלמנט | טיפול |
|---|---|
HTML גולמי (<div>, <script> וכו') |
מוסר לחלוטין. שום HTML אינו עובר. |
תמונות () |
מוסר. תחביר תמונה מוסר מהפלט. |
קישורים ([text](url)) |
הכתובת מרונדרת כטקסט פשוט גלוי. אינה מקושרת אוטומטית. אינה ניתנת ללחיצה ללא פעולת משתמש מפורשת. |
| סכמות URL מסוכנות | javascript:, data:, vbscript:, file: — מוסרות. |
| Iframes, embeds, objects | מוסרים. |
| ישויות HTML | מפוענחות לתווי תצוגה רק אם בטוחות. |
10.3 מימוש
מימושים חייבים להשתמש במנתח רשימת-היתר קפדני, לא ברשימת-חסימה. הגישה המומלצת:
- נתחו את ה-Markdown באמצעות
pulldown-cmark(או שווה-ערך). - הלכו על ה-AST והשמיטו כל צומת שאינו ברשימת-ההיתר (§10.1).
- עבור צמתי קישור: פלטו את הכתובת כטקסט גלוי, לא כאלמנט
<a>הניתן ללחיצה. - המירו את ה-AST המסונן לייצוג ביניים מטופס (למשל, enum מסוג
MarkdownNodeעם וריאנטים בטוחים בלבד). HTML גולמי בלתי-ניתן-לייצוג מבנית בייצוג ביניים זה. - רנדרו מייצוג הביניים המטופס לשכבת התצוגה היעד (למשל, רכיבי תצוגה ריאקטיביים, צמתי DOM). אין שרשור מחרוזות HTML או
innerHTMLבשום שלב.
גישות רשימת-חסימה שבריריות מכיוון שהרחבות Markdown חדשות או מוזרויות מנתח יכולות להכניס אלמנטים לא-מסוננים. גישת ה-AST המטופס הופכת XSS לבלתי-אפשרי מבנית — אין וריאנט שיכול לשאת HTML שרירותי.
10.4 מגבלות גודל ומבנה
- עומק כותרת מרונדר מרבי:
####(H4).#####ועמוק יותר מרונדרים כטקסט מודגש. - אין מגבלה על מספר פסקאות (מגבלות גודל הגוף ב-§6 הן האילוץ).
- בלוקי קוד מגודרים: אין הדגשת תחביר ב-MVP. מרונדרים כטקסט מעוצב-מראש ברוחב-קבוע.
11. אימות צד-שלישי
כל צד שלישי יכול לאמת qub ציבורי ללא שיתוף פעולה של qub. נוהל האימות:
1. Obtain arweave_tx_id (from delivery URL or direct knowledge).
2. Fetch SealedQubCbor from any storage gateway.
3. Confirm storage block inclusion (block height, block timestamp).
4. Parse SealedQubCbor → SealedQub.
5. Fetch drand round signature for SealedQub.drand_round.
6. tlock_decrypt(tlock_ciphertext, round_signature) → QubEnvelope CBOR bytes.
7. Parse → QubEnvelope.
8. Verify SHA3-256(body) == body_hash.
9. Verify QubEnvelope.qub_id == SealedQub.qub_id.
10. Verify QubEnvelope.unlock_at == SealedQub.unlock_at.
11. If sig_alg != 0x00: verify author_signature (see §9.4).
12. All checks pass → qub is verified.
מה האימות מוכיח:
| הוכחה | מה היא מבססת |
|---|---|
| התחייבות | הטקסט המוצפן היה קיים עד חותמת הזמן של בלוק האחסון. |
| שלמות | גוף הטקסט הגלוי תואם את הגיבוב שהתחייבו אליו ולא שונה. |
| תזמון | התוכן היה בלתי-קריא עד סבב ה-drand, התואם את זמן השחרור שנבחר (בכפוף להנחות האבטחה של tlock ו-drand). |
מה האימות אינו מוכיח:
| אי-הוכחה | מדוע |
|---|---|
| מחברוּת | ה-sender_label הוא דקורטיבי. ללא sig_alg ≥ 0x01, כל אחד היה יכול לאטום תוכן זה. |
| כוונה | ה-qub מוכיח תוכן ותזמון, לא מה שהיוצר התכוון סובייקטיבית. |
| תזמון טרום-אירוע | הכללת בלוק האחסון עשויה לפגר אחר ההעלאה בפועל בדקות. חותמת זמן ההתחייבות היא זמן הבלוק, לא הרגע שבו המשתמש לחץ "אטום". |
12. ניהול גרסאות
12.1 גרסת פרוטוקול
שדה ה-version (u8) גם ב-SealedQub וגם ב-QubEnvelope מזהה את גרסת הפרוטוקול הראשית.
- צופים חייבים לדחות גרסאות ראשיות לא ידועות עם שגיאה ברורה.
- גרסאות ראשיות ידועות רשאיות לסבול שדות אופציונליים לא ידועים אם כללי תאימות-קדימה מאפשרים (שדות אופציונליים הנעדרים מסדר המפתחות הקנוני מתעלמים מהם).
- טיפוסי תוכן (
content_type) וסכמות חתימה (sig_alg) מוגבלים-בגרסה: ערכים חדשים רשאים להיות מוכנסים רק לצד גרסת פרוטוקול חדשה או עדכון מרשם מפורש.
12.2 היסטוריית גרסאות
| גרסה | ערך | תיאור |
|---|---|---|
| v1 | 0x01 |
qubs טקסט ציבוריים (content_type 0x01), הסכמים דו-צדדיים של pact (0x03, סכמת structured/v1, מחבר ML-DSA-65 + מאשר-משותף), tlock, SHA3-256 |
12.3 תאימות-קדימה
צופה v1 הנתקל ב-QubEnvelope עם מפתחות מפת CBOR אופציונליים לא ידועים (מפתחות שאינם בסדר הקנוני של §3.2) מומלץ שיתעלם ממפתחות אלה וימשיך באימות תוך שימוש בשדות ידועים. זה מאפשר תוספות מינוריות עתידיות (למשל, מטא-נתונים חדשים) ללא דרישת קפיצת גרסה ראשית.
צופה v1 הנתקל ב-sig_alg = 0x01 (ML-DSA-65) אך חסר תמיכת אימות ML-DSA-65 מומלץ שיציג את תוכן ה-qub עם הודעת "חתימה נוכחת אך אינה ניתנת לאימות", לא ידחה את ה-qub לחלוטין. מימוש-ההפניה כיום דוחה כל ערך sig_alg למעט 0x00 ו-0x01 מכיוון שמרשם v1 אינו מכיל אלגוריתם תקף אחר — דחייה קפדנית וכשל-רך זהים בתצפית עד שאלגוריתם שלישי נרשם. התנהגות הכשל-הרך לעיל הופכת לנושאת-משקל ברגע ש-§9.2 מקבל ערך חדש, וצופה-ההפניה יעודכן לכשל-רך באותה נקודה.
12.4 גרסת עטיפה חיצונית
ה-OuterWrapper המתואר ב-§13 נושא בית version משלו, בלתי-תלוי ב-SealedQub.version וב-QubEnvelope.version. שני מרחבי הגרסאות מתפתחים בנפרד: החלפה סימטרית בטוחת-פוסט-קוונטים עתידית מקדמת את בית העטיפה מבלי לגעת בגרסת הפרוטוקול הפנימית, ותוספת ברמת-הפרוטוקול עתידית (למשל, שדה מעטפה חדש) מקדמת את הגרסה הפנימית מבלי לגעת בבית העטיפה.
OUTER_WRAPPER_VERSION_* |
ערך | אלגוריתם | סטטוס |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
AES-256-GCM עם nonce של 12 בתים, תג אימות של 16 בתים, AAD קשור ל-qub_id |
ברירת מחדל v1 |
| — | 0x02–0xFF |
שמור | עתידי |
צופים חייבים לדחות גרסאות עטיפה לא ידועות עם שגיאה ברורה. הפרוטוקול שומר במכוון את מרחב גרסאות העטיפה צר עד שמופיע מניע מיגרציה קונקרטי (למשל, הנחיית NIST המעדיפה AEAD שונה); משבצת 0x02 תוקצה באותה רוויזיה שמכניסה את האלגוריתם.
13. עטיפת הצפנה חיצונית
13.1 רציונל
שכבות הפרוטוקול (QubEnvelope → tlock → SealedQub) הופכות qub אטום לנעול-זמן: הגוף בלתי-קריא עד unlock_at וחתימת סבב ה-drand פורסמה. לאחר השחרור, עם זאת, חתימת הסבב ציבורית וצורת ה-CBOR הקנונית של SealedQub ניתנת לזיהוי, כך שקוצֵר שאינדקס עסקאות אחסון-קבוע יכול לפענח-בכמות את כל קורפוס ה-qub.
עטיפת ההצפנה החיצונית סוגרת את הערוץ הזה על ידי הוספת שכבת AEAD סימטרית נוספת בין ה-SealedQubCbor הקנוני לבתים הנכתבים לאחסון קבוע. המפתח בן 256 הסיביות K חי רק במקטע ה-URL של כתובת המסירה ובמכשירי המשתמשים; דפדפנים אינם משדרים מקטעי URL לשרתים, כך ש-qub.social, כל שער אחסון וכל CDN מול אחד מהם הם עיוורים-בתצפית ל-K. כל qub באחסון קבוע הוא לפיכך טקסט מוצפן אטום שהטקסט הגלוי שלו בלתי-ניתן-לשחזור ללא ה-URL שהיוצר בחר לשתף.
השפעה נטו:
- חסינות מנייה כברירת מחדל. בתים עטופים באחסון קבוע בלתי-ניתנים-להבחנה-בתים מטקסט מוצפן שרירותי. אסטרטגיית קוצֵר של "שאילתת-GraphQL להעלאות בצורת-qub, פענוח-בכמות עם חתימות drand ציבוריות" אינה מסתיימת בטקסט גלוי.
- עמדת פרטיות של גריסה-קריפטוגרפית. qub.social פשוט אינו יכול לפענח את הקורפוס שלו עצמו. צווי בית-משפט מגיעים לטקסט מוצפן, לא לטקסט גלוי.
- סולם חסיון דו-שכבתי. ברירת מחדל = גישה מבוקרת-קישור (סעיף זה). qubs פרטיים מוצפני-נמען (תכונה שמורה לשלב 2, טרם הוגדרה) מתלבשים מעל כשכבה השנייה.
13.2 שכבוּת
plaintext body ← QubEnvelope.body (§2.2)
↓ canonical CBOR (§3)
envelope CBOR
↓ tlock encrypt to drand round (§7 step 10)
tlock_ciphertext (inside SealedQub) (§2.3)
↓ canonical CBOR (§3)
SealedQubCbor bytes ← inner wire artifact
↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12a, this section)
OuterWrapper CBOR bytes ← uploaded to permanent storage (§7 step 15)
האטימה והשחרור ברמת הפרוטוקול (§7, §8) אינם משתנים מתחת לגבול העטיפה; העטיפה מתחברת באתר הקריאה של seal() ומתנתקת באתר הקריאה של unlock().
13.3 מבנה נתונים של OuterWrapper
struct OuterWrapper {
version: u8, // 0x01, see §12.4
qub_id: [u8; 32], // copied from inner SealedQub; AEAD AAD
nonce: [u8; 12], // 96-bit AEAD nonce
ciphertext: Vec<u8>, // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || 16-byte tag
}
אינווריאנטים של שדות.
versionחייב להיות שווה ל-0x01עבור בתי עטיפה v1.0.qub_idחייב להיות שווה לשדה ה-qub_idשל ה-SealedQub המשוחזר לאחר הפתיחה. שלב הפתיחה אינו אוכף זאת ישירות (קשירת ה-AEAD AAD הופכת שיבוש ברמת-הבתים לבלתי-אפשרי), אך שכבת השחרור בודקת את היחס באופן טרנזיטיבי: אם יוצר עוטףSealedQubCborשה-qub_idהפנימי שלו אינו תואם את ה-qub_idשל העטיפה, §8 שלב 11 נכשל.nonceחייב להיות 96 סיביות (12 בתים), שנוצר טרי על ידי CSPRNG עבור כל פעולת עטיפה. שימוש חוזר ב-nonce תחת אותו מפתח מאפשר התקפות שימוש-חוזר-ב-nonce של AEAD המשחזרות את הטקסט הגלוי; יצרנים חייבים להתייחס לזוגות (key,nonce) כחד-פעמיים.ciphertextהוא פלט ה-AES-256-GCM: בתי טקסט-מוצפן משורשרים עם תג האימות בן 16 הבתים.ciphertext.len() == SealedQubCbor.len() + 16בדיוק.
קידוד CBOR. CBOR קנוני לפי §3, עם אותו כלל סדר-מפתחות (ממוין לפי אורך בתים מקודד עולה, ואז לקסיקוגרפית). ארבעת המפתחות הם:
| מפתח | בתים מקודדים | סדר |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
הבית הראשון של ה-OuterWrapper CBOR הוא לפיכך כותרת המפה באורך-מוגדר עבור מפה בת 4 רשומות (0xA4).
13.4 קשירת AAD ל-qub_id
העטיפה קושרת את qub_id כנתון מאומת נוסף של AEAD. זוהי ההגנה המבנית הנושאת-משקל נגד שלוש מחלקות של התקפה:
| התקפה | הגנה |
|---|---|
העברת טקסט מוצפן תחת שדה qub_id שונה בעטיפה |
אי-התאמת AAD → אימות AEAD נכשל |
| ערבוב מקטע ה-URL של qub A עם בתי האחסון-הקבוע של qub B | אי-התאמת AAD → אימות AEAD נכשל |
שיבוש שדה ה-qub_id של העטיפה לאחר ההעלאה |
אי-התאמת AAD → אימות AEAD נכשל |
נשיאת qub_id בטקסט הגלוי של העטיפה אינה מחלישה משמעותית את חסינות המנייה — qub_id עצמו הוא גיבוב SHA3-256 של הטרום-תמונה של §4.1 ללא טרום-תמונה ניתנת-לשחזור מהתקציר, ומנייה שכבר קצרה את בתי העטיפה אינה לומדת דבר מה-qub_id הגלוי שלא יכלה להסיק מעצם קיומה של ההעלאה.
13.5 אלגוריתמי עטיפה ופתיחה
wrap_sealed_qub(SealedQubCbor S, qub_id Q, key K, nonce N):
require K.len() == 32 and N.len() == 12 and Q.len() == 32
C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
// C includes the 16-byte authentication tag at the end
return canonical_cbor_encode(OuterWrapper{
version: 0x01,
qub_id: Q,
nonce: N,
ciphertext: C,
})
unwrap_sealed_qub(OuterWrapper bytes W, key K):
require K.len() == 32
O := canonical_cbor_decode(W) as OuterWrapper
require O.version == 0x01 // §12.4
P := AES_256_GCM_decrypt(
key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
)
// any AEAD failure → DECRYPT_FAILED, indistinguishable to caller
return P // P is the inner SealedQubCbor
קריסת מצב-כשל. K שגוי, nonce שגוי, אי-התאמת AAD וטקסט מוצפן משובש כולם מפיקים את אותה שגיאת DECRYPT_FAILED. זוהי תכונת AEAD מכוונת: הבחנה במצב הכשל הייתה יוצרת ערוץ-צד שתוקף מרוחק יכול לבחון בשליחת עטיפות פגומות ובמדידת זמן התגובה. מימושי-הפניה חייבים לקרוס את כל כשלי ה-AEAD לצורת שגיאה יחידה.
13.6 חומר מפתח והפצה
מפתח העטיפה K הוא ערך אקראי אחיד בן 256 סיביות שנוצר לכל-qub על ידי CSPRNG. מימושי-ההפניה שואבים אותו מ:
- יוצר WASM:
getrandom(WebCrypto תחת ה-backend שלwasm_js). - מסלול אטימה בצד-השרת של Worker:
crypto.getRandomValues.
הפצה: K חייב להיות מקודד כ-base64 בטוח-ל-URL (RFC 4648 §5, ללא ריפוד) ומצורף לכתובת המסירה כרכיב המקטע:
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
המקטע לעולם אינו משודר לשום שרת על ידי דפדפן תואם. ערוצי שחזור (אינדקס היסטוריה בצד-השרת, שליחה-אוטומטית-בדוא"ל בהצטרפות-מרצון) המשמרים את כתובת המסירה המלאה — כולל המקטע — מעבר למכשיר המשתמש הם פשרה מפורשת מול עמדת ברירת המחדל של גריסה-קריפטוגרפית וחייבים להיות מוגבלים בהסכמת משתמש מפורשת.
אובדן מקטע. אם משתמש מאבד את מקטע ה-URL ואין לו ערוץ שחזור, ה-qub בלתי-קריא. זוהי הפשרה הנושאת-משקל של העיצוב וחייבת להיחשף למשתמש בזמן האטימה. ה-MVP מחזק את חשיפת זמן-האטימה עם טקסט מפורש "שמרו כתובת זו" וערוץ שחזור בדוא"ל-מאומת למשתמשים המצטרפים מרצון.
13.7 מחוץ-להיקף עבור סעיף זה
- חתימת מחברוּת (§9) אינה משתנה: חתימות מחושבות בתוך ה-
QubEnvelopeהפנימי ומשוחזרות לאחר פתיחה → פענוח tlock → ניתוח CBOR. - qubs פרטיים מוצפני-נמען (תכונה שמורה לשלב 2, טרם הוגדרה) מתלבשים מעל עטיפה זו כשכבת חסיון שנייה; שתי השכבות יכולות להיות פעילות בו-זמנית.
- pacts (§6, content_type
0x03) עטופים בדיוק כמו qubs טקסט; העטיפה עיוורת-בתים לטיפוס התוכן הפנימי.
13.8 qubs ציבוריים (השמטת העטיפה)
העטיפה החיצונית היא אופציונלית בשכבת המסירה. יוצר רשאי לחתום qub כציבורי, ובמקרה זה ה-SealedQubCbor הקנוני נכתב לאחסון קבוע ישירות, ללא שכבת OuterWrapper וללא מפתח K:
SealedQubCbor bytes ──(public)──▶ uploaded to permanent storage as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded
qub ציבורי הוא נעול-זמן אך לא מבוקר-קישור: הוא נשאר בלתי-קריא עד שסבב ה-drand שלו מתפרסם (שכבת ה-tlock אינה משתנה), אך לאחר השחרור כל מי שברשותו ה-arweave_tx_id יכול לפענח אותו — אין צורך במקטע URL, מכיוון שאין K. זוהי הפשרה המכוונת עבור משטחים שהשרת חייב להניע: הודעות דוא"ל על שחרור, הטמעות צד-שלישי ו-SEO עשיר יותר לאחר השחרור — כולם זקוקים לקישור שפועל ללא סוד שהשרת לעולם אינו מחזיק בו (§13.6).
השלכות שעל יצרן חייב להביא בחשבון:
- אין חסינות מנייה. qubs ציבוריים מוותרים על תכונת חסינות-המנייה של §13.1 מעצם מבנם. שירות ההעלאה של מימוש-הפניה מטביע עליהם (ועליהם בלבד) תג אחסון-קבוע
Visibility: publicכך שהם ניתנים-לגילוי במכוון; qubs פרטיים אינם נושאים תג כזה ושומרים על אי-יכולת-ההבחנה-בבתים שלהם. - כותרת בטקסט גלוי חשופה בזמן האטימה. שדה ה-
titleשל §3.2 הוא טקסט גלוי בתוךSealedQubCbor. תחת העטיפה הוא מוסתר עד שצופה מספקK; ללא העטיפה הוא קריא-לכול באחסון הקבוע מרגע ההעלאה, לפני השחרור. ממשקי יוצר תואמים חייבים לחשוף זאת בזמן האטימה. - הזיהוי מבני. צופה/הטמעה תואמים מבחינים בין שתי הצורות לפי ניתוח: בתים המתנתחים כ-
OuterWrapperעוברים במסלול הפתיחה-עם-K; בתים המתנתחים כ-SealedQubCborחשוף מתקבלים ישירות. אין צורך בדגל קווי, ו-qub_idאינו קושר את החסיון — אותו תוכן זהה-בתים בשכבת ה-SealedQubבין אם נחתם ציבורי או פרטי.
פרטי (עטוף) נשאר ברירת המחדל; ציבורי הוא בחירת יוצר מפורשת לכל qub.
14. וקטורי בדיקה
14.1 גזירת qub_id
Input:
version = 0x01
content_type = 0x01
created_at = 1735689600 (2025-01-01 00:00:00 UTC)
unlock_at = 1736294400 (2025-01-08 00:00:00 UTC)
outcome_at = absent
drand_round = 4695445 (= (1736294400 - 1595431050) / 30, drand mainnet params §14.2)
body = "Hello, future." (UTF-8, 14 bytes)
title = absent
Intermediate:
body_hash = SHA3-256("Hello, future.")
= 76ab8b3f843c6ed4f2d0fd75b9f457b4
ad49dd4450f9c22723ae430e3af3211d
title_hash = [0u8; 32] (title absent — §4.2.1 sentinel)
Domain separator (10 bytes):
[0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]
Preimage (108 bytes — V1.2):
domain_separator || // 10 bytes
0x01 || // version
0x01 || // content_type
0x0000000067748580 || // created_at as i64 big-endian (1735689600)
0x00000000677DC000 || // unlock_at as i64 big-endian (1736294400)
0x0000000000000000 || // outcome_at_or_zero (outcome_at absent)
0x000000000047A595 || // drand_round as u64 big-endian (4695445)
body_hash || // 32 bytes
title_hash // 32 bytes (all-zeros sentinel; title absent)
Expected output:
qub_id = SHA3-256(preimage)
= 3a9fcb31b750d985c262fada6d4f777f
d6a28be831d941d85c131f5a4bbaf8a4
מימושים חייבים להפיק ערכי body_hash ו-qub_id זהים עבור קלט זה. וקטור בדיקה זה מומלץ שיהיה בדיקת היחידה הראשונה שנכתבת. הערכים הקנוניים לעיל חושבו על ידי מימוש-ההפניה וחייבים להתאים סיבית-אחר-סיבית. פריסות טרום-תמונה היסטוריות (טרום-השקה — שום qub חי לא הסתמך עליהן): ה-qub_id של V1.0 בן 92 הבתים היה 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; ה-qub_id של V1.1 בן 100 הבתים (לאחר קיפול outcome_at_or_zero) היה b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 מקפל את drand_round ומעלה את מפריד התחום ל-QUB_ID_V2.
14.2 מיפוי סבב-שחרור
Input:
unlock_at = 1735689600
chain_genesis_time = 1595431050
chain_period_seconds = 30
Calculation:
(1735689600 - 1595431050) / 30 = 4675285.0
ceil(4675285.0) = 4675285
drand_round = 4675285
14.3 הלוך-ושוב של CBOR קנוני
מימושים חייבים לאמת ש-serialize(parse(serialize(qub))) == serialize(qub) עבור כל הקלטים התקפים. זוהי בדיקת תכונה, לא וקטור יחיד.
14.4 CBOR של PactTerms (content_type 0x03)
Input:
pact_version = 1
title = "Scooter deposit"
terms = [
{ key: "Item", value: "Honda Metropolitan scooter" },
{ key: "Price", value: "$100" },
{ key: "Deposit", value: "$10" }
]
party_a = { label: "Alice" }
party_b = { label: "Bob", contact: "bob@example.com" }
notes = absent
Canonical CBOR key order (PactTerms):
"notes"(6) < "terms"(6) < "title"(6) < "party_a"(8) < "party_b"(8) < "pact_version"(13)
Canonical CBOR key order (PactTerm):
"key"(4) < "value"(6)
Canonical CBOR key order (PartyIdentifier):
"label"(6) < "contact"(8)
בתי ה-CBOR הקנוניים ו-body_hash של SHA3-256 מחושבים על ידי מימוש-ההפניה. מימושים חייבים להפיק CBOR זהה-בתים עבור קלט זה.
מימושים חייבים גם לאמת ש-serialize(parse(serialize(pact))) == serialize(pact) עבור כל קלטי PactTerms התקפים (בדיקת תכונה).
14.5 וקטורים חוצי-שפות של העטיפה החיצונית
לעטיפה החיצונית (§13) יש fixture קנוני נפרד ב-crates/qub-core/tests/vectors/wrapper_v1.json. כל מקרה מקבע צמד (key, nonce, qub_id, sealed_cbor) כקלטי hex אטומים ומאשר פלט expected_wrapper_hex ספציפי. שני מימושי-ההפניה צורכים את אותו קובץ JSON:
- Rust:
crates/qub-core/tests/wrapper_vectors.rs(cargo test -p qub-core --test wrapper_vectors). - TypeScript:
workers/api/src/crypto/__tests__/wrapper.test.ts(npm test).
ה-fixture מקבע כרגע שלושה מקרים:
| מקרה | כיסוי |
|---|---|
basic-text-public |
צורת SealedQub הריאליסטית הקטנה ביותר; ללא שדות אופציונליים. מבססת את צורת העטיפה הקנונית עבור qub טיפוסי-v1.0. |
with-recipient-pubkey |
SealedQub עם recipient_pubkey מוגדר (מסלול שלב 2). קבוצת מפתחות CBOR פנימית שונה, qub_id שונה. |
longer-body |
גוף ~4 KiB — מפעיל קידומות אורך CBOR רב-בתיות גם בתוך המעטפה הפנימית וגם בתוך הטקסט המוצפן החיצוני. |
מימושים חייבים להפיק expected_wrapper_hex זהה-בתים עבור הקלטים המוקלטים. יצירה מחדש של ה-fixture מצריכה QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors ושמורה לשינויי פורמט מכוונים.
15. ממשל פרופיל קריפטו (עתיד)
סעיף זה אינפורמטיבי עבור v1 והופך לנורמטיבי בפעם הראשונה שאלגוריתם שני נכנס לכל אחת מאבני הבניין הקריפטוגרפיות של qub.
15.1 עמדה נוכחית
פרוטוקול v1 קושר בדיוק אלגוריתם אחד לכל אבן בניין:
- חתימה: ML-DSA-65 (
sig_alg = 0x01; מפתח ציבורי בן 1952 בתים, חתימה בת 3309 בתים) ולא חתום (sig_alg = 0x00). מרשם §9.2 אינו מגדיר ערכים אחרים; מאמת v1 חייב לדחות כלsig_algמחוץ ל-{0x00, 0x01}. רשומת Ed25519 עתידית צפויה (§15.3) אך אינה מוקצית ב-v1. - נעילת-זמן: drand quicknet בלבד — גיבוב השרשרת, המפתח הציבורי, זמן הבראשית והמחזור הם פרמטרי-רשת קבועים הנישאים על ידי
DrandTimelockProvider::quicknet()של ההפניה (crates/qub-core/src/tlock.rs) ו-config/drand-endpoints.json. - עטיפה חיצונית: AES-256-GCM v1 בלבד (§13).
מאמתים כיום מקודדים-קשיח אורכי מפתח וחתימה לכל אבן בניין. שום משטח זריזות אינו נחשף על ידי הפורמט הקווי.
15.2 צורה מיועדת
כאשר אלגוריתם שני נכנס לפרוטוקול, המאמת יוגדר עבור CryptoProfile בעל-שם (למשל, ExqubV1) המפרט את הקבוצה המדויקת של ערכים מותרים לכל אבן בניין — sig_algs, שרשראות drand, גרסאות עטיפה, טיפוסי תוכן. הפרופיל מקובע בזמן האימות, לעולם אינו נמשא-ונתן בתוך-הפס. כל ערך מחוץ לפרופיל הפעיל נדחה.
זה מבטיח שהוספת ML-DSA-87 או הפעלת Ed25519 אינן יכולות להחליש למפרע תצורות מאמת קיימות: מאמת v1 נשאר מאמת v1 גם לאחר שפורסם פרופיל v2.
15.3 תנאי הפעלה
קדמו את §15 לסטטוס נורמטיבי כאשר מוצע כל אחד מהבאים:
- בית
sig_algשני (הפעלת Ed25519, ML-DSA-87, או כל רשומה חדשה במרשם §9). - שרשרת drand שנייה בשימוש ייצור.
- גרסת עטיפה-חיצונית שנייה.
עד אז §15 הוא מציין-מקום המקבע את צורת המיגרציה כך ש-PRs עתידיים ינחתו מול יעד ידוע במקום להתדיין מחדש על משטח המשא-ומתן מאפס.