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

구현자 가이드

적합한 Label 309 구현을 구축하는 방법 — 권장되는 계층형 아키텍처, 언어를 넘나들며 바이트 단위로 동일성을 보장하는 계약, 그리고 상호운용성을 정의하는 적합성 테스트 벡터.

Label 309는 와이어 포맷이자 일련의 암호학적 구성이며, 제품이 아닙니다. TypeScript, Python, Rust, Go, 또는 네이티브 모바일 런타임으로 작성된 여러 독립 구현이 공존할 수 있으며, 한 구현이 생성한 레코드는 다른 구현에서도 반드시(MUST) 검증되어야 합니다. 이 페이지는 그러한 구현을 구축하는 팀을 위한 것입니다. 암호학적 표면을 감사 가능한 상태로 유지하는 아키텍처, 두 구현을 상호운용 가능하게 만드는 정확한 계약, 그리고 그 계약을 충족했는지를 기계적으로 판정하는 적합성 스위트에 대해 설명합니다.

Label 309가 언어를 넘나들며 상호운용 가능한 이유는 두 가지입니다. 첫 번째는 결정성입니다. 각 구성은 공개 표준에 고정되어 있어 (RFC 8949 canonical CBOR, RFC 8032 Ed25519, RFC 7748 X25519, RFC 5869 HKDF, RFC 9106 Argon2id, RFC 9052 COSE), 동일한 입력은 어디에서나 동일한 바이트를 산출합니다. 두 번째는 적합성 스위트입니다. 구현이 재현하거나 재현하지 못하거나 둘 중 하나인, 바이트 단위로 정확한 테스트 벡터의 집합입니다. 적합성이란 검증할 수 있는 속성이지, 단지 주장하기만 하는 것이 아닙니다.

계층형 아키텍처

적합한 구현은 암호학적 프리미티브와 애플리케이션 로직을 별개의 계층으로 분리하고, 각 계층이 바로 그 아래 계층에만 의존하도록 해야 합니다(SHOULD). 아래에 나오는 이름은 패키지 이름이 아니라 역할을 가리킵니다. 이름은 자유롭게 정하십시오.

┌─────────────────────────────────────────────────────────┐
│  application                                            │
│  UI, routing, persistence, payments, background jobs    │
├─────────────────────────────────────────────────────────┤
│  SDK                                                    │
│  service client + standalone verifier + helpers         │
├─────────────────────────────────────────────────────────┤
│  wire-format library                                    │
│  schema · structural validator · canonical-CBOR codec   │
├─────────────────────────────────────────────────────────┤
│  cryptographic core                                     │
│  hashes · KDFs · signatures · KEM · AEAD · CBOR · COSE  │
│  no application or framework dependencies               │
└─────────────────────────────────────────────────────────┘

이 경계는 장식이 아니라 구조상 핵심을 떠받치는 것입니다. 각 계층에는 단일한 책무가 있으며, 알아서는 안 되는 사항들이 짧은 목록으로 명확히 정해져 있습니다.

암호학적 코어

최하위 계층은 프리미티브만을 보유합니다. 해시 함수, KDF, 서명 및 KEM 연산, AEAD 콘텐츠 계층, canonical CBOR, COSE_Sign1, 봉인된 PoE의 래핑/언래핑 구성, Merkle 루트 및 증명, 그리고 이들이 발생시키는 타입이 지정된 오류 클래스입니다. 도메인 로직, HTTP, 데이터베이스 접근, UI나 서버 프레임워크 import는 일절 포함하지 않습니다.

이 계층은 애플리케이션 또는 서버에 묶이는 그 무엇과도 의존성이 없어야 하며 (MUST), 또한 반드시(MUST) 브라우저에서 안전하게 동작해야 합니다. 구체적인 이유는 세 가지입니다.

  • 어디에서나 동작합니다. 파일의 해시 계산, 봉투의 구축, 그리고 무엇보다도 결정적으로 독립 검증자(standalone verifier)는 모두 브라우저에서, 서버리스 워커에서, 명령줄에서, 서버에서와 다름없이 손쉽게 실행됩니다. 서버 전용 의존성(데이터베이스 드라이버, 런타임에 묶인 로깅 프레임워크, UI 라이브러리)이 있으면 이러한 동작 대상이 깨지고, 코어를 번들로 묶는 모든 소비처가 비대해집니다.
  • 그 자체가 감사 표면입니다. 검토자는 프리미티브만으로 이루어진 패키지를 RFC와 대조하며 처음부터 끝까지 읽어낼 수 있습니다. 애플리케이션 코드가 스며드는 순간, 보안 검토자가 머릿속에 담아 두어야 할 표면은 끝없이 넓어집니다.
  • 제3자가 임베드하는 것이 바로 이 계층입니다. 어떤 서비스도 신뢰하지 않고 오직 체인만 신뢰하는 독립 검증자는, 이 계층만 끌어다 쓰고 그 위의 어떤 것도 끌어오지 않습니다. 작고 이식성 높은 상태로 유지하는 것이야말로 "직접 검증한다"를 현실적으로 만드는 핵심입니다.

구체적으로, 코어는 ORM이나 데이터베이스 드라이버, UI 프레임워크, 서버에 묶인 로깅 프레임워크, 또는 어떤 애플리케이션 모듈도 import 해서는 안 됩니다(MUST NOT). 난수는 플랫폼 CSPRNG(Web Crypto의 getRandomValues, 또는 동등한 재내보내기)에서 반드시(MUST) 가져와야 하며, Node 전용 소스에서 가져와서는 결코 안 됩니다. 동일한 소스가 브라우저에서도 변경 없이 동작하도록 하기 위함입니다.

경계는 코드 리뷰가 아니라 CI에서 강제하십시오

무의존성 규칙은 편의를 위한 import가 하나 끼어드는 순간 무너지기 시작합니다. 구현은 코어와 와이어 포맷 라이브러리 안의 모든 import를 따라가며, 계층별 허용 목록에 없는 지정자가 발견되면 빌드를 실패시키는 의존성 그래프 lint를 실행해야 합니다(SHOULD). 검토자는 잊지만, linter는 잊지 않습니다.

와이어 포맷 라이브러리

그 한 단계 위 계층은 Label 309 자체를 담당합니다. 레코드 스키마, 구조 검증기, 그리고 canonical CBOR의 인코더와 디코더입니다. 이 계층은 암호학적 코어(해시 계산, COSE, CBOR 코덱을 위해)에만 의존하고, 그 밖에 애플리케이션에 묶인 어떤 것에도 의존하지 않습니다. 그 표면은 작고 순수합니다.

  • encode — 검증된 레코드로부터 canonical CBOR 바이트를 산출합니다.
  • decode — 그 역연산입니다.
  • validate — 디코딩된 레코드에 대해 표준이 정한 구조적·의미적 검사를 실행하고 타입이 지정된 결과를 반환합니다(검증 참조).

레코드의 규칙이 코드로 구현되는 곳이 바로 이 계층입니다. 닫힌 키 집합, 청크 재조립의 규율, items-또는-merkle 불변 원칙, canonical CBOR 요건이 여기에 자리합니다. 코어와 마찬가지로, 이 계층도 HTTP 클라이언트, 데이터베이스 드라이버, 프레임워크 import가 없는 상태를 유지합니다.

SDK와 애플리케이션

SDK는 하위 계층들을 다루기 편한 헬퍼로 감쌉니다. 서비스 클라이언트, 봉투 구축/잠금해제 헬퍼, 그리고 독립 검증자(standalone verifier) 입니다. 이 검증자는 레코드를 디코딩하고, 그 구조를 확인하며, 온체인 키와 대조하여 레코드 서명을 검증하고, 오직 공개 데이터만으로 판정을 내리는 함수입니다. 독립 검증자는 구현자가 운영하는 어떤 서비스에도 네트워크 접근 없이 반드시(MUST) 동작해야 합니다. 그 유일한 외부 입력은 검증자가 직접 선택한 공개 블록체인 익스플로러뿐입니다. SDK 또한 브라우저에서 안전하게 동작하는 상태를 유지해야 합니다(SHOULD).

애플리케이션 계층(UI, 라우팅, 영속화, 과금, 백그라운드 작업)은 그린필드이며 어떤 상호운용 의무도 지지 않습니다. 표준은 그것을 어떻게 구축하는지에 대해 아무런 제약도 두지 않으며, 다만 그것이 이미 검증된 암호학적 표면 속으로 손을 뻗는 것이 아니라 그 위에 자리할 것만을 요구합니다.

바이트 단위로 동일성을 보장하는 계약

상호운용성은 바이트의 속성이지 의도의 속성이 아닙니다. 두 구현이 상호운용 가능한 것은, 출력에 어떤 자유도도 없는 프리미티브가 동일한 입력으로부터 동일한 바이트를 산출하는 경우, 그리고 오직 그 경우뿐입니다. 이것이 패리티 계약이며, 적합성의 핵심입니다.

이 계약은 깔끔하게 둘로 나뉩니다. 출력이 입력에 의해 완전히 결정되는 연산은 구현을 넘나들며 바이트 단위로 동일해야 합니다(MUST). 난수를 소비하는 연산은 호출마다 바이트가 같을 수 없습니다. 그러한 연산에 대한 계약은 상호 소비 가능성입니다. 한 구현이 산출한 값은 다른 어떤 구현에서도 반드시(MUST) 소비 가능해야 합니다(한 언어에서 봉인된 암호문이 다른 언어에서 복호화되는 것입니다).

바이트 단위로 동일한 프리미티브

아래의 모든 연산은 그 입력에 대한 순수 함수이며, 적합한 모든 구현에서 반드시(MUST) 바이트 단위로 동일한 출력을 산출해야 합니다.

프리미티브고정 기준일치해야 하는 출력
시드 → Ed25519 / X25519 키 쌍등록된 info 상수를 사용한 HKDF-SHA-256도출된 공개 키와 개인 키
HKDF-SHA-256RFC 5869고정 입력에 대한 출력 키 자료
HMAC-SHA-256 슬롯 집합 MACRFC 2104고정 CEK와 슬롯 집합에 대한 slots_hashslots_mac 태그 바이트
Argon2id (패스프레이즈 KDF)RFC 9106고정 (m, t, p, salt, len, password)에 대한 도출 키
SHA-256FIPS 180-4다이제스트
BLAKE2b-256RFC 7693다이제스트
canonical CBOR 인코딩RFC 8949 §4.2.1고정 입력에 대한 인코딩 바이트
COSE_Sign1 인코딩RFC 9052고정 헤더, 페이로드, 서명에 대한 구조 바이트
Ed25519 서명 / 검증RFC 8032 (엄격 모드)서명, 판정 결과
X25519 ECDHRFC 7748고정 스칼라에 대한 공유 비밀
봉인된 PoE 래핑 / 언래핑봉인된 PoEephemeral과 CEK를 주입했을 때의 슬롯별 바이트 및 MAC
Merkle 루트 + 포함 증명RFC 9162 §2.1.1순서가 있는 리프 목록에 대한 루트 및 리프별 증명

특히 강조할 점이 두 가지 있습니다. Ed25519는 엄격합니다. 적합한 검증자는 RFC 8032 §5.1.7의 canonical-S 규칙과 저차원 점 배제 규칙을 반드시(MUST) 적용해야 합니다. 이로써 두 구현은 받아들이는 서명뿐 아니라 거부하는 서명에 대해서도 일치합니다. Argon2id는 생태계 경계를 넘나듭니다. 언어가 다르면 손에 잡는 Argon2 라이브러리도 다르지만, 적합한 라이브러리는 모두 RFC 9106을 구현하며, 동일한 파라미터에 대해 동일한 출력을 반드시(MUST) 산출해야 합니다. 계약은 라이브러리가 아니라 파라미터 집합입니다.

난수를 소비하는 연산

키 생성, 슬롯마다 새로운 ephemeral 아래에서 수행하는 봉인된 PoE 래핑, 그리고 봉투 암호화는 모두 새로운 난수를 끌어들이므로 호출마다 출력이 달라지며 바이트 단위로 고정될 수 없습니다. 이들에 대한 계약은 상호 소비 가능성입니다. 한 구현이 산출한 출력은 다른 모든 구현에서 반드시(MUST) 소비 가능해야 합니다. 한 언어에서 봉인된 레코드는 다른 언어에서 반드시(MUST) 복호화되어야 하며, 한 구현에서 주조된 키 쌍은 다른 구현에서 반드시(MUST) 검증되고 암호화 대상으로 사용될 수 있어야 합니다. 적합성 스위트는 ephemeral을 주입하여 래핑을 재현 가능하게 만드는 결정적 테스트 훅과, 한 언어에서 암호화하여 다른 언어에서 복호화하는 왕복 픽스처로 이들을 고정합니다.

봉인된 PoE 구성을 조립하기

봉인된 PoE는 와이어 포맷에서 가장 밀도가 높은 부분이며, 단 한 바이트의 오류 — 순서가 어긋난 맵 키, 한 글자 어긋난 라벨, 비정규 청크화 — 가 자신의 구현에서는 열리지만 다른 어떤 구현에서도 열리지 않는 봉투를 만들어 내는 바로 그 부분입니다. 이 절은 조립 체크리스트입니다. 정확한 레시피, 각 AEAD가 덮는 추가 인증 데이터, 시험 복호화 루프, 그리고 모든 생산자와 검증자가 반드시 강제해야 하는 가드를 제시합니다. 봉인된 PoE의 구성 레퍼런스가 산문이라면, 여기는 패리티 게이트가 초록으로 바뀌도록 배선하는 방법입니다. 다음 외부 초안들은 재현해야 할 바이트를 그 내부에서 고정하므로, 정확하게 고정하십시오.

  • chacha20-poly1305-stream64k — 콘텐츠 포맷 — 은 age v1 사양의 64 KiB 분할 STREAM 레이아웃을 적용한 ChaCha20-Poly1305(RFC 8439)입니다. 청크 크기(65536), 12바이트 청크별 nonce uint88_be(counter) ‖ final_flag, 빈 청크별 AAD, 그리고 final-flag 규칙을 정확하게 고정하십시오 — 이들은 재현해야 할 바이트를 고정합니다.
  • X-Wing(mlkem768x25519 KEM)은 draft-connolly-cfrg-xwing-kem-10입니다. 이를 블랙박스 KEM으로 취급하십시오. 이 구성은 수신자 공개 키와 암호문을 키 도출 단계 그 자체에 결합하므로, 컴바이너 내부 해싱의 어떤 속성에도 의존하지 않습니다. XWing.Encapsulate는 고정된 리비전의 공개 키 유효성 검사를 반드시(MUST) 적용해야 하며, 그것을 통과하지 못하는 키에 대한 캡슐화를 거부해야 합니다. "X25519의 고전적 보안을 결코 밑돌지 않는다"는 하한은 정당하게 생성된 키에 한정하여 적용되며, 이 검사를 건너뛰면 그 수신자에 대해서는 하한을 잃습니다. 적합성 KEM 벡터는 캡슐화를 draft-10에 고정하므로, 초안 리비전 불일치는 즉시 표면화됩니다.

하나의 CEK, 두 개의 키 전달 경로

봉인된 레코드는 평문을 단일 콘텐츠 암호화 키(CEK) 아래에서 단 한 번 암호화한 뒤, 그 CEK를 상호 배타적인 두 경로 중 하나로 전달하며, 그 경로는 필드의 존재 여부로 구별됩니다 — 모드 태그는 없습니다.

  • slots 경로 — CEK는 슬롯별 키 암호화 키 아래에서 각 수신자에게 독립적으로 래핑됩니다. encslots(그리고 kem, slots_mac)를 담습니다.
  • passphrase 경로 — CEK는 정규화된 패스프레이즈로부터 Argon2id를 통해 직접 도출됩니다. encpassphrase를 담으며, kem, slots, slots_mac은 담지 않습니다.

두 경로는 enc.scheme(항상 1; 그 외에는 거부), enc.aead (chacha20-poly1305-stream64k), enc.nonce(24바이트)를 공유합니다. 차이는 키 커밋먼트가 어디에 놓이는가입니다. slots 경로에서는 온체인의 slots_mac에, passphrase 경로에서는 암호문 블롭 안쪽의 32바이트 헤더에 있습니다. 두 경로 모두 아이템의 해시 주장을 각자의 트랜스크립트에 결합하고, 둘 다 콘텐츠를 동일한 분할 STREAM으로 봉인합니다. 차이는 키 전달과 커밋먼트이지 콘텐츠 계층이 아닙니다.

슬롯별 래핑 (slots 경로)

레코드 전체에 대해 하나의 KEM을 고르십시오 — 단일 slots[] 안에서 KEM을 절대 섞지 마십시오. N명의 수신자 각각에 대해 새로운 슬롯별 키 암호화 키를 도출하고, 동일한 CEK를 12바이트 전(全)-0 nonce의 ChaCha20-Poly1305로 그 아래에서 래핑하며, AAD는 그 KEM의 info 라벨 리터럴로 설정하여(빈 AAD는 절대 불가) 정확히 48바이트(32바이트 CEK 암호문 + 16바이트 태그)를 산출합니다. 전-0 nonce가 안전한 것은 오직 키 암호화 키가 슬롯별이기 때문입니다. 아래의 고유성 가드를 참조하십시오.

x25519 (고전). 슬롯마다 새로운 X25519 ephemeral 키 쌍:

priv_epk : randomBytes(32)                        ; fresh per slot
pub_epk  : x25519_publicKey(priv_epk)
shared   : x25519_sharedSecret(priv_epk, pub_R)   ; reject all-zero result
kek_salt : SHA-256("cardano-poe-x25519-kek-salt-v1" || enc.nonce || pub_epk || pub_R)   ; 32 B
KEK      : HKDF-SHA-256(ikm  = shared, salt = kek_salt,
                        info = "cardano-poe-kek-v1", L = 32)
wrap     : ChaCha20-Poly1305(key = KEK, nonce = zeros(12),
                             ad = "cardano-poe-kek-v1", plaintext = CEK)   ; 48 B
slot     : { "epk": pub_epk, "wrap": wrap }

mlkem768x25519 (하이브리드; X-Wing). 슬롯마다 새로운 X-Wing 캡슐화:

enc    = XWing.Encapsulate(pub_R)       ; named fields — MUST NOT consume positional order
kem_ct = enc.ct                         ; 1120 B
shared = enc.ss                         ; 32 B
kek_salt : SHA-256("cardano-poe-xwing-kek-salt-v1" || enc.nonce || kem_ct || pub_R)   ; 32 B
KEK      : HKDF-SHA-256(ikm  = shared, salt = kek_salt,
                        info = "cardano-poe-kek-mlkem768x25519-v1", L = 32)
wrap     : ChaCha20-Poly1305(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

두 salt 모두 하나의 형태 — SHA-256(label || enc.nonce || <slot KEM material> || pub_R) — 를 가지며, 고전 경로에서는 32바이트 ephemeral pub_epk를, 하이브리드 경로에서는 1120바이트 X-Wing 암호문 kem_ct를 운반합니다. ||는 바이트 연결이고, 각 salt 접두사 리터럴은 종단 문자도 길이 접두사도 없는 정확한 ASCII입니다. pub_R은 수신자의 정규 와이어 키입니다(x25519는 32 B, mlkem768x25519는 고정된 1216 B). 하이브리드 슬롯은 별도의 epk담지 않습니다 — X25519 ephemeral은 kem_ct의 끝 32바이트입니다 — 그리고 kem_ct정확히 1120바이트인 단일 CBOR 바이트 문자열입니다. 전송을 위해 청크화되는 것은 레코드 본문 전체뿐이며, 개별 필드는 결코 청크화되지 않습니다.

salt는 세 가지 값을 결합합니다. 슬롯의 KEM 자료(KEK를 슬롯 고유로 만듭니다), pub_R(다른 수신자를 겨냥한 혼동된 대리자 중계를 무력화합니다), 그리고 enc.nonce(KEK를 하나의 봉투에 닻으로 묶어, KEM 난수가 반복되더라도 봉투를 넘나드는 연결 가능성으로만 저하되게 합니다). 서로 다른 info 라벨은 KEM을 넘나드는 도메인 분리를 제공하여, 어떤 KEM 아래에서 도출된 KEK가 동일한 공유 비밀 위에서라도 다른 KEM 아래에서 도출된 것과 같아질 수 없게 합니다. 열한 개의 내부 라벨, 즉 cardano-poe-kek-v1, cardano-poe-kek-mlkem768x25519-v1, cardano-poe-x25519-kek-salt-v1, cardano-poe-xwing-kek-salt-v1, cardano-poe-item-hashes-v1, cardano-poe-slots-transcript-v1, cardano-poe-slots-mac-v1, cardano-poe-passphrase-transcript-v1, cardano-poe-passphrase-mac-v1, cardano-poe-payload-v1, cardano-poe-payload-passphrase-v1을 각각 바이트 단위로 그대로 사용하십시오. 그 어느 것도 와이어 상에 직렬화되지 않습니다. 이들은 고정된 상수이지, 레지스트리에서 선택 가능한 것이 아닙니다. 단 한 바이트라도 어긋나면 정직한 생산자가 재현할 수 없는 slots_mac, 커밋먼트, 또는 AEAD 태그가 됩니다.

MAC를 계산하기 전에 섞으십시오. 입력 순서("주 수신자가 먼저")는 특권적 메타데이터이며, 슬롯을 입력 순서대로 게시하면 그것이 누설됩니다. slots[]를 CSPRNG로 편향 없는 Fisher-Yates 치환을 사용해 섞으십시오 — 단순한 u32 % m 인덱스 추출은 작은 나머지로 치우치므로 균일한 인덱스로 기각 표본추출해야 합니다 — 그리고 이를 슬롯 집합 MAC을 계산하기 전에 수행하면, 그 MAC이 섞인 후의 와이어 상 순서를 결합합니다.

슬롯 집합 MAC: 트랜스크립트를 해시한 뒤 CEK로 HMAC

슬롯 집합 MAC은 슬롯 집합 전체에, 슬롯을 읽는 방식을 고정하는 헤더 필드를 더한 것을 CEK에 결합합니다. 두 단계로 구축합니다 — 닫힌 트랜스크립트를 한 번 해시하고, 그 해시를 HMAC합니다.

hashes_hash : SHA-256("cardano-poe-item-hashes-v1" || canonicalEncode(item.hashes))   ; 32 B

SLOTS_TRANSCRIPT = {                          ; closed 7-key map; keys are a set, not an order
    "scheme":      1,
    "path":        "slots",
    "aead":        <enc.aead>,                ; 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
}
slots_hash : SHA-256("cardano-poe-slots-transcript-v1" || canonicalEncode(SLOTS_TRANSCRIPT))
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 B

여기서 패리티의 성패를 가르는 것은 세 가지입니다.

  • 트랜스크립트는 canonicalEncode로 직렬화되는 닫힌 맵입니다. 그 키 순서는 RFC 8949 §4.2.1의 정렬이지, 결코 손으로 배열한 것이 아닙니다. scheme, path, aead, kem, nonce를 슬롯과 함께 고정한다는 것은, 헤더 필드를 어느 하나라도 뒤집는 중계자가 — 슬롯 형태를 유효하게 둔 채로라도 — slots_hash를 바꾸고 MAC을 깨뜨린다는 뜻입니다.
  • 트랜스크립트는 아이템의 해시 주장을 결합합니다. hashes_hash는 아이템의 완전한 hashes 맵의 canonicalEncode에 대한 라벨 붙은 SHA-256입니다. 수신자는 온체인 바이트만으로 slots_mac을 다시 계산하므로, MAC 일치는 봉투가 바로 이 정확한 해시 주장을 위해 봉인되었음을 확인합니다 — 다른 hashes 맵을 가진 아이템에 이어 붙인 봉투는 어떤 암호문을 가져오기 이전에, 온체인 일치 단계에서 실패합니다. slots 값은 와이어 상 슬롯 맵의 섞인 배열을 그대로 사용합니다. 각 슬롯 필드는 단일 바이트 문자열(epk 32 B, kem_ct 1120 B)이므로, 정규화해야 할 필드별 청크화는 없습니다.
  • slots_hash는 한 번만 계산되어 시험 복호화 루프 전체에 걸쳐 일정하게 유지됩니다. 슬롯별 MAC 검사는 각 후보 CEK로 HMAC의 키를 다시 잡지만, 항상 동일한 32바이트 slots_hash에 대해 수행합니다. 사전 해싱은 CEK로 키가 잡힌 커밋먼트를 그대로 보존합니다. HMAC 메시지를 완전한 트랜스크립트에서 그 SHA-256으로 바꾸는 것뿐, 그 이상은 아무것도 하지 않습니다.

MAC 알고리즘, 그 키 도출, 그리고 트랜스크립트 스키마는 모두 enc.scheme = 1에 의해 고정되며 두 KEM에 대해 동일합니다. 와이어 상 MAC 식별자는 없습니다. slots_mac은 정확히 32바이트이며 상수 시간으로 검증됩니다.

콘텐츠 암호화: 분할 STREAM

평문을, CEK로부터 도출된 콘텐츠 키 아래에서 분할 STREAM으로 단 한 번 암호화합니다. 콘텐츠 키는 CEK의 별개 HKDF 리프로서 — enc.nonce로 salt를 가하고 경로별 info 아래에서 도출되어 — 래핑 계층과 콘텐츠 계층이 동일한 바이트로 동일한 프리미티브의 키를 잡는 일이 결코 없게 합니다.

content_key : HKDF-SHA-256(ikm = CEK, salt = enc.nonce, info = "cardano-poe-payload-v1", L = 32)

; STREAM (chacha20-poly1305-stream64k):
CHUNK_SIZE  : 65536 plaintext bytes per non-final chunk
chunk nonce : uint88_be(counter) || final_flag   ; 12 B; counter from 0, +1 per chunk;
                                                 ; final_flag = 0x01 on the last chunk, else 0x00
per-chunk AAD : empty
ciphertext  : seal(chunk_0) || seal(chunk_1) || ... || seal(chunk_final)
              ; each chunk sealed with ChaCha20-Poly1305 under content_key; sealed = plaintext + 16 B

청크별 AAD는 비어 있습니다 — 그리고 이것은 누락이 아니라 올바른 설계입니다. 콘텐츠 키는 CEK로부터 도출되며, CEK는 이미 slots_mac에 의해 전체 헤더(hashes_hash 포함)에 커밋되어 있습니다. 헤더 필드를 어느 하나라도 뒤집으면 수신자는 다른 콘텐츠 키를 도출하므로 스트림이 열리지 않습니다. 청크별 AAD는 보안을 더하지 않은 채 동일한 컨텍스트를 청크마다 다시 결합할 뿐입니다. 카운터 nonce가 안전한 것은 콘텐츠 키가 일회용(봉투마다 고유한 enc.nonce로 salt를 가한 새로운 CEK)이기 때문이며, 따라서 두 스트림이 (key, nonce) 쌍을 공유하는 일은 없습니다.

절단이 검출 가능하도록 STREAM을 구축하십시오. 모든 비-최종 청크는 정확히 65536바이트 평문이고, 최종 청크는 final_flag = 0x01과 0–65536바이트를 운반하며(빈 평문은 길이가 0인 최종 청크 하나 — 단독 16바이트 태그), 검증자는 최종 플래그 누락, 비-최종 청크에 붙은 최종 플래그, 최종 청크 뒤의 데이터, 또는 짧은 비-최종 청크에 대해 반드시 실패해야 합니다(MUST, TAMPERED_CIPHERTEXT). 각 청크의 태그를 그 평문을 방출하기 전에 검증하고, 복호화 후 해시 재검사가 통과할 때까지 방출된 바이트를 잠정적인 것으로 취급하십시오.

평문은 정확히 원본 콘텐츠 바이트 그 자체입니다. 이 구성은 파일명, MIME 타입, 크기 필드, 메타데이터 래퍼를 앞이나 뒤에 덧붙이거나 암호화하지 않습니다. 게시되는 암호문 블롭은 STREAM 청크입니다(passphrase 경로에서는 아래의 32바이트 커밋먼트 헤더가 앞에 붙습니다). 조립된 enc 맵과 결과 URI는 체인에 올라가지만 암호문 바이트는 올라가지 않습니다 — 그것을 콘텐츠 주소화 저장소에 게시하고, 그 ar:// 또는 ipfs:// URI를 아이템의 uris[]에 넣으십시오.

passphrase 경로

수신자가 없을 때는, 정규화된 패스프레이즈로부터 Argon2id로 CEK를 도출합니다. epk도, 슬롯별 래핑도, 슬롯 집합 MAC도, 시험 복호화 루프도 없습니다. slots 경로에서 slots_mac이 제공하는 키 커밋먼트는, 대신 암호문 블롭 안쪽의 32바이트 헤더에 놓이며, STREAM 청크 앞에 붙습니다.

passphrase_bytes = utf8(normalize(passphrase))   ; cardano-poe-pw-norm-v1
CEK = argon2id(passphrase_bytes, salt = enc.passphrase.salt,
               params = enc.passphrase.params, 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,
    "path":        "passphrase",
    "aead":        <enc.aead>,
    "nonce":       <enc.nonce>,               ; bytes(24)
    "hashes_hash": hashes_hash,               ; bytes(32), over this item's hashes
    "passphrase": {                           ; closed sub-map
        "alg":           "argon2id",
        "salt":          enc.passphrase.salt,
        "params":        { "m": m, "t": t, "p": p },
        "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 B

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

PASSPHRASE_TRANSCRIPT는 KDF 파라미터, 헤더 필드, 그리고 아이템의 해시 주장을 커밋먼트에 결합합니다. salt, 임의의 params 값, nonce, aead를 변조하거나, 봉투를 다른 해시 주장에 이어 붙이면 다른 pw_hash가 생기고 커밋먼트 검사가 실패합니다. "normalization" 값은, CEK가 어떤 정규화 프로파일 아래에서 도출되었는지를 정확히 고정하기 위해 트랜스크립트에 공급되는 스킴 고정 상수이며, 와이어 상에는 결코 직렬화되지 않습니다(생산자는 { alg, salt, params }만 발신합니다).

검증 측에서는 후보 CEK를 도출하고, 암호문 블롭의 선행 32바이트를 읽어, 커밋먼트를 다시 계산한 뒤, 어떤 STREAM 청크를 열기 이전에 상수 시간으로 비교합니다. 48바이트(32바이트 커밋먼트 + 16바이트 최소 STREAM)보다 짧은 블롭은 비정형입니다(TAMPERED_CIPHERTEXT). 불일치 시 — 잘못된 패스프레이즈, 변조된 salt / params / 헤더, 또는 이어 붙인 봉투 — 동일한 단일 일반 실패를 표면화하고 스트리밍을 시작하지 마십시오. 잘못된 패스프레이즈는 변조된 레코드와 구별되지 않습니다. 커밋먼트는 의도적으로 오프체인입니다. 온체인 커밋먼트는 암호문이 보류된 것을 포함한 모든 패스프레이즈 레코드에 대해, 무료 오프라인 추측 오라클이 되어 버리기 때문입니다.

파라미터 하한을 강제하십시오. salt 길이는 16–64바이트, m ≥ 65536 KiB(≈ 64 MiB), t ≥ 3, p ≥ 1입니다. Argon2 버전을 0x13(19)으로 고정하십시오. enc.scheme: 1 아래에서는 다른 버전이 허용되지 않으며, 와이어 상에 버전 필드는 없습니다. 플랫폼이 지원한다면, 생산자는 p = 4(RFC 9106 §4의 두 번째 권장 프로파일)를 발신해야 합니다(SHOULD). 검증자는 배포 상한에 따르되 임의의 p ≥ 1을 받아들여도 됩니다(MAY). Argon2id는 생태계 경계를 깔끔하게 넘나듭니다 — 계약은 라이브러리가 아니라 파라미터 집합입니다 — 따라서 고정된 (m, t, p, salt, len, password)는 모든 구현에서 바이트 단위로 동일한 출력을 산출해야 합니다. 패스프레이즈와 그 봉투 사이의 결합은 위의 암호문 내 커밋먼트입니다. 잘못된 패스프레이즈와 변조된 암호문은 둘 다 하나의 일반 실패로 표면화됩니다.

정규화와 Argon2id 이전에 원시 패스프레이즈를 제한하십시오. 참조 상한 MAX_PASSPHRASE_INPUT_BYTES = 4096 UTF-8 바이트보다 긴 입력은 모두 거부하여, 병적인 패스프레이즈가 KDF 이전 서비스 거부를 유발하지 못하게 하십시오. slots 경로의 MAX_SLOTS 및 디코딩된 봉투 상한과 마찬가지로, 이것은 배포 단계에서 고정된 상수이며 더 빡빡하게 조여도 되지만(MAY) 와이어 필드는 아닙니다.

정규화 프로파일은 규범적이다

두 구현은 동일한 패스프레이즈로부터 바이트 단위로 동일한 CEK를 반드시(MUST) 도출해야 하며, 그것을 보장하는 유일한 방법은 고정된 정규화입니다. 프로파일 cardano-poe-pw-norm-v1을 순서대로 적용합니다.

  1. 미할당 코드포인트 거부Unicode 16.0에서 미할당인 코드포인트를 하나라도 포함하는 패스프레이즈는, 어떤 정규화가 실행되기 이전에 거부됩니다(ENC_PASSPHRASE_UNNORMALIZABLE). Unicode는 할당된 코드포인트에 한해서만 정규화 안정성을 보장하므로, 이는 미래 표류 구멍을 막으며 정직한 사용자에게는 보이지 않습니다.
  2. NFKCUnicode 16.0 아래에서 UAX #15에 따른 Normalization Form KC.
  3. 공백 — 공백을 Unicode 16.0 아래에서 Unicode White_Space 속성을 지닌 모든 문자로 정의하고, 그러한 최대 연속을 단일 U+0020 SPACE로 접습니다.
  4. 트림 — 선행 및 후행 공백을 제거합니다.
  5. 빈 문자열 거부 — 결과가 빈 문자열이면 거부합니다(ENC_PASSPHRASE_EMPTY). 그러지 않으면 공백만으로 이루어진 패스프레이즈가 레코드를, 누구나 도출할 수 있는 CEK에 묶어 버립니다.
  6. 인코딩 — UTF-8. 그 바이트가 Argon2id의 비밀번호 입력입니다.

Unicode를 16.0으로 문자 그대로 고정하고 떠다니게 두지 마십시오. White_Space 속성 집합, 할당된 코드포인트 집합, NFKC 매핑 테이블은 모두 버전 의존적이므로, 프로파일을 다른 Unicode 버전에 대해 해석하면 동일한 패스프레이즈로부터 다른 CEK를 도출하여 정직한 레코드를 열지 못하게 될 수 있습니다. 더 새로운 Unicode 버전을 채택하는 미래의 리비전은, cardano-poe-pw-norm-v1을 재해석하는 것이 아니라 새로운 프로파일 식별자 아래에서 그렇게 합니다.

시험 복호화: 모든 슬롯을 열고, MAC을 접어 넣고, 일반적으로 실패하기

수신자는 하나의 KEM 개인 키를 보유하고, 각 슬롯을 열어 보며 자신의 슬롯을 발견합니다 — 수신자 공개 키는 와이어 상에 없습니다. 어떤 KEM이나 AEAD 프리미티브를 호출하기 전에, 먼저 자원 한계를 실행하고, 이어서 구조 가드를 실행합니다. 먼저 파서의 자원 사용을 제한하십시오. 디코딩된 크기가 65536바이트를 초과하는 봉투는 거부하고(ENC_ENVELOPE_TOO_LARGE), slots[]MAX_SLOTS = 1024를 초과하는 봉투는 거부합니다(ENC_SLOTS_TOO_MANY). 두 참조 한계 모두 정직한 레코드를 제약하는 약 16 KiB의 Cardano 트랜잭션 메타데이터 천장보다 한참 위에 있습니다. 이들은 배포 단계에서 고정된 상수로, 더 빡빡하게 조여도 되지만(MAY) 결코 와이어 필드가 아닙니다. 이어서 구조 가드입니다. scheme == 1; aead, kem이 등록되어 있음; nonce가 24바이트; slots_mac이 32바이트; slots가 비어 있지 않음; 수신자 비밀이 32바이트; 각 wrap이 48바이트; KEM별로, 각 epk가 정확히 32바이트이며 kem_ct가 없음(x25519), 또는 각 kem_ct가 정확히 1120바이트이며 epk가 없음(mlkem768x25519).

어떤 프리미티브 이전에, 여기서 레코드 내부의 중복 캡슐화를 거부하십시오. 고전 경로에서는 모든 epk 값이, 하이브리드 경로에서는 모든 kem_ct 값이 서로 달라야 합니다. 중복은 ENC_SLOTS_DUPLICATE_KEM_MATERIAL을 발생시킵니다. 이는 전-0 nonce 래핑이 의존하는 슬롯별 KEK 고유성 불변 원칙 가운데 검증자가 확인할 수 있는 조각입니다. 레코드를 넘나들거나 키를 넘나드는 재사용은 어떤 검증자도 검출할 수 없는 생산자의 의무입니다. 이 거부는 오직 반복된 epk / kem_ct에 대해서만 발화됩니다 — 새로운 슬롯별 ephemeral을 사용해 같은 수신자에게 두 번 봉인하는 것은 정당하며 이를 건드리지 않습니다(아래의 복수 일치 규칙 참조). unwrap-negative가 중복 epk와 KEK 재사용 사례를 담고 있습니다.

그런 다음 루프를 실행하되, 그 이전에 slots_hash를 한 번 다시 계산하여 일정하게 유지합니다.

found        = false
cek_conflict = false
selected_CEK = 0^32
for slot in slots:                            ; iterate ALL slots — no early break
    ; derive KEK per-KEM, as in the wrap recipe. For x25519 the all-zero shared
    ; secret is rejected via a secret-independent bit, not an early branch:
    ;   kem_ok = NOT constantTimeEqual(shared, 0^32)
    ;   KEK    = ct_select(kem_ok, real_KEK, dummy_KEK)   ; dummy_KEK from ikm=0^32, same salt/info
    ; (XWing.Decapsulate has no all-zero case; kem_ok stays true on the hybrid path.)
    open_ok, candidate_CEK = ChaCha20-Poly1305_open_or_dummy(KEK, zeros(12), kem_info_label, slot.wrap)
    HMAC_KEY = HKDF-SHA-256(candidate_CEK, salt = "", info = "cardano-poe-slots-mac-v1", L = 32)
    mac_ok   = constantTimeEqual(HMAC-SHA-256(HMAC_KEY, slots_hash), 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 constantTimeEqual(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)
if cek_conflict: reject (single generic failure)
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

이 루프에서 양보할 수 없는 점은 다음과 같습니다.

  • 원자적으로 여십시오. 검증되지 않은 평문을 결코 방출하지 마십시오.*_open_or_dummy 프리미티브는 원자적입니다. AEAD 태그 실패 시에는 평문을 반환하지 않으며, 반환되는 후보(래핑된 CEK, 또는 콘텐츠 평문)는 실패한 암호문과 무관한 고정값 또는 의사난수 더미입니다. 바로 이것이 루프로 하여금, 인증되지 않은 바이트를 일절 노출하지 않으면서, 실패한 래핑 열기를 넘어 candidate_CEK를 이어 나르게 합니다.
  • 전-0 검사를 비밀에 의존하지 않는 kem_ok 비트로 접어 넣으십시오. x25519 경로에 대해 kem_ok = NOT constantTimeEqual(shared, 0^32)을 계산하고, 진짜 KEK와 동일한 salt 및 info 아래에서 0^32로부터 도출된 더미 KEK 사이에서 KEK를 상수 시간으로 선택한 뒤, kem_ok를 수용에 접어 넣으십시오(ok = kem_ok AND open_ok AND mac_ok). 무효한 공유에서 일찍 분기해 빠져나가지 마십시오 — 무효한 ECDH 슬롯은 결코 수용될 수 없으며, 루프는 여전히 동일한 작업을 수행합니다. (XWing.Decapsulate는 전-0 사례가 없으므로, 하이브리드 경로에서 kem_ok는 참으로 고정됩니다.)
  • slots_mac 검사를 루프에 접어 넣으십시오. 악의적 발신자는 (개인 키를 알지 못해도) 수신자의 키로 열리면서 공격자가 선택한 CEK를 산출하는 슬롯을 조작할 수 있습니다. 첫 AEAD 성공을 "우리 것"으로 받아들이면 그 위조 슬롯이 정직한 슬롯을 가려 버립니다. 후보 CEK가 slots_hash에 대해 slots_mac까지 재현하도록 요구하면 슬롯 치환, 슬롯 제거, 슬롯 재정렬을 무력화합니다. 결코 건너뛰지 마십시오.
  • 복수 일치를 허용하되, CEK 충돌만 거부하십시오. 수신자 키는 둘 이상의 슬롯에 정당하게 일치할 수 있습니다(MAY) — 같은 CEK를 같은 수신자에게, 각각 새로운 ephemeral과 함께 여러 슬롯에 봉인하는 것은 정당한 수신자 수 패딩이며 중복 epk/kem_ct 거부를 건드리지 않습니다. 첫 번째 일치의 CEK를 선택하고, 단지 둘 이상의 슬롯이 일치했다는 이유만으로 거부하지 마십시오. 거부해야 할 유일한 이상은 서로 다른 CEK를 복원하는 두 일치 슬롯입니다(상수 시간으로 비교). cek_conflict 비트를 추적하고, 그것이 세워지면 단일 일반 실패를 표면화하십시오. 이는 심층 방어입니다 — 슬롯 집합 커밋먼트 아래에서 서로 다른 CEK를 산출하는 일치는 이미 실현 불가능하므로 — 따라서 결함이 있는 구현에 대해 안전하게 닫히며 실패합니다.
  • 단일 개인 키의 한 패스 안에서 모든 슬롯을 순회하십시오 — 키당 일정한 수의 슬롯 연산, 조기 중단 없음 — 그래야 타이밍 관찰자가 어느 슬롯이 일치했는지를 추론할 수 없습니다. 전-0 거부를 조기 종료가 아니라 kem_ok와 더미 작업을 통해 구동하십시오. 여러 키를 가진 수신자는 키 × 슬롯을 순회하며 키 사이에서 단락(short-circuit)해도 됩니다(MAY)(약한 "어느 키가 일치했는가" 신호만 누설). 다만 어느 한 키의 슬롯들 사이에서는 상수 시간을 유지해야 하며, 두 KEM 모두 수신자 자신의 공개 키를 KEK salt에 결합하므로 키마다 salt의 pub_R 절반을 다시 도출해야 합니다. 그 salt를 키의 정규 와이어 인코딩 — 정확히 32바이트인 X25519 공개 키, 또는 정확히 고정된 1216바이트인 X-Wing 공개 키 바이트 — 에 결합하고, 비정규 재인코딩은 결코 사용하지 마십시오. 그러지 않으면 양측이 서로 다른 KEK를 도출합니다.
  • 신뢰할 수 없는 호출자에게는 하나의 일반 실패 형태를 표면화하십시오. 내부적으로는 로컬 진단을 위해 타입이 지정된 결과를 추적해도 됩니다 — WRONG_RECIPIENT_KEY(어떤 슬롯도 열리지 않음), TAMPERED_HEADER(슬롯은 열렸으나 어떤 후보 CEK도 slots_mac을 재현하지 못함), TAMPERED_CIPHERTEXT(CEK가 복원되고 MAC이 검증된 뒤 콘텐츠 AEAD가 실패) — 그러나 외부 관찰자가 이들을 응답 형태로 구별할 수 있어서는 안 됩니다(MUST NOT). 타이밍에 대해 이 모델은 의도적으로 범위가 좁습니다. 검증자는 콘텐츠 복호화 이전에 if NOT found 검사에서 반환해도 됩니다(MAY). 이는 비수신자를, 암호문이 열리지 않는 수신자와 분리합니다. 그것이 드러내는 것은 수신자인지 비수신자인지뿐이며, 어느 슬롯인지도 어떤 키 자료도 결코 드러내지 않습니다. 그 두 경우 사이에서 타이밍을 균일하게 하는 것은 요구되지 않으며, 더미 콘텐츠 열기를 의무화해서는 안 됩니다(MUST NOT). 성립하는 상수 시간 보장은 위의 슬롯 횡단 불변 원칙입니다.
  • 복호화 후 평문 해시를 다시 계산하여 비교하십시오. 온체인의 hashes 맵은 암호문이 아니라 평문에 커밋하므로, 수신자는 (애플리케이션 계층에서) 다이제스트를 다시 계산하여 비교해야 합니다. sha2-256 항목이 일치해야 하며, 존재한다면 blake2b-256도 일치해야 합니다. 불일치는 레코드의 해시 주장이 복호화된 바이트와 맞지 않음을 뜻합니다 — 그 평문에 근거해 행동하기를 거부하십시오. 구조 검증기는 결코 복호화하지 않습니다.

양측 모두에서 페이로드를 제한하기

분할 STREAM은 어떤 암호학적 페이로드 천장도 부과하지 않습니다. 88비트 청크별 카운터는 2^88개의 청크를 허용하고, 각 청크는 RFC 8439 단일 호출 한계에 한참 못 미치는, 각기 다른 (content_key, nonce) 쌍 아래에서 봉인되므로, 경계해야 할 카운터 오버플로 위험은 없습니다. 따라서 생산자나 검증자가 강제하는 최대값은 와이어 상수가 아니라 배포 단계의 서비스 거부 정책입니다 — 스트림을 쓰거나 읽는 동안 증분적으로 강제하고, 과대한 페이로드를 버퍼링하기 전에 중단하십시오. 절단은 크기 상한이 아니라 최종 플래그에 의해 구조적으로 포착됩니다. 동일한 자세가 slots 경로와 passphrase 경로 양쪽에 적용됩니다.

봉인된 PoE 적합성 픽스처

코퍼스에서 봉인된 PoE 영역은 대부분의 언어 횡단 버그가 표면화되는 곳입니다. 구현을 그 전부에 통과시키십시오. 긍정 픽스처는 두 KEM에 대해 결정적 래핑과 시험 복호화 루프(단일 및 복수 수신자, 혼합 N, 그리고 다중 개인 키 최악의 경우)를 고정하고, 한 수신자가 두 슬롯에 일치하는 정당한 경우(새로운 ephemeral, 동일한 CEK, 반드시(MUST) 복호화되어야 하므로, 복수 일치를 거부하는 구현은 여기서 실패합니다)와 passphrase 경로(하나의 블롭 안의 커밋먼트 헤더 더하기 STREAM 청크)를 더합니다. 전용 STREAM 레이아웃 집합은 빈 평문(길이가 0인 최종 청크 하나), 단일 청크 페이로드, 그리고 65536바이트 경계를 가로지르는 다중 청크 페이로드를 고정합니다. 표적 KAT는 두 KEK salt(SHA-256(label ‖ enc.nonce ‖ <KEM material> ‖ pub_R)), hashes_hash와 두 트랜스크립트에서의 그 위치, draft-10에 대한 X-Wing 캡슐화, 길이가 0인 salt의 HKDF 추출(RFC 5869 §2.2의 salt 부재 관례이며, slots_mac 키 도출을 반영), Bech32 수신자/비밀 인코딩, 그리고 체크섬이 붙은 신원 시드 인코딩을 고정합니다.

부정 픽스처는 거부 코드를 고정합니다. 정직한 슬롯 앞에 놓인 위조 그림자 슬롯(레코드는 정직한 CEK 아래에서 반드시(MUST) 여전히 복호화되어야 함), 슬롯 형태를 유효하게 두는 헤더 뒤집기(kem/aead/scheme), 다른 해시 주장을 가진 아이템에 이어 붙이는 hashes 접합, 패스프레이즈 커밋먼트 실패(잘못된 패스프레이즈, 변조된 salt/params, 변조된 헤더 — 모두 어떤 청크가 열리기 이전에 실패), 패스프레이즈 정규화 거부(미할당 코드포인트 입력과 공백만으로 이루어진 입력), 전-0 X25519 공유 비밀, 레코드 내부 중복 슬롯, 그리고 STREAM 변조 사례(뒤집힌 청크 태그, 절단된 스트림, 후행 데이터, 짧은 비-최종 청크)입니다. 두 속성은 바이트 벡터가 없으며 대신 행동으로 단언됩니다. CEK 충돌 거부(그것을 구성하는 것은 표준이 실현 불가능하다고 가정하는 바로 그 다중 키 커밋먼트 충돌입니다)와 슬롯 횡단 상수 시간 보장입니다. 고정된 모든 바이트 문자열을 재현하고, 모든 부정 사례에 대해 정확한 코드를 발신하십시오.

봉인된 PoE의 한 속성은 바이트 벡터가 없습니다. CEK 충돌 거부 — 서로 다른 CEK를 복원하는 두 일치 슬롯 — 는 픽스처로 구성할 수 없습니다. 그것을 구성하는 것은 표준이 실현 불가능하다고 가정하는 바로 그 다중 키 커밋먼트 충돌이기 때문입니다. 대신, 시험 복호화 루프가 강제된 충돌에 대해 안전하게 닫히며 실패함을 단언하는 구현 수준의 행동 테스트로 고정하십시오. 이는 슬롯 횡단 상수 시간 속성이 바이트 문자열이 아니라 행동으로 단언되는 것과 같은 방식입니다.

적합성과 테스트 벡터

규범적 테스트 벡터는 그 자체가 상호운용 계약입니다. 한 구현이 적합하다고 할 수 있는 것은, 동일한 입력으로부터 적합성 스위트 안의 모든 고정 바이트 문자열을 재현하고, 또한 모든 부정 픽스처에 대해 올바른 타입의 오류 코드를 발신하는 경우, 그리고 오직 그 경우뿐입니다. 부분 점수도 없고 항소의 여지도 없습니다. 비교가 실패하면 틀린 것은 구현이지, 결코 벡터가 아닙니다.

벡터는 표준의 적합성 스위트에 담겨 있으며, 프리미티브 종류별로 정리되어 있습니다. 레코드 픽스처, 봉인된 PoE 래핑/언래핑, COSE_Sign1 서명, HKDF, 시드 도출, Argon2id, 그리고 canonical CBOR입니다. 각 픽스처는 소문자 16진수 입력과 기대 출력을 고정합니다. 사용법은 이렇습니다. 입력을 자신의 구현에 넣고, 이름이 붙은 각 출력을 바이트 단위로 비교한 뒤, 불일치가 있으면 코드를 고치십시오.

모든 구현이 충족해야 하는 세 가지 의무

긍정 벡터를 재현하십시오. 모든 레코드 픽스처에 대해, encode(record) == expected_cbor와 왕복 encode(decode(expected_cbor)) == expected_cbor 두 절반이 반드시(MUST) 동시에 성립해야 합니다. 이 왕복은 픽스처 너머로 일반화됩니다. 임의의 정형 입력에 대해 encode(decode(x)) == x가 성립합니다. 정보를 잃거나 재배열하는 디코더, 또는 canonical하지 않은 인코더는 이를 깨뜨려 적합성에 실패합니다.

올바른 거부 코드를 발신하십시오. 부정 픽스처는 의도적으로 비정형으로 만든 레코드를, 구조 검증기가 반드시(MUST) 발생시켜야 하는 정확한 타입의 오류 코드와 짝짓습니다. 유효한 레코드의 바이트를 재현하는 것은 계약의 절반일 뿐이며, 무효한 레코드를 올바른 코드로 거부하는 것이 나머지 절반입니다. 나쁜 레코드를 잘못된 이유로 거부하는 — 또는 받아들이는 — 검증기는 적합하지 않습니다. 부정 픽스처는 언어 횡단 거부 패리티에 관한 유일한 권위 있는 출처입니다. 동일한 비정형 입력은 모든 구현에서 동일한 코드를 반드시(MUST) 발생시켜야 합니다. 코드의 완전한 목록과 그 의미는 검증에 있습니다.

레지스트리와 일치시키십시오. 알고리즘 식별자는 알고리즘 레지스트리의 레지스트리에서 가져온, 이름이 붙은 문자열입니다. 인식되지 않는 식별자는 묵묵히 받아들이거나 패닉을 일으키는 것이 아니라, 정확한 "지원되지 않는 알고리즘" 코드를 반드시(MUST) 표면화해야 합니다.

구현을 고치되, 벡터는 결코 고치지 마십시오

벡터는 상위 RFC와 본 표준의 결정적 구성에 고정되어 있습니다. 비교가 실패할 때, 버그는 테스트 대상 구현에 있습니다. 스위트를 통과시키려고 벡터를 편집하는 것은, 실제 상호운용 실패를, 레코드가 체인 위에서 구현을 넘나들 때에야 비로소 표면화되는 잠복 실패로 바꿔치기하는 것입니다 — 그것을 발견하기에 가장 나쁜 시점입니다.

모든 변경마다 패리티를 실행하십시오

둘 이상의 언어로 배포하는 — 또는 다른 구현과의 상호운용성을 입증하려는 — 구현은, 모든 패키지를 빌드하고, 각 언어의 테스트 스위트를 공유 픽스처에 대해 실행하며, 의존성 그래프 lint를 강제하고, 픽스처 집합이 양측에서 동일한지를 확인하는 단일 지속적 통합 작업을 실행해야 합니다(SHOULD). 한쪽에는 추가되었으나 다른 쪽에는 추가되지 않은 픽스처는 게이트를 실패시킵니다. 두 구현이 소리 없이 갈라졌다는 뜻이며, 빌드는 실제 레코드가 그것을 드러내기 전에 그것을 붙잡습니다. 픽스처가 권위 있는 출처입니다. 각 언어는 바이트 단위로 동일한 미러를 보유하고, 게이트는 그 미러가 완전하고 정확함을 단언합니다.

명명과 와이어 규약

몇 가지 규약이 구현을 읽기 쉽게 하고 와이어 포맷을 안정적으로 유지합니다.

  • 와이어 필드 이름은 snake_case입니다leaf_count, cose_sign1, slots_mac. 이는 언어를 넘나들며 성립합니다. 어떤 언어가 자신의 인메모리 API에서 관용적으로 camelCase를 쓰더라도, 인코딩된 레코드는 snake_case 키를 사용합니다. 그 키들이 서명이 덮는 canonical 바이트의 일부이기 때문입니다.
  • 식별자는 레지스트리 문자열이지, 코드에 박힌 열거형이 아닙니다. 해시, AEAD, KEM, KDF, 서명은 모두 이름이 붙은 식별자를 참조합니다. 알고리즘을 추가하는 것(이를테면 포스트 양자 KEM)은 레지스트리에 항목을 더하는 일이지, 결코 와이어 포맷의 파괴가 아닙니다.
  • 언어 횡단 메서드 이름은 의미적으로 서로 대응합니다. 한 언어의 함수는 다른 언어에서 같은 이름의 대응물을 가집니다(encode_canonical_cborencodeCanonicalCbor). 그래서 어느 한 언어에 능통한 독자는 한쪽 표면을 다른 쪽에 대응시키고, 살펴보는 것만으로 패리티를 추론할 수 있습니다.
  • 암호학 계층을 먼저 부트스트랩하십시오. 암호학적 코어와 와이어 포맷 라이브러리를 벡터와 대조하여 세우고, 애플리케이션 코드를 한 줄 쓰기 전에 패리티 게이트를 초록으로 만드십시오. 독립 검증자는 애플리케이션에 가장 가까운 가장 작은 표면이자 다음으로 구축할 것입니다. 그 밖의 모든 것은, 여러분이 이미 정확함을 입증한 암호학 계층 위에 자리합니다.

관련 페이지

  • 레코드 — 검증기와 인코더가 구현하는 와이어 포맷.
  • 봉인된 PoE — 여기 나오는 구축 레시피 뒤에 있는 구성 레퍼런스.
  • 알고리즘 레지스트리 — 구현이 해석하는, 이름이 붙은 식별자.
  • 검증 — 검증 파이프라인, 독립 검증자, 그리고 오류 코드 목록.