qub 프로토콜 명세

qub은 암호학적 시간 약속을 위한 프로토콜입니다. 미래의 특정 날짜에 단어들을 봉인하고, 그 날짜가 도래했을 때 무엇이 정확히 언제 말해졌는지를 증명하는 시스템입니다.

세 가지 기본 요소가 이를 가능하게 합니다. drand는 탈중앙화된 무작위성 비콘입니다 — 공개 일자는 어떠한 당사자의 선의가 아니라 물리 법칙에 의해 강제됩니다. 영구 공개 저장소는 변조 방지 공개 저장소입니다 — 어떠한 당사자도 한 번 봉인된 qub을 편집하거나 삭제할 수 없습니다. ML-DSA-65는 포스트 양자 디지털 서명입니다 — 각 qub은 비밀 키가 작성자의 기기를 절대 떠나지 않는 키 쌍에 결속됩니다.

이 기본 요소들이 함께 만들어내는 진술은 시간 잠금되어 있고, 변조가 드러나며, 귀속 가능한 — 세상이 과거를 조작하는 능력이 향상될수록 그 가치가 커지는 영수증입니다.

이 문서의 나머지 부분은 상호 운용 가능한 구현을 위해 요구되는 규범적 사양입니다.


qub 프로토콜 사양

항목
버전 1.0 (프로토콜 버전 0x01, 외부 래퍼 버전 0x01)
날짜 2026-05-01
상태 초안
검토 완료 시점 2026-05-01

이 문서는 qub 시간 약속 시스템의 규범적 프로토콜 사양입니다. 상호 운용 가능한 구현에 요구되는 데이터 구조, 직렬화 규칙, 도출 공식, 검증 절차를 정의합니다.

범위: 프로토콜 계층은 의도적으로 언어 중립적입니다 — qub 본문은 불투명한 평문 / 마크다운 / 약정 바이트이며, 로케일 인식 렌더링은 열람자의 책임입니다 (qub.social 웹 앱, <qub-embed> iframe, MCP 클라이언트 등).


1. 표기법 및 규약

표기 의미
u8, u64, i64 지정된 비트 폭의 부호 없는/부호 있는 정수
[u8; N] N 바이트의 고정 길이 바이트 배열
Vec<u8> 가변 길이 바이트 배열
Option<T> T 타입의 값, 또는 부재
String UTF-8 텍스트 문자열, NFC 정규화됨
`
SHA3-256(x) 바이트 문자열 x의 NIST SHA3-256 해시 (FIPS 202)
ceil(x) 천장 함수: x 이상의 가장 작은 정수
CBOR Concise Binary Object Representation (RFC 8949)
big-endian 최상위 바이트 먼저

원본 이미지 구성에 사용되는 모든 정수는 별도로 명시되지 않는 한 big-endian 고정 폭 바이트 배열로 인코딩됩니다 (i64 → 8 바이트, u8 → 1 바이트).

모든 타임스탬프는 UTC Unix 초입니다.


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 (약정 본문, §6.1 참조); sig_alg = 0x01 (ML-DSA-65)에 author_signatureauthor_pubkey 존재 (§9.3 참조); 공동서명된 약정에 대해 cosigner_pubkeycosigner_signature 함께 존재 (§9.7 참조); 회신 체인 qub에 대해 부모 qub의 qub_id로 설정된 reply_to (서명 범위에 관한 함의는 §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 프로파일

모든 SealedQubQubEnvelope 직렬화는 이 프로파일을 준수해야 합니다. 동일한 논리적 구조가 주어진 두 구현은 동일한 바이트를 생성해야 합니다.

3.1 인코딩 규칙

규칙 사양
표준 RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements)
맵 키 정렬 인코딩된 바이트 길이 우선 정렬 (짧은 것이 긴 것보다 먼저), 그 다음 사전식 (동일 길이 인코딩에 대해 바이트별)
정수 인코딩 최단 형식: 0–23은 초기 바이트에; 24–255는 2 바이트에; 256–65535는 3 바이트에; 이하 동일
길이 인코딩 확정 길이만 허용. 불확정 길이 배열, 맵, 바이트 문자열, 텍스트 문자열 금지 (추가 정보 = 31 금지).
태그 CBOR 태그 없음 (주 타입 6 금지).
부동 소수점 부동 소수점 없음 (주 타입 7의 값 0xF9–0xFB 금지).
텍스트 문자열 UTF-8 인코딩, NFC 정규화됨 (유니코드 정규화 형식 C).
바이트 문자열 원시 바이트. CBOR 계층에서 base64 인코딩 없음.
중복 키 오류와 함께 거부. 파서는 중복 맵 키를 조용히 받아들여서는 안 됩니다.
단순 값 true (0xF5), false (0xF4), null (0xF6)만 허용됩니다.
선택적 필드 부재한 선택적 필드는 CBOR 맵에서 완전히 생략됩니다 (null로 인코딩되지 않음). 존재하는 선택적 필드는 정렬된 키 순서로 포함됩니다.

3.2 검증된 정규 키 순서

이 키 순서는 규범적입니다. 구현은 정확히 이 순서로 키를 방출해야 합니다. 디버그 어설션은 비-릴리즈 빌드에서 순서를 검증해야 합니다.

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 (약정 본문, 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 바이트 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 바이트 author_signature, cosigner_signature
ML-DSA-65 공개 키 (1,952 바이트) 0x59 0x07 0xA0 + 1,952 바이트 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 바이트입니다. 정렬을 위해 10 바이트에 도달하도록 단일 0x00 패딩 바이트가 추가됩니다. 구현은 정확히 다음 10 바이트를 사용해야 합니다: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].

outcome_at 인코딩: V1.1은 선택적 outcome_at 필드를 결속에 접어 넣기 위해 원본 이미지를 92 바이트에서 100 바이트로 확장했습니다. 부재한 outcome_at은 8개의 0 바이트로 인코딩됩니다; 프로토콜 검증기는 모든 곳에서 outcome_at <= 0을 거부하므로 이 센티넬이 정당한 값과 충돌할 수 없습니다. §3.2 (와이어 형식) 및 이 필드를 동기 부여하는 verdict 메커니즘에 대해서는 인트리 tasks/verdict-uplift-plan.md를 참조하십시오.

drand_round 인코딩: V1.2는 drand_round (대상 drand 라운드, §4.3)를 결속에 접어 넣기 위해 원본 이미지를 100 바이트에서 108 바이트로 확장했으며, 도메인 분리자를 QUB_ID_V2로 올렸습니다. 이는 타임록 라운드를 qub 정체성에 결속합니다: 게이트웨이는 표시된 unlock_at이 함의하는 것과 다른 (예: 이미 지난) 라운드로 암호문을 재결속할 수 없습니다. 또한 잠금 해제 절차 (§8)는 tlock 암호문 스탠자에 구워진 라운드가 unlock_round(unlock_at)과 일치하는지 검증하므로, 표시된 잠금 해제 시각이 복호화를 통제하는 라운드임이 증명 가능합니다.

속성:

4.2 body_hash

body_hash = SHA3-256(body)

여기서 body는 원시 Vec<u8> 콘텐츠 페이로드입니다. 텍스트 qub의 경우, 이는 UTF-8 인코딩된 qub 본문입니다.

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 정규화는 해시 시점에 실행되어 시각적으로 동등한 코드 포인트 시퀀스에 대해 다이제스트가 안정적입니다. 전체 0 센티넬은 부재 케이스를 위해 예약됩니다. 빈 문자열은 "부재"의 비정규 인코딩으로서 정규 CBOR 경계에서 거부됩니다 (정규 인코딩은 필드 전체를 생략합니다).

4.3 공개 라운드 매핑

drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
매개변수 출처 예시
unlock_at 사용자가 선택한 UTC Unix 초 1735689600 (2025-01-01 00:00:00 UTC)
chain_genesis_time drand 체인 정보 (genesis_time) 1595431050
chain_period_seconds drand 체인 정보 (period) 30

ceil() 연산은 공개 시간이 ≥ unlock_at인 첫 번째 drand 라운드를 선택합니다. 이는 qub이 선택된 공개 시간 이전에 복호화 가능해지지 않음을 보장합니다.

경계 케이스: (unlock_at - chain_genesis_time)chain_period_seconds로 정확히 나누어떨어지면, 결과는 정확히 그 라운드입니다 — qub은 그 라운드의 공개 시간에 정확히 잠금 해제됩니다.

검증: unlock_at은 봉인 시점에 미래여야 합니다. unlock_atcreated_at으로부터 10년 이상 떨어져서는 안 됩니다 (장기 drand 의존성 리스크를 제한하기 위함; UI는 2년 이상의 공개 일자에 대해 경고해야 합니다).


5. 와이어 형식 뉴타입

와이어 형식 뉴타입은 CBOR 바이트를 JSON, 원시 평문, 또는 다른 바이트 인코딩과 혼동하는 것에 대한 컴파일 타임 안전성을 제공합니다.

타입 포함 생성자 소비자
SealedQubCbor SealedQub의 정규 CBOR serialize_sealed_qub() 영구 저장소 업로드, 열람자 페치
QubEnvelopeCbor QubEnvelope의 정규 CBOR 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, 제한된 마크다운) 유료 50 KB / 무료 10 KB 렌더링 규칙은 §10 참조. 무료 / 유료 분할은 업로드 서비스에 의해 강제됩니다. 프로토콜 계층의 절대 상한은 50 KB입니다.
0x02 예약 (향후) 향후 콘텐츠 타입을 위해 할당됨; v1에서는 유효하지 않습니다. 열람자는 아래 규칙에 따라 거부해야 합니다.
0x03 약정 (양자 합의, CBOR 본문) 100 KB 본문은 정규 CBOR PactTerms입니다 (§6.1). 공동서명자 서명은 §9.7에 따릅니다.
0x04 판정 (작성자 자기 채점, CBOR 본문) 8 KB 본문은 정규 CBOR VerdictBody입니다 (§6.2). 시스템 측 verdict 의도에 의해서만 방출됩니다. 상위 관계는 본문이 아니라 Parent-Tx-Id Arweave 태그에 있습니다. verdict-uplift-plan §3.4 참조.

열람자는 알려지지 않은 콘텐츠 타입을 명확한 사용자 가시 오류와 함께 거부해야 합니다. 열람자는 알려지지 않은 타입을 텍스트로 렌더링하려 시도해서는 안 됩니다.

6.1 약정 본문 (content_type = 0x03)

약정 본문은 PactTerms 값의 정규 CBOR 인코딩입니다:

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 총 크기는 100 KB를 초과해서는 안 됩니다 (§6과 일치).

스키마 판별자. structured/v1 약정의 terms의 첫 번째 행은 { key: "pact_schema", value: "structured/v1" }이어야 합니다. 이 마커가 없는 행은 "custom" 약정이며 구조화된 검증이나 스키마 인식 렌더링을 받지 않습니다.

고정된 확인 슬롯. structured/v1 약정은 다음 키 하에 정확히 4개의 확인 행을 가집니다:

"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"

각각의 value(role, kind) 쌍에 의해 선택된 8개의 고정된 영어 문자열 중 하나이며, 여기서 role ∈ { seller, buyer, provider, client }, kind ∈ { standard, capacity }입니다. 문자열 자체는 규범적 프로토콜 데이터입니다 — 두 당사자의 ML-DSA-65 서명이 body_hash를 통해 정확한 바이트에 결속됩니다. 이들은 현지화되지 않습니다; 서명된 본문은 언어 중립적입니다. 어떠한 문구 변경도 새로운 스키마 버전 (structured/v2)을 요구합니다.

8개의 문자열, 그 조회 (acknowledgement_for(role, kind)), 그리고 각각의 근거는 참조 구현에 의해 고정됩니다. 준수 구현은 바이트 동일한 확인 값을 방출해야 합니다; 네 가지 역할 조합 모두를 다루는 골든 픽스처 SHA3-256 body-hash 테스트가 어떤 드리프트라도 포착합니다.

열람자 표시 순서. 확인 문자열에는 "described above"와 같은 어구가 포함되어 있으며, 이는 설명 / 범위 행이 확인보다 먼저 렌더링됨을 전제로 합니다. 열람자는 terms 배열을 CBOR 순서로 렌더링해야 합니다; 재정렬은 산문의 의미를 깨뜨립니다.

상대방 연락처. 당사자 B의 contact가 유효한 이메일 주소일 때, qub 업로드 서비스는 스테이지 시점에 검토 / 공동서명 초대 이메일을 자동 발송하고, 이후의 공동서명을 동일한 주소의 검증과 결속합니다 (§9.7). 당사자 B 연락처가 부재한 약정은 여전히 공동서명될 수 있지만, 오직 대역 외 채널을 통해서만 가능합니다 — 서비스는 일치하는 15분 이메일 검증 마커를 생성할 수 없는 공동서명 요청을 거부합니다.

6.2 판정 본문 (content_type = 0x04)

판정 본문은 VerdictBody 값의 정규 CBOR 인코딩입니다:

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를 초과해서는 안 됩니다 (위 레지스트리 행과 일치).

Outcome 열거형. 와이어 바이트는 의도 중립적입니다; 네 가지 구분 Right / Partial / Wrong / Unfalsifiable은 판정을 동반하는 모든 의도의 결과 공간을 포괄합니다. 의도별 라벨(Right에 대해 "적중", "지킴", "출시", "확인" 등)은 상위 qub의 의도에 따라 해소되는 열람자 측 렌더링 사항입니다 — 와이어는 언어 및 의도 중립으로 유지됩니다. 1..=4 범위를 벗어난 값은 디코드 시점에 거부해야 합니다.

상위 연결. 판정 qub은 본문에 상위 참조를 담지 않습니다. 상위 qub의 Arweave 트랜잭션 id는 업로드 시점에 Parent-Tx-Id 저장소 태그로 방출됩니다(§7 저장소 태그 계층). 이렇게 하면 본문은 자기 평가에 대한 자체 완결적 서명 진술로 유지되며, 감사 체인("무엇에 대해 맞았는가?")은 Arweave 태그 조회를 통해 성립됩니다.

증거 URL 안전성 (규범적). evidence_url이 존재할 때, 검증자(작성 측, 와이어 측, Worker 엣지)는 다음을 강제해야 합니다:

  1. HTTPS만. 문자열은 바이트 시퀀스 https://로 시작해야 합니다. 그 외의 스킴 — http, ftp, javascript, data, file 등 — 은 거부합니다.
  2. 길이 상한. ≤ 2,048 바이트 (브라우저 URL 실용 한계).
  3. NFC + 적대적 코드포인트 확인. titlereflection과 동일한 규칙 — bidi-override / 영폭 / tag-block / BOM / C0 / C1 코드포인트는 거부합니다. 정의는 Rust의 crate::handle::contains_hostile_text_codepoint 및 TS의 workers/api/src/utils/unicode.ts::isHostileCodepoint와 일치해야 합니다 (일치 유지).
  4. 공백 없음, ASCII 제어 문자 없음. URL 어디에서도 공백 / DEL / 0x20 미만 바이트는 거부합니다 — bidi 규칙이 막지 못하는 \n/\t 주입 벡터를 차단합니다.
  5. 비어 있지 않은 호스트 세그먼트. https://와 첫 /, ?, 또는 # 사이의 모든 것은 비어 있지 않아야 합니다.

서버 측 페치 없음. Worker는 URL을 프록시하거나, 페치하거나, 미리보기 해서는 안 됩니다. 프로토콜은 문자열을 저장하며, 렌더링은 rel="nofollow noopener noreferrer" target="_blank"와 함께 링크 텍스트 옆에 표시되는 호스트와 함께 열람자 측에서 일어납니다.

회고. 선택적인 작성자 작성 회고 텍스트("무엇이 바뀌었나요, 무엇을 배우셨나요"). title과 동일한 NFC + 적대적 코드포인트 검증. 비어 있거나 공백만 있는 입력은 구성 시점에 부재로 접힙니다.

스키마 버전. 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 (64자 소문자 16진수로 표현된 작성자의 §9.3 공개 키 지문), Parent-Tx-Id (회신 체인에 대한 부모 qub의 저장소 트랜잭션 ID, 43자 base64url).

Author 태그는 qub별로 옵트인입니다: 참조 작성자 앱은 사용자가 봉인 시점에 공개 귀속을 명시적으로 활성화한 경우에만 이를 첨부합니다. 토글이 꺼져 있을 때 — 기본값 — Author 태그는 작성되지 않고 qub은 체인상에서 귀속 없이 존재합니다: 영구 저장소의 어떤 것도 업로드를 작성자의 핸들, 이메일, 또는 다른 qub과 연결하지 않습니다. 토글이 켜져 있을 때, Author 지문은 §9.5 증명 체인을 통해 작성자가 선택한 @handle로 해석됩니다. 회신 체인 관계 및 Intent는 비식별적입니다. 외부 래퍼 (§13)는 내부 본문을 암호문 상관 관계로부터 보호합니다 — 수집자가 qub 형태의 업로드를 인식하고 drand 라운드가 공개된 후 대량 복호화하는 것을 막습니다.

참조 서비스는 의도적으로 App-Name, App-Version, 또는 Type 태그를 첨부하지 않습니다: 그러한 단일 값 필터는 GraphQL 쿼리에 qub 전체 말뭉치를 반환할 것이며, 이는 래퍼의 본문 전용 기밀성 범위와 일치하지 않습니다.

준수 검증자는 §11 제3자 검증을 위해 어떠한 저장소 태그에도 의존해서는 안 됩니다; 본문 해시 / 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 근거

qub은 영구 저장소에 저장됩니다. 작성자 서명은 무기한 위조 불가능해야 하며, 이것이 v1.0이 qub의 영구 수명 내에 보안성이 저하될 수 있는 고전적 방식이 아닌 포스트 양자 ML-DSA-65 방식 (FIPS 204)을 사용하는 이유입니다.

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이어야 합니다. 참조 구현은 이를 crates/qub-core/src/signing.rs에서 상수 ORG_ID_PRESENT_INDIVIDUAL = 0x00으로 노출합니다; 검증을 위해 sig_input을 재구성하는 열람자는 동일한 바이트를 방출해야 합니다.

서명 범위 — 무엇이 다루어지고 무엇이 다루어지지 않는가. sig_input은 네 개의 봉투 필드에 결속됩니다: version, qub_id, body_hash, unlock_at (고정된 도메인 분리자 및 org_id_present 바이트에 더해). 그 네 개 중 세 개는 구조적 불변량입니다: qub_id 자체는 §4.1 원본 이미지를 통해 version, content_type, created_at, unlock_at, outcome_at, drand_round, body_hash로부터 도출되므로, 그 필드들의 어떤 변경도 다른 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 또는 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 핸들, 이메일 주소, 소셜 핸들, 패스키 자격 증명과 같은 사람이 인식 가능한 정체성 청구로 매핑하는 것 — 은 열람자측 점진적 향상이며 서명 검증에 필요하지 않습니다. 증명을 표시 정체성으로 해석하는 열람자는 다음 우선순위를 적용해야 합니다:

handle > email > social > fingerprint

지문 폴백은 SHA3-256(author_pubkey)의 소문자 16진수입니다; 이는 서명된 어떤 qub에 대해서도 항상 사용 가능합니다. 열람자는 표시를 위해 이를 축약할 수 있습니다 — 참조 열람자는 qub: 뒤에 처음과 마지막 4 바이트를 렌더링합니다 (qub:<8 hex>…<8 hex>).

준수 검증자는 qub API에 연락하지 않고, 영구 저장소 및 drand 이외의 어떠한 네트워크 없이도, 어떠한 서버측 조회 없이도 §9.4의 모든 검사를 완료할 수 있습니다. 증명 해석은 서명 검증이 성공한 후에만 수행되는 별도의 최선 노력 단계입니다.

9.6 크기 영향

Ed25519 ML-DSA-65
서명 64 바이트 3,309 바이트
공개 키 32 바이트 1,952 바이트
qub당 총합 96 바이트 5,261 바이트
저장 비용 차이 (~$5/MB 기준) ~$0.0005 ~$0.026

500–2,000 바이트의 텍스트 qub에 대해, ML-DSA-65는 저장 크기를 대략 세 배로 만듭니다. 절대 비용은 무시할 만합니다.

9.7 공동서명자 검증 (약정 양자 합의)

양자 합의 (content_type = 0x03)에 대해, 두 번째 서명 계층은 두 당사자 모두가 동일한 조건에 동의했음을 증명합니다.

봉투 필드:

두 필드 모두 함께 존재하거나 모두 부재해야 합니다. 정확히 하나만 존재하면, 열람자는 무결성 오류를 보고해야 합니다.

검증 절차:

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."

속성:

이메일 결속 게이트 (운영적). 스테이지된 약정이 당사자 B 이메일 연락처 (§6.1)를 포함할 때, qub 업로드 서비스는 스테이징 ID 및 그 연락처의 정규화 이메일 해시 모두와 일치하는 단명 이메일 검증 마커가 존재하지 않으면 공동서명 요청을 거부해야 합니다. 매직 링크 토큰이 staging_id를 포함하고 검증된 주소가 SHA-256(normalise_email(party_b.contact))과 일치할 때 /api/v1/auth/verify에 의해 마커가 작성됩니다 — 여기서 normalise_email(addr)은 로컬 부분의 대소문자를 보존하고 도메인 부분만 소문자화하며 (RFC 5321 §2.3.11에 따라), 여기서 SHA-256은 NIST FIPS 180-4 해시입니다 (§4 도출에서 사용되는 SHA3-256과는 구별됨) — 발행 후 900초 (15분) 후에 만료됩니다. 이는 운영적 사칭 방지 게이트이며, 온체인 qub 증명의 일부가 아닙니다 — §11을 재현하는 제3자 검증자는 어떠한 서버측 조회 없이 영구 저장소 및 drand만을 필요로 합니다. 마커는 오직 서버측에만 존재하며 서명된 본문의 일부가 절대 아닙니다.

크기 영향 (ML-DSA-65 작성자 + 공동서명자):

구성 요소 크기
작성자 서명 3,309 바이트
작성자 공개 키 1,952 바이트
공동서명자 서명 3,309 바이트
공동서명자 공개 키 1,952 바이트
총 암호 오버헤드 10,522 바이트
저장 비용 차이 ~$0.05

10. 마크다운 렌더링 및 살균

이 절은 보안에 결정적입니다. 열람자는 제한된 마크다운 부분집합을 사용하여 텍스트 qub (content_type = 0x01)을 렌더링합니다.

10.1 허용되는 요소

10.2 금지되는 요소

요소 처리
원시 HTML (<div>, <script> 등) 완전히 제거됨. 어떠한 HTML도 통과하지 않음.
이미지 (![alt](url)) 제거됨. 이미지 구문은 출력에서 제거됨.
링크 ([text](url)) URL이 보이는 평문으로 렌더링됨. 자동 링크 없음. 명시적 사용자 행동 없이는 클릭 불가능.
위험한 URL 스킴 javascript:, data:, vbscript:, file: — 제거됨.
iframe, 임베드, 객체 제거됨.
HTML 엔티티 안전한 경우에만 표시 문자로 디코딩됨.

10.3 구현

구현은 차단 목록이 아닌 엄격한 허용 목록 파서를 사용해야 합니다. 권장 접근 방식:

  1. pulldown-cmark (또는 동등물)을 사용하여 마크다운을 파싱합니다.
  2. AST를 순회하며 허용 목록 (§10.1)에 없는 노드를 모두 제거합니다.
  3. 링크 노드의 경우: URL을 클릭 가능한 <a> 요소가 아닌 보이는 텍스트로 방출합니다.
  4. 필터링된 AST를 타입화된 중간 표현으로 변환합니다 (예: 안전한 변형만 가지는 MarkdownNode 열거형). 원시 HTML은 이 IR에서 구조적으로 표현 불가능합니다.
  5. 타입화된 IR로부터 대상 뷰 계층으로 렌더링합니다 (예: 반응형 뷰 컴포넌트, DOM 노드). 어느 지점에서도 HTML 문자열 연결 또는 innerHTML 사용 없음.

차단 목록 접근 방식은 새로운 마크다운 확장이나 파서의 특이사항이 필터링되지 않은 요소를 도입할 수 있어 취약합니다. 타입화된 AST 접근 방식은 XSS를 구조적으로 불가능하게 만듭니다 — 임의의 HTML을 담을 수 있는 변형이 존재하지 않습니다.

10.4 크기 및 구조 제한


11. 제3자 검증

어떠한 제3자도 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_alg0x01 없이는 누구든지 이 콘텐츠를 봉인했을 수 있습니다.
의도 qub은 콘텐츠와 타이밍을 증명하며, 작성자가 주관적으로 의미한 바를 증명하지 않습니다.
사전 이벤트 타이밍 저장소 블록 포함은 실제 업로드보다 몇 분 지연될 수 있습니다. 약속 타임스탬프는 블록 시간이며, 사용자가 "봉인하다"를 누른 순간이 아닙니다.

12. 버전 관리

12.1 프로토콜 버전

SealedQubQubEnvelope 모두의 version 필드 (u8)는 주 프로토콜 버전을 식별합니다.

12.2 버전 히스토리

버전 설명
v1 0x01 공개 텍스트 qub (content_type 0x01), 약정 양자 합의 (0x03, structured/v1 스키마, ML-DSA-65 작성자 + 공동서명자), tlock, SHA3-256

12.3 정방향 호환성

알려지지 않은 선택적 CBOR 맵 키 (§3.2 정규 순서에 없는 키)를 가진 QubEnvelope을 만나는 v1 열람자는 그 키를 무시하고 알려진 필드를 사용하여 검증을 진행해야 합니다. 이를 통해 주 버전 충돌 없이 향후 작은 추가 (예: 새로운 메타데이터)가 가능합니다.

sig_alg = 0x01 (ML-DSA-65)을 만나지만 ML-DSA-65 검증 지원이 부족한 v1 열람자는 qub을 완전히 거부하기보다 "서명은 존재하나 검증할 수 없음" 알림과 함께 qub 콘텐츠를 표시해야 합니다. 참조 구현은 오늘 v1 레지스트리에 다른 유효한 알고리즘이 없기 때문에 0x000x01을 제외한 모든 sig_alg 값을 거부합니다 — 엄격한 거부와 소프트 페일은 세 번째 알고리즘이 등록될 때까지 관찰적으로 동일합니다. §9.2가 새 항목을 인정하는 순간 위의 소프트 페일 동작은 부담을 지게 되며, 그 시점에서 참조 열람자는 소프트 페일하도록 업데이트될 것입니다.

12.4 외부 래퍼 버전

§13에 기술된 OuterWrapper는 SealedQub.versionQubEnvelope.version독립적인 자체 version 바이트를 가집니다. 두 버전 공간은 별도로 발전합니다: 향후 포스트 양자 안전 대칭 교체는 내부 프로토콜 버전을 건드리지 않고 래퍼 바이트를 올리고, 향후 프로토콜 계층 추가 (예: 새 봉투 필드)는 래퍼 바이트를 건드리지 않고 내부 버전을 올립니다.

OUTER_WRAPPER_VERSION_* 알고리즘 상태
OUTER_WRAPPER_VERSION_1 0x01 12 바이트 논스, 16 바이트 인증 태그를 가진 AES-256-GCM, qub_id에 결속된 AAD v1 기본값
0x020xFF 예약됨 향후

열람자는 알려지지 않은 래퍼 버전을 명확한 오류와 함께 거부해야 합니다. 프로토콜은 구체적인 마이그레이션 동인이 나타날 때까지 (예: 다른 AEAD를 선호하는 NIST 지침) 의도적으로 래퍼 버전 공간을 좁게 유지합니다; 0x02 슬롯은 알고리즘을 도입하는 동일한 개정판에서 할당될 것입니다.


13. 외부 암호화 래퍼

13.1 근거

프로토콜 계층 (QubEnvelope → tlock → SealedQub)은 봉인된 qub을 시간 잠금시킵니다: 본문은 unlock_at까지 그리고 drand 라운드 서명이 공개될 때까지 읽을 수 없습니다. 그러나 잠금 해제 후에는 라운드 서명이 공개되고 SealedQub의 정규 CBOR 형태가 인식 가능하므로, 영구 저장소 트랜잭션을 색인한 수집자는 qub 전체 말뭉치를 대량으로 복호화할 수 있습니다.

외부 암호화 래퍼는 정규 SealedQubCbor와 영구 저장소에 기록되는 바이트 사이에 추가적인 대칭 AEAD 계층을 끼워 넣음으로써 그 채널을 닫습니다. 256비트 키 K오직 전달 링크의 URL 프래그먼트와 사용자 기기에만 존재합니다; 브라우저는 URL 프래그먼트를 서버로 전송하지 않으므로, qub.social, 모든 저장소 게이트웨이, 그리고 그 앞에 있는 모든 CDN은 관찰적으로 K에 대해 맹목적입니다. 따라서 영구 저장소의 모든 qub은 작성자가 공유하기로 선택한 URL 없이는 평문을 복구할 수 없는 불투명한 암호문입니다.

순효과:

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
}

필드 불변량.

CBOR 인코딩. 동일한 키 정렬 규칙 (인코딩된 바이트 길이 오름차순, 그 다음 사전식 정렬)을 가진 §3에 따른 정규 CBOR. 네 개의 키는:

인코딩된 바이트 순서
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

따라서 OuterWrapper CBOR의 첫 바이트는 4-항목 맵에 대한 확정 길이 맵 헤더 (0xA4)입니다.

13.4 qub_id에 대한 AAD 결속

래퍼는 qub_id를 AEAD 추가 인증 데이터로 결속합니다. 이는 세 종류의 공격에 대한 부담을 지는 구조적 방어입니다:

공격 방어
래퍼의 다른 qub_id 필드 아래로 암호문 이동 AAD 불일치 → AEAD 인증 실패
qub A의 URL 프래그먼트를 qub B의 영구 저장소 바이트와 혼합 AAD 불일치 → AEAD 인증 실패
업로드 후 래퍼의 qub_id 필드 변조 AAD 불일치 → AEAD 인증 실패

래퍼 평문에 qub_id를 포함시키는 것은 열거 면역성을 의미 있게 약화시키지 않습니다 — qub_id 자체가 다이제스트로부터 복구 가능한 원본 이미지가 없는 §4.1 원본 이미지의 SHA3-256 해시이며, 래퍼 바이트를 이미 수집한 열거자는 업로드의 존재 자체로부터 추론할 수 없는 어떠한 것도 가시적인 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, 잘못된 논스, AAD 불일치, 변조된 암호문은 모두 동일한 DECRYPT_FAILED 오류를 생성합니다. 이는 의도적인 AEAD 속성입니다: 실패 모드를 구별한다면 원격 공격자가 잘못된 형식의 래퍼를 보내고 응답 시간을 측정하여 탐색할 수 있는 사이드 채널을 만들어낼 것입니다. 참조 구현은 모든 AEAD 실패를 단일 오류 형태로 붕괴시켜야 합니다.

13.6 키 자료 및 배포

래핑 키 K는 CSPRNG에 의해 qub당 생성되는 256비트 균일 무작위 값입니다. 참조 구현은 이를 다음에서 소싱합니다:

배포: K는 URL 안전 base64 (RFC 4648 §5, 패딩 없음)로 인코딩되어 전달 링크의 프래그먼트 구성 요소로 추가되어야 합니다:

delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>

프래그먼트는 준수하는 브라우저에 의해 어떠한 서버로도 전송되지 않습니다. 사용자 기기를 넘어 프래그먼트를 포함한 전체 전달 링크를 유지하는 복구 채널 (서버측 기록 색인, 옵트인 이메일 자동 전송)은 기본 암호 분쇄 자세에 대한 명시적인 교환이며, 명시적인 사용자 동의에 게이트되어야 합니다.

프래그먼트 손실. 사용자가 URL 프래그먼트를 잃고 복구 채널이 없으면, qub은 읽을 수 없습니다. 이는 설계의 부담을 지는 교환이며 봉인 시점에 사용자에게 공개되어야 합니다. MVP는 명시적인 "이 URL을 저장하라" 문구 및 옵트인하는 사용자에 대한 검증된 이메일 복구 채널로 봉인 시점 공개를 강화합니다.

13.7 이 절의 범위 외

13.8 공개 qub (래퍼 생략)

외부 래퍼는 전달 계층에서 선택적입니다. 작성자는 qub을 공개로 봉인할 수 있으며, 이 경우 정규 SealedQubCborOuterWrapper 계층 없이 그리고 키 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를 가진 누구나 이를 복호화할 수 있습니다 — K가 없으므로 URL 프래그먼트가 필요하지 않습니다. 이는 서버가 구동해야 하는 표면들을 위한 의도적인 교환입니다: 공개 알림 이메일, 제3자 임베드, 그리고 더 풍부한 공개 후 SEO는 모두 서버가 결코 보유하지 않는 비밀 없이도 작동하는 링크를 필요로 합니다 (§13.6).

생산자가 반드시 고려해야 하는 결과:

비공개 (래핑됨)가 기본값으로 유지됩니다; 공개는 명시적인 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_hashqub_id 값을 산출해야 합니다. 이 테스트 벡터는 가장 먼저 작성되는 단위 테스트여야 합니다. 위의 정규 값들은 참조 구현에 의해 계산되었으며 비트 단위로 일치해야 합니다. 과거의 원본 이미지 레이아웃 (사전 출시 — 어떠한 라이브 qub도 이 값들에 의존하지 않았음): 92 바이트 V1.0 qub_id는 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0였고; 100 바이트 V1.1 qub_id (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 PactTerms CBOR (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 바이트 및 SHA3-256 body_hash는 참조 구현에 의해 계산됩니다. 구현은 이 입력에 대해 바이트 동일한 CBOR을 생성해야 합니다.

구현은 또한 모든 유효한 PactTerms 입력에 대해 serialize(parse(serialize(pact))) == serialize(pact)을 검증해야 합니다 (속성 테스트).

14.5 외부 래퍼 교차 언어 벡터

외부 래퍼 (§13)는 crates/qub-core/tests/vectors/wrapper_v1.json에 별도의 정규 픽스처를 가집니다. 각 케이스는 (key, nonce, qub_id, sealed_cbor) 튜플을 불투명한 16진수 입력으로 고정하고 특정 expected_wrapper_hex 출력을 단언합니다. 두 참조 구현 모두 동일한 JSON 파일을 소비합니다:

픽스처는 현재 세 케이스를 고정합니다:

케이스 범위
basic-text-public 가장 작은 현실적인 SealedQub 형태; 선택적 필드 없음. v1.0-전형적 qub에 대한 정규 래퍼 형태를 확립합니다.
with-recipient-pubkey recipient_pubkey가 설정된 SealedQub (Phase 2 경로). 다른 내부 CBOR 키 세트, 다른 qub_id.
longer-body ~4 KiB 본문 — 내부 봉투와 외부 암호문 모두 안에서 멀티바이트 CBOR 길이 접두사를 실행합니다.

구현은 기록된 입력에 대해 바이트 동일한 expected_wrapper_hex를 생성해야 합니다. 픽스처 재생성은 QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors를 요구하며 의도적인 형식 변경을 위해 예약됩니다.


15. 암호 프로파일 거버넌스 (향후)

이 절은 v1에 대해 정보적이며 qub의 암호 기본 요소 중 어디라도 두 번째 알고리즘이 진입하는 첫 시점에 규범적이 됩니다.

15.1 현재 자세

프로토콜 v1은 기본 요소당 정확히 하나의 알고리즘에 결속됩니다:

검증자는 현재 기본 요소별로 키와 서명 길이를 하드코딩합니다. 와이어 형식에 의해 노출되는 민첩성 표면은 없습니다.

15.2 의도된 형태

두 번째 알고리즘이 프로토콜에 진입할 때, 검증자는 기본 요소별로 허용되는 값의 정확한 집합 — sig_alg, drand 체인, 래퍼 버전, 콘텐츠 타입 — 을 나열하는 이름 붙은 CryptoProfile (예: ExqubV1)에 대해 구성될 것입니다. 프로파일은 검증 시점에 고정되며, 대역 내에서 협상되지 않습니다. 활성 프로파일 밖의 어떠한 값도 거부됩니다.

이는 ML-DSA-87을 추가하거나 Ed25519를 활성화하는 것이 기존 검증자 구성을 소급적으로 약화시킬 수 없음을 보장합니다: v1 검증자는 v2 프로파일이 게시된 후에도 v1 검증자로 남습니다.

15.3 트리거 조건

다음 중 어떠한 것이라도 제안되면 §15를 규범적 상태로 승격합니다:

그때까지 §15는 향후 PR이 협상 표면을 처음부터 재논의하기보다 알려진 대상에 대해 착지하도록 마이그레이션 형태를 고정하는 자리 표시자입니다.