이 번역은 참고용입니다. 정식 기준은 영어판이며, 내용이 다를 경우 영어판이 우선합니다. 영어판 읽기

봉인된 PoE

Label 309의 암호화 봉투 — 송신자가 콘텐츠를 하나 이상의 수신자 키 앞으로 봉인하면서도, 체인에는 평문 해시와 래핑된 키 슬롯만 실릴 뿐 평문은 결코 실리지 않고 수신자도 결코 드러나지 않는 방식.

봉인된 PoE는 평문에 타임스탬프가 찍힌 커밋먼트를 온체인에 기록하면서도, 그 평문은 선택된 대상자만 읽을 수 있도록 유지합니다. 온체인 레코드에는 평문 해시 — 다른 모든 레코드와 똑같이, 시점에 대한 증명 — 와 더불어, 콘텐츠 암호화 키를 복구하는 데 필요한 자료를 담은 암호화 봉투(enc)가 실립니다. 암호문 자체는 결코 체인에 닿지 않으며, 콘텐츠 주소 지정 URI(ar:// 또는 ipfs://)에 놓입니다. 체인상에는 평문을 드러내는 것이 전혀 없으며, 수신자가 누구인지를 드러내는 것도 전혀 없습니다.

이 페이지는 enc 봉투를 규정합니다. 상호 배타적인 두 가지 키 전달 경로, 수신자별 키 슬롯, 슬롯셋 MAC, 분할된 콘텐츠 STREAM, 그리고 수신자가 자신 앞으로 온 메시지를 발견하고 열기 위해 수행하는 시험 복호화를 정의합니다. 수신자 키 자체 — 시드에서 파생된 X25519 및 X-Wing 키쌍 — 는 에서 정의되며, 이 페이지는 그것을 사용합니다. enc 맵이 레코드 맵 안에서 차지하는 위치, 그리고 그것을 체인에 싣는 본문 전체 전송은 레코드에서 정의됩니다.

HPKE가 아닙니다

이것은 RFC 9180 HPKE가 아닙니다. 수신자별 캡슐화, HKDF로 파생되는 키 암호화 키, 그리고 AEAD로 래핑되는 콘텐츠 암호화 키를 갖춘 age 스타일의 다중 수신자 KEM-then-wrap 설계이며, age v1의 스탠자 패턴을 정규 CBOR로 옮긴 것입니다. suite_id도 없고 LabeledExtract/LabeledExpand 캐스케이드도 없습니다. HPKE의 분석이 아니라 ECIES 문헌과 age v1 명세에 비추어 평가하시기 바랍니다.

모델과 그 프라이버시 속성

송신자는 특정 평문이 특정 대상자 앞으로 시각 T에 봉인되었음을 증명하는, 영구적이고 타임스탬프가 찍힌 커밋먼트를 게시하면서도 그 대상자만 읽을 수 있도록 보장하고자 합니다. 해시만 담은 PoE는 시점 주장은 제공하지만 대상자 결속은 없으며, 공개된 암호문에 대한 PoE는 기밀성을 전혀 확보하지 못합니다. 봉인된 PoE는 그 둘을 잇습니다. 레코드는 평문 해시(공개, 타임스탬프 첨부)에 커밋하고 키 전달 자료를 enc에 실으며, ar:// 또는 ipfs:// URI에 놓인 암호문은 일치하는 잠금 해제 비밀이 없으면 복호화할 수 없습니다.

이 구성은 메시지에 대해서는 가능한 한 적게, 대상자에 대해서는 아무것도 체인에서 새지 않도록 의도적으로 설계되었습니다.

  • 평문은 결코 체인에 존재하지 않습니다. 체인에는 그 해시와 래핑된 키만 있습니다. 나중에 평문을 입수한 사람은 누구든 "바로 이 평문이 블록 시각 T에 커밋되었다"는 것을 증명할 수 있지만, 그 밖의 누구도 무엇이 봉인되었는지는 알지 못합니다.
  • 수신자 공개 키는 결코 체인에 존재하지 않습니다. 수신자의 공개 키는 enc의 어디에도 나타나지 않습니다. 수신자는 슬롯을 성공적으로 시험 복호화함으로써만 어떤 메시지가 자신 앞으로 온 것임을 인식합니다. 읽어 들일 수신처 필드는 존재하지 않습니다. 후보 키가 전혀 없는 관찰자가 알 수 있는 것은 슬롯 개수, KEM 계열(enc.kem), 그리고 봉인됨/봉인되지 않음의 구분뿐입니다. 이보다 강한 속성 — 후보 수신자 키를 보유한 적대자조차 어떤 슬롯이 그중 어느 것을(있다면) 겨냥하는지 시험할 수 없다는 속성 — 은 키 프라이버시이며, 고전 x25519 경로에 대해서만 주장되고 하이브리드 mlkem768x25519 경로에 대해서는 주장되지 않습니다(익명성과 KEM별 분기 참조).
  • 수신자들은 서로에 대해 아무것도 알지 못합니다. 수신자별 각 슬롯은 불투명한 래핑된 키입니다. 자신의 슬롯을 연 수신자는 다른 어떤 수신자의 키도 파생할 수 없으며, 또 누가 수신처로 지정되었는지도 알 수 없습니다.
  • 슬롯 순서는 아무것도 누설하지 않습니다. 송신자가 수신자를 나열하는 순서(예컨대 "주요 수신자를 먼저")는 민감한 메타데이터입니다. 슬롯 배열은 게시 전에 CSPRNG로 셔플되므로 위치상의 순서마저도 아무런 신호를 담지 않습니다.
  • 서명되지 않은 봉인된 PoE는 송신자의 익명성을 보존합니다. 저작자성 서명은 선택 사항입니다(서명 참조). sigs[]가 없는 봉인된 레코드는 체인상에 송신자 신원을 전혀 결속하지 않습니다. 이는 내부고발 제보, 봉인 입찰 경매, 증거 에스크로가 요구하는 바로 그 특성입니다.

체인이 실제로 드러내는 것은 한정적입니다. 어떤 레코드가 봉인된 PoE라는 사실(enc가 존재함), 평문 해시, 블록 타임스탬프, 그리고 슬롯의 개수(배열 길이)입니다. 이 개수가 수신자에 인접한 유일하게 노출되는 사실이며, 그것은 "몇 명인지"만 드러낼 뿐 "누구인지"는 결코 드러내지 않습니다. 레코드 간의 타이밍 상관관계는 와이어 수준의 암호로는 해결할 수 없는 메타데이터 차원의 문제입니다. 이를 무력화해야 하는 송신자는 게시를 민감한 타임라인에서 분리하여 일괄 처리해야 합니다.

수신자 공개 키는 **대역 외(out of band)**로 교환됩니다. Label 309는 어떠한 발견 메커니즘도 규정하지 않습니다. 수신자는 자신의 웹사이트, DNS 레코드, 소셜 프로필, QR 코드, 또는 온체인 자기 증명 등 어떤 방법으로든 키를 게시할 수 있습니다. 검증자는 수신자 키 바이트를 입력으로 받아들이며, 그것이 누구의 키인지에 대해서는 아무런 주장도 하지 않습니다. 그 출처는 PGP 키를 이메일로 보낼 때와 똑같이 송신자의 신뢰 판단입니다.

봉투와 그 두 경로

enc 맵은 공통 필드와 더불어, 상호 배타적인 두 가지 키 전달 경로 중 정확히 하나를 싣습니다. 구조 검증기는 이 배타성을 강제합니다. 둘 다 싣거나 둘 다 싣지 않은 레코드는 거부됩니다.

필드상태의미
schemeREQUIRED구성 계열 버전. v1은 scheme = 1을 정의합니다.
aeadREQUIRED콘텐츠 포맷 식별자. v1은 "chacha20-poly1305-stream64k"를 정의합니다.
nonceREQUIRED24개의 무작위 바이트 — 콘텐츠 키와 모든 슬롯 KEK의 봉투별 고유 솔트.
kemslots 경로 전용슬롯별 KEM 선택자("x25519" 또는 "mlkem768x25519").
slots두 경로 중 하나수신자별 키 슬롯의 배열(다중 수신자).
slots_macslots 경로 전용슬롯셋과 항목의 해시 주장을 콘텐츠 키에 결속하는 32바이트 HMAC.
passphrase다른 한 경로패스프레이즈 KDF 블록(패스프레이즈 파생 키).
  • enc.slots — 다중 수신자. 봉투는 각각 독립적으로 래핑된 N개의 키 슬롯을 수신자마다 하나씩 싣습니다. 슬롯 중 하나에 일치하는 개인 키가 없으면 암호문은 복호화할 수 없습니다. 자세한 내용은 아래 슬롯과 슬롯셋 MAC을 참조하십시오.
  • enc.passphrase — 패스프레이즈 파생. 봉투에는 슬롯이 없으며, 콘텐츠 키는 정규화된 패스프레이즈로부터 직접 파생됩니다. 자세한 내용은 아래 패스프레이즈 경로를 참조하십시오.

두 경로는 scheme, aead, nonce를 공유합니다. 차이는 존재하는 키가 무엇인지, 그리고 그 결과로 키 커밋먼트가 어디에 놓이는지에 있습니다. slots 경로에서는 커밋먼트가 온체인에 있습니다. slots_mac은 헤더 필드, 슬롯셋, 그리고 항목의 해시 주장을 고정하는 트랜스크립트에 대한 CEK 키 HMAC이므로, 수신자는 무언가를 가져오기 전에 올바른 키임을 확인합니다. 패스프레이즈 경로에는 결속할 슬롯이 없으므로, 커밋먼트는 암호문 블롭 내부에 실리는 32바이트 헤더입니다. 패스프레이즈 추측을 시험하려면 블롭 자체가 필요하며, 공개 체인만으로는 결코 충분하지 않습니다. 각 경로는 동일한 canonicalEncode 함수로 자신의 트랜스크립트를 직렬화하고, 생성자나 검증자는 slots / passphrase 중 어느 것이 존재하는지를 검사하여 경로를 선택합니다. 이 두 경로는 망라적이며 상호 배타적입니다.

enc.scheme은 레코드의 v 필드와 무관하게 구성 계열 의 이름을 붙입니다. 검증자는 enc.scheme === 1을 요구하고 그 밖의 모든 값을 거부해야 합니다(MUST). 이 필드는 미래의 횡단적 변경 — 다른 슬롯셋 MAC 일정이나 콘텐츠 포맷 — 을 위해 예약되어 있으며, KEM을 추가하기 위한 것이 아닙니다. 슬롯별 KEM은 enc.kem으로 선택되며, 아래 두 KEM은 모두 첫 릴리스부터 scheme = 1 아래에 존재합니다. 더 넓게 보면, enc.scheme: 1은 MAC과 콘텐츠 포맷뿐 아니라 전체 암호 스위트를 식별합니다. canonicalEncode 규칙, 슬롯 스키마, HKDF 해시, HMAC 해시, 슬롯별 래핑 AEAD, 분할 STREAM 콘텐츠 포맷, slots 및 passphrase 트랜스크립트 스키마(hashes_hash 항목 결속 포함), 암호문 내 패스프레이즈 커밋먼트, 고정된 X-Wing 리비전, 도메인 분리 라벨, Argon2id 버전과 프로파일, 그리고 패스프레이즈 정규화 프로파일이 모두 이것에 의해 고정됩니다. 따라서 그중 어느 하나라도 변경하려면 새로운 enc.scheme 값이 필요합니다.

콘텐츠 계층

두 경로는 모두 평문에 대한 한 차례의 대칭 연산으로 수렴하며, 그 키는 단일 32바이트 **콘텐츠 암호화 키(CEK)**로부터 파생된 값입니다. CEK는 슬롯이 전달하는 것(각 슬롯이 그것을 래핑합니다)이거나 패스프레이즈 KDF가 산출하는 것입니다. 콘텐츠는 CEK로 직접 암호화되지 않습니다. 대신 각 경로는 CEK의 HKDF 리프로서 별도의 32바이트 콘텐츠 키를 파생합니다. 이는 봉투별 고유 enc.nonce를 솔트로 삼아 경로별 info 아래에서 파생되므로, 키 전달 계층과 콘텐츠 계층이 동일한 바이트에 대해 동일한 프리미티브에 키를 부여하는 일은 결코 없습니다.

CBOR
content_key = HKDF-SHA-256(ikm = CEK, salt = enc.nonce,
                           info = <"cardano-poe-payload-v1" on the slots path,
                                   "cardano-poe-payload-passphrase-v1" on passphrase>,
                           L = 32)

그런 다음 콘텐츠는 콘텐츠 포맷 식별자 chacha20-poly1305-stream64k로 명명된 분할 STREAM 안에 봉인됩니다. 이것은 age v1 명세의 STREAM 레이아웃입니다. 즉, 고정 크기 청크로 분할된 평문에 대한 ChaCha20-Poly1305(RFC 8439, 12바이트 논스 변형)이며, 각 청크는 콘텐츠 키 아래에서 청크별 카운터 논스로 봉인됩니다.

CBOR
cipher       : ChaCha20-Poly1305 (RFC 8439; 12-byte nonce, 16-byte tag)
CHUNK_SIZE   : 65536 plaintext bytes per non-final chunk
chunk nonce  : uint88_be(counter) || final_flag      ; 12 bytes
               counter starts at 0, +1 per chunk;
               final_flag = 0x01 on the final chunk, 0x00 otherwise
per-chunk AAD: empty
final chunk  : 0 to 65536 plaintext bytes; every non-final chunk is exactly 65536
empty input  : exactly one final chunk of zero-length plaintext (a lone 16-byte tag)

ciphertext = seal(chunk_0) || seal(chunk_1) || ... || seal(chunk_final)
                                             ; each sealed chunk = plaintext length + 16 bytes

최종 플래그는 마지막 청크를 나머지로부터 도메인 분리하며, 바로 이것이 절단을 탐지 가능하게 만듭니다. 마지막 청크가 0x01 플래그를 지니지 않은 스트림, 마지막이 아닌 청크에 붙은 0x01 플래그, 최종 청크 뒤에 따라오는 데이터, 또는 CHUNK_SIZE보다 짧은 비최종 청크는 모두 복호화에 실패해야 합니다(MUST, TAMPERED_CIPHERTEXT). 봉인된 모든 청크는 적어도 그 16바이트 태그만큼은 되므로, 이 레이아웃은 구조적 하한도 함의합니다. 잘 구성된 slots 경로 암호문 블롭은 16바이트 — 빈 최종 청크의 단독 태그 — 보다 결코 짧지 않습니다.

청크별 AAD는 설계상 비어 있습니다. 모든 컨텍스트가 전이적으로 콘텐츠에 결속되어 있기 때문입니다. 콘텐츠 키는 CEK로부터 파생되고, CEK는 slots 경로에서는 slots_mac(그 트랜스크립트가 scheme, path, aead, kem, nonce, 슬롯셋, 그리고 항목의 해시 주장을 망라합니다)에 의해, 패스프레이즈 경로에서는 암호문 내 커밋먼트에 의해 헤더 전체에 커밋되어 있습니다. 헤더 필드를 하나라도 뒤집으면 수신자는 다른 키를 파생하거나 받아들이게 되어 복호화가 실패합니다. 청크별 AAD는 안전성을 더하지 않은 채 동일한 컨텍스트를 청크마다 다시 결속할 뿐입니다.

카운터 기반 청크 논스가 안전한 이유는 콘텐츠 키가 일회성이기 때문입니다. 그것은 봉투별 고유 enc.nonce를 솔트로 삼는 새로운 CEK로부터 파생되므로, 두 스트림이 (key, nonce) 쌍을 공유하는 일은 결코 없으며, 상태를 갖지 않는 생성자 — 브라우저 탭, CLI 실행, 워커, 재시도 — 도 봉투를 가로질러 논스를 조율할 필요가 없습니다. 88비트 카운터는 2^88개의 청크를 허용하는데, 이는 실현 가능한 어떤 페이로드보다도 훨씬 높으므로 이 포맷은 암호학적 페이로드 상한을 부과하지 않습니다. 현실적인 최댓값은 와이어 상수가 아니라 배포 차원의 서비스 거부(DoS) 정책입니다.

평문 입력은 정확히 원래의 콘텐츠 바이트입니다. 이 구성은 파일명, MIME 타입, 크기 필드, 매니페스트를 앞이나 뒤에 덧붙이거나 암호화하지 않습니다. 스트림은 그 바이트로, 오직 그 바이트로만 복호화됩니다.

해시 재확인 전까지 방출된 청크는 잠정적

분할 포맷이 존재하는 이유는, 검증자가 한정된 메모리로 수 GiB 규모의 페이로드를 증분적으로 인증하고 방출할 수 있도록 하기 위함입니다. 각 청크의 평문이 방출되기 전에 그 청크의 태그가 검증되며, 절단은 최종 플래그로 포착됩니다 — 그러나 평문 해시 재확인은 마지막 청크 이후에 전체 평문에 대해 수행됩니다. 따라서 스트리밍 소비자는 그 최종 검사가 통과될 때까지 방출된 바이트를 잠정적인 것으로 취급해야 합니다(MUST). 부작용도, 확인 응답도, "수신됨" 상태도 일절 부여해서는 안 됩니다.

게시되는 암호문은 단일 객체입니다. slots 경로에서는 그것이 정확히 STREAM 청크입니다. 패스프레이즈 경로에서는 32바이트 키 커밋먼트 헤더가 같은 블롭 내부에 앞붙습니다(같은 객체, 같은 URI, 같은 가져오기이며, 결코 두 번째 저장 객체가 아닙니다).

CBOR
slots path      : ciphertext blob = [ STREAM chunks ]
passphrase path : ciphertext blob = [ commitment: 32 bytes ] || [ STREAM chunks ]

items[].hashes의 평문 해시는 enc가 존재하는 경우에도 항상 평문에 커밋합니다. 이것이 하중을 지탱하는 속성입니다. 복호화할 수 없는 검증자도 레코드가 존재한다는 것, 봉투가 잘 구성되어 있다는 것, URI를 가져올 수 있다는 것을 여전히 확인할 수 있습니다 — 그러나 일치하는 수신자 키를 보유한 자만이 암호문을 복호화하고 해시를 재계산하여 그 커밋먼트가 무엇에 대한 것인지 확인할 수 있습니다. 따라서 검증기는 해시를 "검증"하기 위해 복호화해서는 안 됩니다(MUST NOT). 평문 해시 검증은 바이트가 복구된 이후 수신자 측에서 일어납니다. 콘텐츠와 해싱검증을 참조하십시오.

슬롯과 슬롯셋 MAC

다중 수신자 경로에서 enc.slots는 수신자별 슬롯의 비어 있지 않은 배열입니다. 모든 슬롯은 수신자별 키 암호화 키(KEK) 아래에서 동일한 CEK를 래핑합니다. 어느 슬롯이든 연 수신자는 콘텐츠를 복호화하는 유일한 CEK를 복구합니다. 송신자는 다음을 수행합니다.

  1. 레코드 전체에 사용할 KEM 하나를 선택하고 CEK(32개의 무작위 바이트)와 nonce(24개의 무작위 바이트)를 생성합니다.
  2. 수신자마다 슬롯별 KEK를 파생하고 그 아래에서 CEK를 래핑합니다(KEM별 세부 사항은 아래 참조).
  3. CSPRNG로 슬롯 배열을 셔플합니다(편향 없는 Fisher-Yates).
  4. 셔플된 배열, KEM을 가로지르는 헤더 필드, 그리고 항목의 해시 주장 위에 slots 트랜스크립트를 구축하고, 그것을 slots_hash로 해시한 뒤, slots_mac을 그 해시에 대한 CEK 키 HMAC으로 계산합니다.
  5. CEK와 enc.nonce로부터 콘텐츠 키를 파생하고, 위의 분할 STREAM 안에 콘텐츠를 봉인합니다.

슬롯별 래핑

각 슬롯은 슬롯별 KEK 아래에서 ChaCha20-Poly1305(RFC 8439, 12바이트 논스 변형)로 CEK를 래핑하여 48바이트 wrap(32바이트 CEK 암호문 + 16바이트 Poly1305 태그)을 산출합니다.

CBOR
wrap = ChaCha20-Poly1305_seal(
  key       = KEK,                    ; per-slot, 32 bytes
  nonce     = bytes(12, 0x00),        ; ZERO nonce
  ad        = <KEM info literal>,     ; the KEK info string for the chosen KEM
  plaintext = CEK)

12바이트 전부 0인 논스가 안전한 까닭은 바로 각 슬롯의 KEK가 레코드마다 고유하기 때문입니다. 즉, 하나의 KEK는 정확히 한 번의 래핑에만 사용되므로, 어떤 단일 키 아래에서도 논스가 충돌하는 일은 결코 없습니다. 이것은 엄격한 불변 원칙입니다. 만약 어떤 리비전이라도 KEK 재사용(캐싱, 결정론적 일시 키, 슬롯을 재사용하는 수신자 중복 제거)을 허용한다면, 그와 동일한 변경에서 0 논스를 무작위 논스로 교체해야 합니다.

슬롯셋 MAC

slots_mac은 슬롯 전체 집합을 — 슬롯이 해석되는 방식을 고정하는, KEM을 가로지르는 헤더 필드 및 항목의 평문 해시 주장과 함께 — CEK에 결속하여 슬롯 치환, 슬롯 제거, 슬롯 재정렬, 봉투 접합 변조를 무력화합니다. 이 결속은 2단계 구성입니다. 슬롯 트랜스크립트를 한 번 해시하여 32바이트 slots_hash를 얻고, 그 해시를 CEK 키 HMAC의 메시지로 삼습니다.

CBOR
hashes_hash = SHA-256("cardano-poe-item-hashes-v1" || canonicalEncode(item.hashes))  ; 32 bytes

SLOTS_TRANSCRIPT = {                  ; closed 7-key map; keys are a set, not an order
  "scheme":      1,                   ; uint
  "path":        "slots",             ; text
  "aead":        <enc.aead>,          ; text: the content-format identifier
  "kem":         <enc.kem>,           ; "x25519" | "mlkem768x25519"
  "nonce":       <enc.nonce>,         ; bytes(24)
  "slots":       <slots>,             ; the shuffled on-wire slot array
  "hashes_hash": hashes_hash}         ; bytes(32), over this item's hashes map

slots_hash = SHA-256("cardano-poe-slots-transcript-v1" || canonicalEncode(SLOTS_TRANSCRIPT))  ; 32 bytes
HMAC_KEY   = HKDF-SHA-256(ikm = CEK, salt = "",
                          info = "cardano-poe-slots-mac-v1", L = 32)
slots_mac  = HMAC-SHA-256(key = HMAC_KEY, msg = slots_hash)   ; 32 bytes

SLOTS_TRANSCRIPT는 정확히 그 일곱 개 키의 집합을 싣는 닫힌 맵이며, 양측이 바이트 단위로 동일한 바이트를 산출하도록 canonicalEncode로 직렬화됩니다. 그 키 순서는 RFC 8949 §4.2.1의 바이트 단위 정렬이며, 결코 손으로 배열한 것이 아닙니다. slots 값은 와이어에 나타나는 그대로의 닫힌 슬롯 맵(x25519{epk, wrap}, mlkem768x25519{kem_ct, wrap})의 셔플된 배열이므로, 모든 슬롯의 완전한 슬롯별 와이어 내용이 트랜스크립트 안에 들어갑니다. 트랜스크립트는 추가로 scheme, path, aead, kem, nonce를 고정합니다. 슬롯 모양을 유효하게 둔 채로 이 헤더 필드 중 어느 하나라도 뒤집는 릴레이는 다른 slots_hash를 산출하므로 MAC이 실패합니다. slots_hashhashes_hash의 SHA-256 접두사(cardano-poe-slots-transcript-v1, cardano-poe-item-hashes-v1)는 종결자도 길이 접두사도 없는 정확한 ASCII입니다.

hashes_hash는 봉투를 이 항목의 해시 주장에 결속하는 바로 그 부분입니다. 그것은 항목의 완전한 hashes 맵의 canonicalEncode에 대한 라벨링된 SHA-256입니다. 수신자는 온체인 바이트만으로 slots_mac을 재계산하므로, MAC 일치는 봉투가 바로 이 주장을 위해 봉인되었음을 확인해 줍니다. 다른 hashes 맵을 가진 항목에 접합된 봉투는 어떤 암호문 가져오기보다도 앞서, 온체인 일치 단계에서 실패합니다. 항목의 uris[]는 의도적으로 결속되지 않습니다. 그래서 암호문은 봉투를 무효화하지 않고 새로운 콘텐츠 주소 지정 URI에 다시 호스팅될 수 있습니다. URI 목록을 주장의 일부로 삼는 송신자는 대신 레코드 수준 서명으로 그것을 결속합니다.

HMAC_KEY 파생에서 salt = ""길이 0의 옥텟 문자열로, RFC 5869 §2.2의 솔트 부재 관례입니다(HKDF-Extract는 HashLen개의 0 바이트 — SHA-256의 경우 32개 — 를 대입합니다). 이것은 라이브러리 기본값에 맡기지 않고 바이트 단위로 정확한 적합성 벡터로 고정됩니다. 그래서 솔트 부재를 잘못 처리하는 구현은 조용히 다른 키를 파생하는 대신 그 벡터에서 실패합니다.

slots_hash는 레코드마다 한 번만 계산되며, 수신자 시험 복호화 루프 전체에 걸쳐 일정합니다. 슬롯별 MAC 검사는 후보 CEK마다 HMAC에 다시 키를 부여하지만, 항상 동일한 32바이트 slots_hash에 대해 수행합니다. HMAC 키가 여전히 HKDF-SHA-256(CEK, …)이므로 커밋먼트 속성이 보존됩니다. 트랜스크립트를 사전에 해시하는 것은 HMAC의 메시지를 완전한 트랜스크립트에서 그 SHA-256으로 바꿀 뿐이며, CEK 키 결속은 그대로 둡니다.

슬롯셋 MAC은 enc.scheme에 의해 고정됩니다. 와이어상에 그것을 위한 식별자는 없으며, scheme 값마다 정확히 하나의 구성이 존재하고, 두 KEM에 대해 동일합니다. slots_mac은 정확히 32바이트여야 하며(길이가 틀리면 ENC_SLOTS_MAC_INVALID_LENGTH), 상수 시간에 검증되어야 합니다(MUST).

트랜스크립트는 각 슬롯의 와이어 바이트에 직접 의존합니다. 두 슬롯 필드는 모두 단일 CBOR 바이트 문자열입니다(epk는 32바이트, kem_ct는 1120바이트). 따라서 정규화할 필드별 청킹도 없고 청크 경계의 모호함도 없습니다. Label 309가 수행하는 유일한 청킹은 레코드에서의 본문 전체 전송 분할이며, 이 모든 것이 실행되기 전에 이미 되돌려집니다. 슬롯 안 어디서든 한 바이트가 뒤집히면 slots_hash가 바뀌어 MAC이 실패합니다.

콘텐츠 계층은 슬롯셋에 대한 별도의 패스별 결속을 필요로 하지 않습니다. 콘텐츠 키는 CEK의 HKDF 리프이고, CEK는 이미 slots_mac에 의해 헤더 전체 — hashes_hash 포함 — 에 커밋되어 있기 때문입니다. 어떤 슬롯이나 헤더 필드를 편집하더라도 수신자가 파생하는 것이 바뀌므로 콘텐츠 스트림은 그저 열리지 않습니다. 따라서 청크별 AAD는 비어 있습니다(콘텐츠 계층 참조).

두 가지 KEM

enc.kem으로 레코드마다 선택되는 KEM은 슬롯 모양과 KEK 파생을 고정합니다. 둘 다 첫 릴리스부터 enc.scheme = 1 아래에 등록되어 있습니다.

enc.kemKEM수신자 공개 키슬롯 모양KEK info 문자열
"x25519"X25519(고전)32바이트{ epk: bstr(32), wrap: bstr(48) }"cardano-poe-kek-v1"
"mlkem768x25519"X-Wing = X25519 + ML-KEM-7681216바이트{ kem_ct: bstr(1120), wrap: bstr(48) }"cardano-poe-kek-mlkem768x25519-v1"

생성자는 기본값으로 mlkem768x25519를 사용하는 것이 좋습니다(SHOULD). 이 하이브리드 KEM은 고전 공격자와 "지금 수집하고 나중에 복호화하는(harvest-now-decrypt-later)" 양자 공격자 양쪽에 대해 안전하면서도, X25519의 고전 보안성을 하한으로 보존합니다 — X-Wing 결합기가 두 공유 비밀을 모두 결속하기 때문입니다. 그 "X25519 고전 보안성을 결코 밑돌지 않는다"는 하한은 적법하게 생성된 수신자 키에 한정됩니다. 즉, 공개 키가 (캡슐화 시 적용되는) 고정된 X-Wing 리비전의 키 유효성 검사를 통과한다는 것을 전제로 합니다(아래 하이브리드: mlkem768x25519 참조). 고전 x25519 KEM은 게시한 키가 X25519 전용인 수신자를 위해 계속 사용할 수 있습니다. 식별자 mlkem768x25519는 X-Wing/age 생태계의 표기에 맞추어 의도적으로 하이픈 없이 쓰입니다.

두 KEM은 모두 같은 age 스탠자 패턴(수신자별 KEM 자료에 파일 키의 대칭 래핑을 더한 것)과 같은 헤더 결속(슬롯셋 MAC)을 사용하므로, 하나의 통일된 구성이 HPKE 의존성 없이 둘 다를 포괄합니다. 고전 x25519 경로는 age의 네이티브 X25519 수신자를 충실히 따릅니다. 하이브리드 mlkem768x25519 경로는 age 자체의 포스트 양자 선택으로부터 의도적으로 갈라집니다. age v1.3.0은 네이티브 포스트 양자 수신자(가시 접두사 age1pq…)를 출시하는데, 이는 ML-KEM-768 + X25519 KEM 위에서 HPKE SealBase(RFC 9180)로 파일 키를 래핑하며, 스탠자 패턴이 아닙니다. 하이브리드 경로에 스탠자 래핑을 유지하는 것이 바로 하나의 통일된 래핑과 하나의 통일된 헤더 결속으로 두 KEM을 포괄할 수 있게 하는 이유입니다. 따라서 하이브리드 래핑은 age의 HPKE 구성을 계승하지 않으며, 그것에 대해 age 계승 주장을 하지 않습니다. 서로 다른 age1pqc 수신자 인코딩(자세한 내용은 참조)은 이 두 하이브리드 인코딩이 독립적임을 반영합니다.

고전: x25519

송신자는 수신자마다 새로운 일시 X25519 키쌍을 생성하고, 수신자 공개 키에 대해 ECDH를 수행한 뒤, 라벨링된 해시 솔트 아래에서 HKDF(RFC 5869)로 KEK를 파생합니다.

CBOR
shared   = X25519(priv_epk, pub_R)               ; per RFC 7748; reject all-zero output
kek_salt = SHA-256("cardano-poe-x25519-kek-salt-v1" || enc.nonce || pub_epk || pub_R)  ; 32 bytes
KEK      = HKDF-SHA-256(ikm = shared,
                        salt = kek_salt,          ; binds nonce, ephemeral, recipient
                        info = "cardano-poe-kek-v1",
                        L = 32)
slot     = { "epk": pub_epk, "wrap": wrap }       ; epk = 32 bytes

와이어상에 존재하는 키 자료는 32바이트 일시 공개 키 epk뿐이며, 수신자 공개 키는 결코 게시되지 않습니다. 솔트는 세 값을 결속하는 라벨링된 SHA-256입니다. pub_epk는 모든 슬롯의 KEK를 고유하게 만들고, pub_R은 그것을 특정 수신자에 결속하며(어떤 epk를 다른 수신자에게 전용하려는 시도를 무력화함), 봉투별 고유 enc.nonce는 KEK를 하나의 봉투에 닻처럼 고정합니다 — 그래서 두 봉투에 걸쳐 KEM 무작위성을 반복한 CSPRNG 고장이 일어나도 봉투 간 연결 가능성으로만 열화될 뿐, 반복된 (KEK, 0 논스) 래핑 쌍으로는 결코 이어지지 않습니다. X25519 구현은 RFC 7748 §6.1에 따라 전부 0인 공유 비밀을 거부해야 합니다(MUST). 주류 라이브러리는 이를 전이적으로 수행합니다.

하이브리드: mlkem768x25519(X-Wing)

이 하이브리드 KEM은 X-Wing 구성(draft-connolly-cfrg-xwing-kem-10)이며, ML-KEM-768(FIPS 203)과 X25519를 결합합니다. 각 캡슐화는 새로운 ML-KEM 무작위성과 새로운 X25519 일시 키를 뽑아, 1120바이트 암호문과 32바이트 결합 공유 비밀을 산출합니다. KEK 파생은 슬롯 자체의 와이어 바이트에 대해 계산되는 외부 솔트를 통해 수신자를 결속합니다.

CBOR
enc      = XWing.Encapsulate(pub_R)    ; named fields — MUST NOT consume positional order
kem_ct   = enc.ct                      ; 1120 bytes
shared   = enc.ss                      ; 32 bytes
kek_salt = SHA-256("cardano-poe-xwing-kek-salt-v1" || enc.nonce || kem_ct || pub_R)  ; 32 bytes
KEK      = HKDF-SHA-256(ikm = shared,
                        salt = kek_salt,          ; binds nonce, kem_ct, recipient
                        info = "cardano-poe-kek-mlkem768x25519-v1",
                        L = 32)
wrap     = ChaCha20-Poly1305_seal(key = KEK, nonce = zeros(12),
                                  ad = "cardano-poe-kek-mlkem768x25519-v1", plaintext = CEK)
slot     = { "kem_ct": kem_ct, "wrap": wrap }     ; kem_ct = single 1120-byte byte string

X-Wing의 키와 암호문 크기:

구성 요소크기구성
공개 키1216바이트ML-KEM-768 ek (1184) ‖ X25519 pk (32)
암호문1120바이트ML-KEM-768 ct (1088) ‖ X25519 ephemeral (32)
공유 비밀32바이트X-Wing 결합기 출력
디캡슐화 키32바이트시드. 공개 키는 이로부터 파생됩니다.

하이브리드 슬롯에는 epk 필드가 없습니다 — X25519 일시 키는 1120바이트 kem_ct의 뒤쪽 32바이트입니다. XWing.Encapsulate는 고정된 X-Wing 리비전의 공개 키 유효성 검사를 pub_R에 적용해야 하며(MUST), 무효한 키에 캡슐화하는 대신 그것을 거부해야 합니다. 이것이 하이브리드 하한이 X25519 고전 보안성 아래로 결코 떨어지지 않도록 하는 전제 조건입니다. 이 구성은 이름 붙은 필드만 사용하는 어댑터를 통해 X-Wing을 소비합니다. 즉, Encapsulate(pk).ct(1120 B)와 .ss(32 B)를 산출하고, Decapsulate(sk, ct)는 32바이트 공유 비밀을 산출합니다. 구현은 고정된 리비전의 API에 이름으로 매핑해야 하며(MUST), 위치 기반 반환값을 소비해서는 안 됩니다(MUST NOT). 고정된 리비전은 캡슐화에서 (ss, ct)를 반환하고 디캡슐화를 Decapsulate(ct, sk)로 기술하는데, 이는 왼쪽에서 오른쪽으로 읽는 순진한 독해와 정반대입니다. KEK 파생은 고정 길이 라벨링된 솔트 SHA-256("cardano-poe-xwing-kek-salt-v1" || enc.nonce || kem_ct || pub_R)을 통해 수신자를 결속하며, 여기서 kem_ct는 슬롯에 실린 그대로의 1120바이트 암호문이고 pub_R은 1216바이트 X-Wing 수신자 공개 키입니다. 이것은 고전 솔트가 자신의 라벨 아래에서 사용하는 것과 동일한 세 값 형태입니다 — kem_ct가 KEK를 슬롯별 고유 값에 닻처럼 고정하고, pub_R이 그것을 특정 수신자에 결속하며, enc.nonce가 그것을 하나의 봉투에 닻처럼 고정합니다 — 다만 하이브리드 입력이 원시 솔트에는 너무 크므로 SHA-256 다이제스트를 통해 표현합니다. 솔트 모두에서 pub_R 항은 수신자 키의 정규 와이어 인코딩입니다. x25519에서는 정확히 32바이트 x25519_publicKey(priv_R), mlkem768x25519에서는 정확히 고정된 1216바이트 X-Wing 공개 키 바이트 문자열입니다. 생성자와 검증자는 이 정확한 인코딩을 사용해야 하며(MUST), 어떤 비정규 또는 재인코딩된 등가물로도 대체해서는 안 됩니다(MUST NOT). 그렇지 않으면 양측이 서로 다른 KEK를 파생하여 정직한 레코드가 열리지 않습니다. 결정적으로, 이 결속은 KEM 바깥에서 슬롯 자체의 와이어 바이트에 대해 계산되므로, 이 구성은 X-Wing을 블랙박스 KEM으로 다룹니다. 즉, 공개 KEM 인터페이스(캡슐화, 디캡슐화, 32바이트 공유 비밀)만 소비하며 결합기 내부 해싱에 대해서는 아무런 가정도 하지 않습니다. KEM별로 다른 info 라벨 cardano-poe-kek-mlkem768x25519-v1은 더 나아가, 동일한 32바이트 공유 비밀 위에서라도 한 KEM을 위해 파생된 KEK가 다른 KEM을 위해 파생된 것과 결코 같을 수 없음을 보장합니다. 1120바이트 암호문은 slot.kem_ct 안에 단일 CBOR 바이트 문자열로 실립니다 — 전송을 위해 청킹되는 것은 레코드 본문 전체뿐이며(레코드 참조), 개별 필드는 결코 아닙니다.

레코드당 하나의 KEM

단일 봉인된 PoE 항목은 enc.kem정확히 하나 싣습니다. 모든 슬롯은 그 KEM의 모양과 KEK 파생을 사용합니다. 파일은 전부 고전이거나 전부 하이브리드입니다 — 서로 다른 KEM의 슬롯이 같은 slots 배열에 나타나서는 안 되며(MUST NOT), 검증자는 슬롯 모양이 선언된 enc.kem과 모순되는 레코드를 거부해야 합니다(MUST, ENC_SLOT_INVALID_SHAPE).

캡슐화 자료는 또한 하나의 slots 배열 안에서 서로 달라야 합니다. x25519의 경우 모든 epk 값이, mlkem768x25519의 경우 모든 kem_ct 값이 서로 달라야 합니다(MUST). 중복은 어떤 KEM이나 AEAD 프리미티브가 실행되기 전에 ENC_SLOTS_DUPLICATE_KEM_MATERIAL로 거부됩니다. 이것은 0 논스 래핑이 의존하는 슬롯별 KEK 고유성 불변 원칙 가운데 검증 가능한 부분입니다. 레코드를 가로지르거나 키를 가로지르는 KEK 재사용은 검증자가 탐지할 수 없는 생성자의 의무이지만, 레코드 내부의 중복은 구조적으로 보이므로 반드시 실패해야 합니다.

수신자 시험 복호화

수신자는 개인 키를 보유합니다(x25519의 경우 32바이트 X25519 스칼라, mlkem768x25519의 경우 32바이트 X-Wing 디캡슐화 시드 — 둘 다 시드에서 파생됩니다. 참조). 어느 슬롯이(있다면) 자신의 것인지 미리 알지 못하므로, 배열을 시험 복호화합니다. 두 가지 속성이 이 루프를 형성합니다. 슬롯셋 MAC 검사가 안으로 접혀 있다는 것(슬롯은 그 후보 CEK가 와이어상의 slots_mac도 재현하는 경우에만 받아들여집니다), 그리고 루프가 조기 중단 없이 모든 슬롯에 대해 실행되며 일치를 상수 시간에 선택하여 타이밍 관찰자가 어느 슬롯 인덱스가 일치했는지 추론할 수 없다는 것입니다.

어떤 KEM이나 AEAD 프리미티브를 호출하기 전에, 검증자는 구조 모양 검사(분할 오라클 방어)를 실행해야 합니다(MUST). scheme == 1, aead/kem이 등록됨, nonce가 24바이트, slots_mac이 32바이트, slots이 비어 있지 않음, 수신자 비밀이 32바이트, 각 slot.wrap이 정확히 48바이트, 각 x25519 epk가 정확히 32바이트이며 kem_ct가 없음, 각 mlkem768x25519 kem_ct가 정확히 1120바이트이며 epk가 없음, 그리고 모든 캡슐화 자료가 slots 안에서 서로 다름(그렇지 않으면 ENC_SLOTS_DUPLICATE_KEM_MATERIAL)을 검사합니다.

같은 프리미티브 이전 단계에서 검증자는 파서의 자원 사용에도 경계를 둬야 합니다(MUST). 참조 경계는 MAX_SLOTS = 1024 슬롯과 디코딩된 enc 봉투에 대한 65536 바이트입니다. 둘 다 정직한 레코드를 제약하는 약 16 KiB의 Cardano 트랜잭션 메타데이터 천장보다 훨씬 높으므로, 둘 중 하나를 초과하는 레코드는 잘못된 형식이며 여기서 거부됩니다 — 슬롯이 너무 많으면 ENC_SLOTS_TOO_MANY, 봉투가 너무 크면 ENC_ENVELOPE_TOO_LARGE — 어떤 KEM이나 AEAD 프리미티브가 실행되기 전에 거부됩니다. 이 경계는 검증자가 강제하는 배포별 고정 상수이며 와이어 필드가 아닙니다. 배포 측에서 더 빠듯하게 해도 됩니다(MAY).

; hashes_hash, SLOTS_TRANSCRIPT and slots_hash are recomputed once, before the loop, and held constant:
hashes_hash = SHA-256("cardano-poe-item-hashes-v1" || canonicalEncode(item.hashes))
slots_hash  = SHA-256("cardano-poe-slots-transcript-v1" || canonicalEncode(SLOTS_TRANSCRIPT))
if kem == "x25519": pub_R = x25519_publicKey(priv_R)   ; recipient public key, 32 B
else:               pub_R = XWing.publicKey(priv_R)     ; recipient X-Wing public key, 1216 B

found        = false
cek_conflict = false
selected_CEK = zeros(32)
for slot in enc.slots:                  ; iterate ALL slots — no early break
    kem_ok = true
    if kem == "x25519":
        shared    = x25519(priv_R, slot.epk)
        kem_ok    = NOT constant_time_eq(shared, zeros(32))     ; explicit all-zero reject, secret-independent
        kek_salt  = SHA-256("cardano-poe-x25519-kek-salt-v1" || enc.nonce || slot.epk || pub_R)
        real_KEK  = HKDF-SHA-256(shared,    salt = kek_salt, info = "cardano-poe-kek-v1", L = 32)
        dummy_KEK = HKDF-SHA-256(zeros(32), salt = kek_salt, info = "cardano-poe-kek-v1", L = 32)
        KEK       = ct_select(kem_ok, real_KEK, dummy_KEK)      ; constant-time, no early exit
        ad_wrap   = "cardano-poe-kek-v1"
    else:                               ; mlkem768x25519
        shared   = XWing.Decapsulate(sk=priv_R, ct=slot.kem_ct)   ; pinned API writes Decapsulate(ct, sk)
        kek_salt = SHA-256("cardano-poe-xwing-kek-salt-v1" || enc.nonce || slot.kem_ct || pub_R)
        KEK      = HKDF-SHA-256(shared, salt = kek_salt,
                               info = "cardano-poe-kek-mlkem768x25519-v1", L = 32)
        ad_wrap  = "cardano-poe-kek-mlkem768x25519-v1"

    open_ok, candidate_CEK = ChaCha20-Poly1305_open_or_dummy(KEK, zeros(12), ad_wrap, slot.wrap)
    HMAC_KEY = HKDF-SHA-256(candidate_CEK, salt = "", info = "cardano-poe-slots-mac-v1", L = 32)
    mac_ok   = constant_time_eq(HMAC-SHA-256(HMAC_KEY, slots_hash), enc.slots_mac)
    ok       = kem_ok AND open_ok AND mac_ok                    ; kem_ok folded into acceptance
    first        = ok AND NOT found                             ; first matching slot
    cek_conflict = cek_conflict OR (ok AND found AND NOT constant_time_eq(candidate_CEK, selected_CEK))
    selected_CEK = ct_select(first, candidate_CEK, selected_CEK)   ; constant-time
    found        = found OR ok

if NOT found:    reject (single generic failure)   ; WRONG_RECIPIENT_KEY / TAMPERED_HEADER
if cek_conflict: reject (single generic failure)   ; cek_conflict

content_key = HKDF-SHA-256(selected_CEK, salt = enc.nonce, info = "cardano-poe-payload-v1", L = 32)
plaintext   = STREAM_open(content_key, ciphertext)   ; per-chunk authenticated release
if STREAM_open fails at any chunk: reject (single generic failure)   ; TAMPERED_CIPHERTEXT

KEK 파생은 enc.kem에 따라 분기합니다. x25519의 경우 수신자는 slot.epk에 대해 ECDH를 수행하고 enc.nonce || slot.epk || pub_R에 대해 동일한 라벨링된 솔트를 다시 파생합니다. mlkem768x25519의 경우 slot.kem_ct(단일 1120바이트 바이트 문자열)를 직접 X-Wing 디캡슐화하고 enc.nonce || slot.kem_ct || pub_R에 대해 동일한 라벨링된 솔트를 다시 계산하는데, 여기서 pub_R은 보유한 시드에서 파생한 자신의 1216바이트 X-Wing 공개 키입니다. X25519의 전부 0인 공유 비밀 거부는 전이적으로 의존하는 대신 여기서 명시적입니다. 공유 비밀을 zeros(32)로 몰아가도록 조작된 슬롯(RFC 7748 §6.1)은 비밀과 무관한 유효성 비트 kem_ok을 거짓으로 설정하고, KEK는 동일한 솔트와 info 아래에서 zeros(32)로부터 파생된 dummy_KEK로 상수 시간에 선택되어 루프가 동일한 작업을 수행하며, kem_okok에 접혀 듭니다 — 그래서 무효 ECDH 슬롯은 래핑이나 MAC 결과와 무관하게 결코 받아들여지지 않으며, 다른 어떤 것도 일치하지 않으면 레코드는 유일한 일반 실패를 표면화합니다. 래핑이 열린 이후의 모든 것 — 슬롯셋 MAC 검사, 콘텐츠 키 파생, 콘텐츠 복호화 — 은 KEM에 무관합니다.

*_open_or_dummy AEAD 프리미티브는 모두 원자적입니다. 태그 검증 실패 시 평문을 전혀 반환하지 않으며, 반환되는 후보(래핑 열기의 경우 candidate_CEK, 콘텐츠 열기의 경우 plaintext)는 실패한 암호문과 무관한 고정 또는 의사난수 더미입니다. 검증되지 않은 평문이 호출자에게 방출되는 일은 결코 없으므로, 실패한 열기가 복호화 오라클이 될 수 없습니다.

MAC 검사가 루프 안에 있는 이유

악의적인 송신자는 수신자의 키로는 열리지만 공격자가 선택한 CEK를 산출하는 슬롯을 만들 수 있습니다(수신자 공개 키로 캡슐화하는 데에는 개인 키가 필요 없습니다). 수신자가 첫 번째 AEAD 성공을 "자신의 것"으로 받아들인다면, 그 위조된 슬롯이 배열 뒤쪽의 정직한 슬롯을 가려 버립니다. slots_mac 검사를 루프 안에 접어 넣으면, 슬롯은 그 후보 CEK가 slots_hash에 대한 MAC을 재현하는 경우에만 받아들여집니다 — 그래서 위조된 슬롯은 건너뛰어지고 스캔이 계속됩니다. slot.wrap 길이는 어떤 AEAD 호출보다도 앞서 48바이트인지 검사되어야 하며(MUST), 이는 age v1도 적용하는 분할 오라클 방어입니다.

여러 슬롯이 일치하는 경우: 중복은 허용되고, CEK 충돌은 허용되지 않습니다. 수신자의 개인 키가 둘 이상의 슬롯에 적법하게 일치할 수 있습니다(MAY). 생성자는 같은 CEK를 같은 수신자에게 여러 슬롯에 걸쳐 — 각각 자신의 새로운 슬롯별 일시 키를 지닌 채 — 봉인하여, 겉보기 수신자 수를 부풀릴 수 있습니다. 이는 정당한 프라이버시 기법입니다. 검증자는 첫 번째 일치의 CEK를 선택하며, 단지 둘 이상의 슬롯이 일치했다는 이유만으로 거부해서는 안 됩니다(MUST NOT). 이것은 레코드 내부의 중복 캡슐화 자료 거부(ENC_SLOTS_DUPLICATE_KEM_MATERIAL, 반복된 epk 또는 kem_ct에서 발화)와는 별개입니다. 정직한 중복은 출현할 때마다 새로운 슬롯별 KEM 무작위성을 뽑으므로 그 epk / kem_ct가 서로 달라 그 검사와 결코 충돌하지 않습니다. 검증자가 반드시 거부해야 하는(MUST) 유일한 이상 현상은 서로 다른 CEK를 복구하는 두 일치 슬롯입니다(상수 시간에 비교). 루프는 cek_conflict 비트를 모든 슬롯에 걸쳐 운반하며, 이후의 어떤 일치가 선택된 CEK와 다른 CEK를 복구하면 유일한 일반 실패를 표면화합니다. 이것은 심층 방어입니다 — 복구된 CEK가 공급하는 커밋먼트 속성(슬롯셋 MAC이 CEK를 단일 슬롯 트랜스크립트에 결속함. 익명성과 KEM별 분기 참조) 아래에서는, 서로 다른 CEK를 내는 일치는 이미 실현 불가능하며, 그것이 바로 커밋먼트가 배제하는 다중 키 충돌이기 때문입니다. 그래서 이 검사는 결함 있는 구현이나 그 가정의 미래의 약화에 대해 안전하게 닫히는 방향으로 실패합니다(fail closed).

단일 일반 실패 형태, 슬롯을 가로질러 상수 시간

신뢰할 수 없는 호출자는 복호화가 실패한 이유와 무관하게 정확히 하나의 일반 실패 형태를 받아야 합니다(MUST). 어느 슬롯도 열리지 않았든, 슬롯셋이 변조되었든, 콘텐츠 AEAD가 실패했든 마찬가지입니다. 응답은 이들을 구별해서는 안 되며(MUST NOT), 어느 슬롯이 일치했는지도 드러내서는 안 됩니다. 구현은 내부의 타입 지정 코드 — WRONG_RECIPIENT_KEY(어느 슬롯도 열리지 않음), TAMPERED_HEADER(슬롯은 열리지만 어떤 후보 CEK도 slots_hash에 대한 slots_mac을 재현하지 못함), TAMPERED_CIPHERTEXT(CEK가 복구된 뒤 콘텐츠 AEAD가 실패) — 를 진단을 위해 신뢰할 수 있는 로컬 호출자에게 표면화해도 됩니다(MAY). 그러나 그 코드는 구별 가능한 응답을 통해 외부 관찰자에게 새어 나가서는 안 됩니다(MUST NOT).

타이밍에 관해서는, 검증자가 무일치 검사(if NOT found)에서 콘텐츠 복호화 이전에 반환해도 됩니다(MAY). 그 조기 반환이 드러내는 것은 수신자 대 비수신자뿐이며, 어느 슬롯이 일치했는지도 어떤 키 자료도 결코 드러내지 않습니다. 검사에 도달한 시점에 위의 슬롯 횡단 루프는 이미 끝까지 실행되었기 때문입니다. 비수신자 경우와 암호문이 열리지 않는 수신자 사이의 균일한 타이밍은 요구되지 않으며(NOT), 더미 콘텐츠 열기를 의무화해서는 안 됩니다(MUST NOT). 모든 비수신자에게 콘텐츠 복호화 전체 비용을 강요해도, 루프가 이미 제공하는 것 이상의 프라이버시는 사지 못하기 때문입니다. 실제로 성립하는 상수 시간 보장은 슬롯 횡단 불변 원칙입니다 — 루프는 개인 키당 일정한 수의 슬롯 연산을 조기 중단 없이 처리하므로, 네트워크 수준의 관찰자가 알 수 있는 것은 슬롯 개수뿐이며 그 키가(있다면) 어느 슬롯을 풀었는지는 결코 알지 못합니다. 여러 키를 보유한 수신자(예컨대 신원 회전을 가로질러 보관된 키)는 개인 키 × 슬롯을 순회하며, 키마다 솔트의 pub_R 절반을 다시 파생합니다. 키 사이에서는 단락해도 되지만(MAY, 약한 "어느 키가 일치했는가" 신호만 누설됨) 어느 한 키의 슬롯 사이에서는 상수 시간을 유지해야 합니다(MUST).

평문을 복구한 후, 수신자는 — 복호화 함수 안이 아니라 애플리케이션 계층에서 — 평문 해시를 재계산하여 items[].hashes와 대조합니다. 불일치는 레코드의 온체인 커밋먼트가 복호화된 바이트와 일치하지 않음을 의미하며, 수신자는 그 평문에 따라 행동하기를 거부해야 합니다(MUST). 이 단계가 일련의 검증을 완결합니다. 체인은 시각 T에 어떤 커밋먼트가 있었음을 증언했고, 수신자는 그것이 바로 이 바이트에 대한 커밋먼트임을 확인합니다.

패스프레이즈 경로

대안적인 키 전달 경로는 수신자 슬롯을 패스프레이즈로 대체합니다. slots 배열도, slots_mac도, 슬롯별 일시 키도, 시험 복호화 루프도 없습니다. CEK는 Argon2id(RFC 9106)로 온체인 솔트와 파라미터 위에서 정규화된 패스프레이즈로부터 직접 파생됩니다. slots 경로에서 slots_mac이 제공하는 키 커밋먼트는, 여기서는 대신 암호문 블롭 내부에 있는 32바이트 헤더에 놓여 STREAM 청크 앞에 앞붙습니다 — 같은 객체, 같은 URI, 같은 가져오기입니다.

CBOR
passphrase_bytes = utf8(normalize(passphrase))   ; cardano-poe-pw-norm-v1 (see below)
CEK              = argon2id(passphrase_bytes,
                           salt   = enc.passphrase.salt,    ; 16–64 bytes, on chain
                           params = enc.passphrase.params,  ; { m, t, p }, on chain
                           L      = 32)

hashes_hash = SHA-256("cardano-poe-item-hashes-v1" || canonicalEncode(item.hashes))

PASSPHRASE_TRANSCRIPT = {              ; closed 6-key map; keys are a set, not an order
  "scheme":      1,                    ; uint
  "path":        "passphrase",         ; text
  "aead":        <enc.aead>,           ; text: the content-format identifier
  "nonce":       <enc.nonce>,          ; bytes(24)
  "hashes_hash": hashes_hash,          ; bytes(32), over this item's hashes
  "passphrase": {                      ; closed sub-map
    "alg":           "argon2id",       ; text
    "salt":          enc.passphrase.salt,         ; bytes
    "params":        { "m": m, "t": t, "p": p },  ; closed map of uints
    "normalization": "cardano-poe-pw-norm-v1"}}   ; scheme-fixed constant, NOT on the wire

pw_hash     = SHA-256("cardano-poe-passphrase-transcript-v1" || canonicalEncode(PASSPHRASE_TRANSCRIPT))
PW_MAC_KEY  = HKDF-SHA-256(ikm = CEK, salt = "", info = "cardano-poe-passphrase-mac-v1", L = 32)
commitment  = HMAC-SHA-256(key = PW_MAC_KEY, msg = pw_hash)   ; 32 bytes

content_key = HKDF-SHA-256(ikm = CEK, salt = enc.nonce,
                           info = "cardano-poe-payload-passphrase-v1", L = 32)
ciphertext blob = commitment || STREAM chunks                 ; STREAM under content_key, as on the slots path

와이어상의 enc.passphrase 블록은 { alg, salt, params }이며 — KDF의 이름("argon2id"), 솔트, 그리고 파라미터를 명시합니다. Label 309는 파라미터 하한을 고정합니다. m ≥ 65536 KiB(64 MiB), t ≥ 3, p ≥ 1입니다. 생성자는 하한 이상의 값을 선택하며, 솔트는 16~64바이트 포함입니다(64바이트 상한은 메타데이텀 바이트 문자열 상한입니다). 플랫폼이 지원하는 곳에서 생성자는 p = 4(RFC 9106 §4의 두 번째 권장 프로파일)를 사용하는 것이 좋습니다(SHOULD). 검증자는 아래의 배포 천장을 조건으로 임의의 p ≥ 1을 받아들여도 됩니다(MAY).

PASSPHRASE_TRANSCRIPT는 KDF 파라미터, 헤더 필드, 그리고 항목의 해시 주장을 커밋먼트에 결속합니다. 검증자는 받은 enc 맵과 항목의 hashes로부터 트랜스크립트를 재계산하므로, salt, 임의의 params 값, nonce, aead를 변조하거나 봉투를 다른 해시 주장에 접합하면 다른 pw_hash가 산출되어 커밋먼트 검사가 실패합니다. 그런 다음 콘텐츠는 slots 경로와 동일한 분할 STREAM 안에, 패스프레이즈 경로 콘텐츠 키 아래에서 봉인됩니다. "normalization" 값은 CEK가 파생된 정확한 프로파일을 고정하기 위해 트랜스크립트에 투입되는 scheme 고정 상수이며, 와이어상에 결코 직렬화되지 않습니다.

검증 순서. 검증자는 입력된 패스프레이즈로부터 후보 CEK를 파생하고, 암호문 블롭의 선두 32바이트를 읽어 커밋먼트를 재계산한 뒤 상수 시간에 — 어떤 STREAM 청크도 열기 전에 비교합니다. 48바이트 — 32바이트 커밋먼트 헤더에 16바이트 최소 STREAM을 더한 것 — 보다 짧은 패스프레이즈 경로 블롭은 잘 구성될 수 없으며 잘못된 형식의 암호문입니다(TAMPERED_CIPHERTEXT). 불일치 시 — 틀린 패스프레이즈, 변조된 salt / params, 변조된 헤더, 또는 접합된 봉투 — 검증자는 다른 어떤 복호화 실패와도 동일한 유일한 일반 실패를 표면화하며, 스트리밍을 시작해서는 안 됩니다(MUST NOT). 따라서 틀린 패스프레이즈는 변조된 레코드와 구별할 수 없습니다.

정규화와 Argon2id 이전에, 구현은 과대한 패스프레이즈가 KDF 이전 서비스 거부를 유발할 수 없도록 원시 패스프레이즈 입력 길이에 경계를 둬야 합니다(MUST). 참조 경계는 원시 입력 4096 UTF-8 바이트이며, 어떤 정규화나 해싱 작업보다도 앞서 거부됩니다. slots 경로가 강제하는 MAX_SLOTS 및 디코딩된 enc 봉투 경계와 마찬가지로, 이것은 검증자가 강제하는 배포별 고정 상수이며 — 와이어 필드가 아닙니다 — 배포 측에서 더 빠듯하게 해도 됩니다(MAY). 파라미터 하한 외에도, 구현은 검증자 측 DoS에 대비하여 m, t, p상한도 강제하는 것이 좋습니다(SHOULD). 그 천장은 비규범적이며(하드웨어 의존적) 하한과 혼동해서는 안 됩니다(MUST NOT).

커밋먼트가 오프체인에 있는 이유

온체인 패스프레이즈 커밋먼트는 모든 관찰자에게 공짜 오프라인 시험 오라클을 — 추측한 패스프레이즈로부터 후보 CEK를 파생하여 체인에 대조하는 방식으로 — 모든 패스프레이즈 레코드에 대해, 영원히, 암호문이 보류된 레코드까지 포함하여 넘겨주게 됩니다. 커밋먼트를 암호문 블롭 안에 싣는다는 것은 추측을 시험하는 데 블롭 자체가 필요함을 의미합니다. 암호문이 보류된 레코드는 영구 원장에 패스프레이즈 추측 가능한 자료를 전혀 노출하지 않으며, 이미 블롭을 보유한 적법한 수신자는 32바이트 헤더를 먼저 읽는 데 아무런 비용도 들지 않습니다.

정규화 프로파일

Argon2id 이전에 패스프레이즈에 적용되는 정규화는 고정된 프로파일 cardano-poe-pw-norm-v1입니다. 이것은 규범적입니다. 두 구현은 같은 패스프레이즈로부터 바이트 단위로 동일한 CEK를 파생해야 하며(MUST), 그것을 보장하는 유일한 방법은 고정된 정규화입니다. 이 프로파일은 순서대로 적용하며 다음과 같습니다.

  1. 할당되지 않은 코드포인트 거부. Unicode 16.0에서 할당되지 않은 코드포인트를 하나라도 포함하는 패스프레이즈는 어떤 정규화 단계가 실행되기 전에 ENC_PASSPHRASE_UNNORMALIZABLE로 거부됩니다.
  2. NFKC. UAX #15에 따라 Unicode 16.0 아래에서 Normalization Form KC를 적용합니다.
  3. 공백. "공백"을 Unicode 16.0 아래에서 Unicode White_Space 속성을 지닌 모든 문자로 정의하고, 그러한 문자의 모든 최대 연속 구간을 단일 U+0020 SPACE로 접습니다.
  4. 트림. 선두와 말미의 공백을 제거합니다.
  5. 빈 문자열 거부. 결과가 빈 문자열이면 ENC_PASSPHRASE_EMPTY로 거부합니다. 공백만으로 이루어졌거나 그 밖의 의미로 공허한 패스프레이즈는 0바이트로 정규화되는데, Argon2id는 그것을 조용히 받아들여 — 어느 당사자든 파생할 수 있는 CEK에 레코드의 키를 부여하게 됩니다.
  6. 인코딩. 결과를 UTF-8로 인코딩합니다. 그 바이트가 Argon2id 패스워드 입력입니다.

1단계가 바로 이 프로파일을 구현 간에, 그리고 시간을 가로질러 결정론적으로 만드는 것입니다. Unicode 정규화 안정성 정책은, 문자열의 모든 코드포인트가 그것이 정규화된 버전에서 할당된 경우에만 그 문자열의 정규화가 미래의 Unicode 버전을 가로질러 안정적임을 보장합니다. 할당되지 않은 코드포인트는 나중에 분해형을 획득하여 파생되는 CEK를 조용히 바꿀 수 있습니다. 할당되지 않은 코드포인트를 거부하면 이 구멍이 완전히 막히며, 정직한 사용자에게는 보이지 않습니다 — 실제로 쓰이는 모든 문자는 할당되어 있습니다.

Unicode 버전은 글자 그대로 Unicode 16.0에 고정되며 변동해서는 안 됩니다(MUST NOT). White_Space 속성 집합, 할당된 코드포인트 집합, NFKC 매핑 테이블은 모두 버전 의존적이며, 다른 Unicode 버전에 대해 이 프로파일을 해석하는 검증자는 같은 패스프레이즈로부터 다른 CEK를 파생하여 정직한 레코드를 복호화하지 못할 수 있습니다. 더 새로운 Unicode 버전을 채택하는 미래의 리비전은 cardano-poe-pw-norm-v1을 재해석하는 것이 아니라 새로운 프로파일 식별자 아래에서 그렇게 합니다.

패스프레이즈 엔트로피가 유일한 장벽

솔트와 Argon2id 파라미터는 체인에 영원히 공개되므로, 공격자는 그것들에 대해 패스프레이즈를 무차별 대입할 무한한 오프라인 시간을 갖습니다. 패스프레이즈 엔트로피가 이 경로의 유일한 보안 여유입니다. 생성자는 사람이 고른 것이 아니라 CSPRNG로 생성된 diceware 패스프레이즈를 사용하는 것이 좋으며(SHOULD), 입력된 패스프레이즈를 받아들일 때는 온체인 암호문이 영구히 오프라인 공격의 대상이 됨을 알리는 눈에 띄는 경고를 표시하는 것이 좋습니다(SHOULD).

순방향 비밀성과 슬롯별 독립성

슬롯 구성은 슬롯마다 새로운 일시 키로 일시-정적 ECDH(또는 새로운 X-Wing 캡슐화)를 사용하며, 이는 정적-정적 또는 공유 일시 키 설계라면 잃었을 두 가지 속성을 사들입니다.

  • 송신자 침해에 대한 순방향 비밀성. 송신자는 이 구성에서 장기 키를 보유하지 않으며, 일시 키는 봉인 후 0으로 지워집니다. 나중에 송신자 상태를 침해하더라도 침해 이전에 게시된 레코드는 복호화할 수 없습니다.
  • 슬롯별 독립성. 서로 다른 수신자는 서로 다른 일시 키를 받으므로 서로 다른 공유 비밀과 KEK를 얻습니다. 한 수신자가 자신의 래핑된 CEK를 누설하면 CEK가 드러나지만(불가피합니다 — 그것이 파일 키입니다) 다른 수신자의 KEK는 결코 드러나지 않습니다.

봉인된 PoE에는 설계상 수신자 순방향 비밀성이 없습니다. 레코드가 일단 장기 수신자 키 앞으로 봉인되면, 일치하는 개인 키를 보유한 자는 그것을 영원히 복호화할 수 있습니다. 이는 장기 키에 대한 공개 키 암호화의 속성이지 결함이 아닙니다.

익명성과 KEM별 분기

봉인된 PoE 레코드가 sigs를 싣지 않을 때, 그 와이어 바이트는 송신자의 신원과 무관합니다. 각 슬롯은 레코드별, 슬롯별 일시 KEM 자료(slot.epk의 X25519 일시 키, 또는 slot.kem_ct의 X-Wing 암호문)만 싣고, 송신자의 장기 키는 결코 나타나지 않으며, 슬롯은 CSPRNG로 셔플되고, 와이어상에 수신자 공개 키가 없으며, 어떤 서술적 필드(파일명, MIME 타입, 크기)도 없습니다. 따라서 서명되지 않은 봉인된 레코드는 체인상에 송신자 신원을 전혀 결속하지 않습니다 — 이는 내부고발 제보, 봉인 입찰 경매, 증거 에스크로가 요구하는 바로 그 특성입니다.

KEM 모두에 대해 정직한 누설은 동일하고 불가피합니다. 슬롯 개수, 봉인됨 대 봉인되지 않음 구분, 그리고 고전 대 하이브리드 KEM 계열(enc.kem)이 어떤 관찰자에게도 보이며, 수신자에 대해 그 이상은 아무것도 보이지 않습니다.

더 강한 주장 — 후보 수신자 공개 키 집합을 보유한 적대자가 주어진 슬롯이 그중 하나 앞으로 지정되었는지 시험할 수 없다는 것(키 프라이버시 / 수신자 익명성) — 은 KEM별 속성입니다.

  • x25519 — 키 프라이빗. 슬롯별 캡슐화는 수신자 키와 통계적으로 독립인 새로운 일시 공개 키입니다. 후보 수신자 공개 키를 보유한 적대자는 slot.epkslot.wrap만으로는 일치하는 개인 키 없이 그 슬롯이 어느 후보를(있다면) 겨냥하는지 판정할 수 없습니다. 따라서 고전 경로는 키 프라이빗이며, 이는 레코드 간 연결 불가능성도 부여합니다. 같은 수신자 앞으로 보낸 두 봉인된 PoE는 서로 무관한 epk/wrap 블롭처럼 보입니다.
  • mlkem768x25519 — 주장하지 않음. 후보 수신자 키를 보유한 적대자에 대한 수신자 익명성은 하이브리드 KEM의 IND-CCA 보안성에 의해 함의되지 않는 별개의 속성입니다. Label 309는 그것이 X-Wing에 대해 독립적으로 정당화되지 않는 한 X-Wing 경로에 대해 그것을 주장하지 않습니다. 위협 모델이 키를 보유한 적대자에 대한 수신자 익명성을 요구하는 배포는 그 속성을 위해 하이브리드 경로에 의존해서는 안 됩니다(MUST NOT).

레코드 간 타이밍 상관관계를 우려하는 송신자는 게시를 임계 타임라인에서 분리하여 일괄 처리해야 합니다(MUST). 와이어 수준의 암호는 메타데이터 타이밍 공격을 해결할 수 없습니다.

슬롯셋 MAC이 커밋먼트이며, 래핑은 그럴 필요가 없습니다

복구된 CEK는 수신자가 일치시킨 슬롯셋에 대한 커밋먼트입니다. 악의적인 송신자는 단일 수신자가 자신의 것으로 받아들이는 서로 다른 두 슬롯셋을 구성할 수 없습니다. 여기서 요구되는 속성은 RFC 9771의 의미에서 봉투 CEK에 대한 제한된 키 커밋먼트입니다 — 복구된 CEK가 단일 슬롯 트랜스크립트에 결속됩니다 — 임의의 입력에 대한 완전한 커밋 AEAD가 아닙니다. 그것은 적대적으로 선택된 CEK와 트랜스크립트에 대해 CEK ↦ HMAC-SHA-256(HKDF-SHA-256(CEK, "cardano-poe-slots-mac-v1"), slots_hash)의 다중 키 충돌 저항성에 기댑니다 — 약 128비트의 일반 충돌 여유(256비트 출력에 대한 생일 한계)이며, 이 위협 모델에는 충분합니다. 트랜스크립트 자체의 위변조 탐지성은 SHA-256의 약 2^128 충돌 한계를 물려받습니다. 커밋된 헤더 필드나 슬롯 바이트의 어떤 변경도 slots_hash를 바꾸며, 다른 트랜스크립트 위에서 변하지 않은 slots_hash를 위조하는 것은 바로 그 약 2^128 충돌 탐색입니다. 커밋먼트가 slots_mac에 의해 공급되므로, 슬롯별 wrap AEAD는 커밋 AEAD일 필요가 없습니다. 기본의 비커밋 ChaCha20-Poly1305가 여기서는 건전합니다.

금지된 패턴

적합한 구현은 다음을 해서는 안 됩니다(MUST NOT).

  • 슬롯이나 레코드를 가로질러 슬롯별 일시 키를 재사용하거나, 그 밖의 방식으로 KEK가 반복되게 함 — 0 논스 래핑은 슬롯별 KEK 고유성에 의존합니다.
  • 봉투를 가로질러 CEK를 재사용enc를 싣는 항목마다 새로운 CSPRNG CEK를, 레코드 내부에서나 레코드를 가로질러서나 마찬가지로 사용합니다.
  • 패스프레이즈 솔트를 재사용 — 모든 패스프레이즈 봉투마다 새로운 CSPRNG enc.passphrase.salt를 생성합니다. 재사용되는 패스프레이즈에 대해 솔트는 유일한 레코드 간 분리자입니다.
  • 하나의 slots 배열 안에서 KEM을 혼용(레코드당 하나의 enc.kem).
  • 슬롯을 입력 순서로 게시 — CSPRNG 셔플이 필수입니다.
  • 12바이트 0 논스 이외의 어떤 논스로, 또는 빈 래핑 AEAD AAD로 CEK를 래핑 — 래핑 AAD는 KEM의 info 라벨 그 자체입니다.
  • 수신자 공개 키를 와이어에 올림 — 시험 복호화 설계가 곧 프라이버시 기능이며, 공개 키를 게시하면 그것을 무력화합니다.
  • slots_mac 검증을 건너뜀 — 그것이 없으면 슬롯 치환이 성공합니다.
  • ar:///ipfs:// URI에 평문을 저장 — 암호문만 게시되며, 평문은 대역 외로 전달되거나 송신자가 보유합니다.
  • ar:// 또는 ipfs:// 이외의 어떤 스킴으로 암호문을 참조 — 콘텐츠 주소 지정 스킴은 URI를 바이트에 결속합니다. 호스트가 제공하는 URL은 봉인된 PoE가 싣지 않는 별도의 온체인 암호문 커밋먼트를 요구할 것입니다.
  • CEK, 임의의 KEK, 슬롯셋 HMAC 키, 패스프레이즈 MAC 키, 콘텐츠 키, ECDH 공유 비밀, 일시 개인 키, 또는 수신자 개인 키를 로그하거나 영속화.

관련 페이지

  • — 수신자 및 송신자 키 자료를 공급하는, 시드에서 파생된 X25519 및 X-Wing 키쌍.
  • 레코드enc가 레코드 맵에서 차지하는 위치, 그리고 레코드를 체인에 싣는 본문 전체 전송.
  • 알고리즘 레지스트리enc.aead, enc.kem, 그리고 패스프레이즈 KDF 식별자와 그것을 뒷받침하는 프리미티브.
  • 콘텐츠와 해싱 — 모든 봉인된 레코드가 싣는 평문 해시 커밋먼트.
  • 검증 — 검증 파이프라인, 검증기가 결코 복호화하지 않는 이유, 그리고 오류 목록.