본문으로 건너뛰기

TCB — 파일 시그니처와 MAC

상태: 구현 완료 — 변환기와 모든 런타임 · 대상 버전 105 유지

게이트도 전부입니다. 적합성 코퍼스가 서명되어 나오고, 변조된 사본을 모든 언어가 각각 거부하는지를 확인합니다(8절).

배포된 파일이 없으므로 버전 정수를 올리지 않고 105의 정의를 개정합니다. 이전 105 파일은 다시 내보내면 되고, 호환 경로를 두지 않습니다.

요청서에 「104에 수정」으로 적혀 있으나 현재 파일에 찍히는 정수는 105입니다(104는 그 앞 개정입니다). 정수를 올리지 않는다는 판단은 그대로 적용됩니다.

세 가지를 바꿉니다. 아래는 확정된 설계이고, 구현이 그것을 따라간 기록은 10절에 있습니다.

  1. magic 4바이트가 파일의 맨 앞으로 갑니다 — 암호화의 일부가 아니라 파일 형식 시그니처입니다.
  2. MAC 필드가 헤더에 생깁니다. 0이면 검사하지 않고, recipe가 MAC 키를 지정할 때만 채워집니다.
  3. 헤더가 42바이트 고정이 됩니다 — 암호화 여부·MAC 여부와 무관하게 모든 필드가 같은 자리에 있습니다.

암호화와 MAC은 서로 독립입니다. 4개 조합(평문 · 암호화만 · MAC만 · 둘 다)이 전부 유효합니다.


1. 근거 — 구조 검증이 검출하지 못하는 변조

기존 문서는 MAC을 두지 않는 근거를 이렇게 적었습니다.

MAC은 없습니다. 키가 클라이언트에 있는 이상 MAC은 무결성 보장이 아니라 손상 감지인데, 손상 감지는 구조 검증이 이미 합니다.

두 번째 문장이 사실과 다릅니다. 구조 검증이 검출하는 것은 파일의 구조가 어긋난 경우뿐입니다. 값 하나를 바꾸는 변조는 구조를 어긋나게 하지 않습니다.

변조구조 검증현재 결과
f32 컬럼의 4바이트를 다른 유한값으로 교체byteLength 합계 · 런 길이 합 · 사전 인덱스 범위가 전부 그대로검출되지 않습니다
i32 컬럼의 고정폭 값 교체위와 같음검출되지 않습니다
bool 1바이트를 0↔1위와 같음검출되지 않습니다
길이 접두어·런 길이 변경합계가 어긋납니다검출됩니다
파일 절단·바이트 삽입길이가 어긋납니다검출됩니다

고정폭 원소는 어떤 비트 패턴도 유효한 값이므로, 검사할 것이 남지 않습니다. 데미지 계수를 100배로 바꾸는 변조는 정확히 이 표의 첫 줄입니다.

스트림 XOR의 가단성 — 암호화가 막지 않는 것

envelope의 ChaCha20은 스트림 XOR입니다. 암호문의 비트를 뒤집으면 평문의 같은 자리 비트가 뒤집힙니다 — 키가 없어도 그렇습니다. 공격자가 어느 오프셋이 무슨 값인지 알아내면(같은 클라이언트로 두 번 내보내 비교하는 것으로 충분합니다) 그 자리의 값을 원하는 대로 밀 수 있고, 복호는 성공하며, magic도 맞고, 구조 검증도 통과합니다.

현재 형식에서 암호화는 변조 저항을 전혀 제공하지 않습니다. 「값을 바꿔 넣어도 그대로 로드된다」를 없앤다고 적은 자리가 실제로는 「눈으로 찾기 어렵게 만든다」까지였습니다.

MAC이 올리는 것 — 위협별 판정

MAC 키도 클라이언트에 실립니다. 바이너리에서 키를 꺼낼 수 있는 공격자는 MAC을 다시 계산할 수 있고, 그 공격자를 막는 형식은 없습니다. 그러나 위협의 대부분은 그 아래에 있습니다.

공격자현재MAC 이후
파일을 헥스 에디터로 여는 사용자값 교체가 통과합니다거부됩니다
공개된 변조 도구를 받아 쓰는 사용자통과합니다도구가 키를 함께 배포해야 합니다
클라이언트를 리버싱해 키를 꺼내는 공격자통과합니다통과합니다

첫 두 줄이 서비스에서 실제로 발생하는 것이고, 세 번째 줄은 형식으로 해결할 수 없습니다. MAC이 바꾸는 것은 변조의 난이도와 규모입니다 — 개인이 파일을 고치는 것에서, 클라이언트를 분석할 수 있는 사람이 도구를 만들어 배포하는 것으로 올라갑니다.


2. 헤더 — 새 배치

오프셋 크기 이름 구간 내용
0 4 magic 평문 54 43 42 00 "TCB\0" — 파일 시그니처
4 4 version 평문 fixed32 = 105
8 1 flags 평문 bit0 = 암호화, bit1 = 예약(압축)
9 1 cipher 평문 0 = 없음, 1 = ChaCha20
10 12 nonce 평문 암호화가 아니면 전부 0
22 16 mac 평문 MAC이 없으면 전부 0
38 4 keyCheck 암호문 평문값 54 43 42 00
42 ... body 암호문 counter32 rowCount, columnCount, 디스크립터, 블록
  • 암호화 구간[38, 파일 끝)입니다. keyCheck가 그 구간의 첫 4바이트이므로 keystream 위치 0에서 시작하고, 블록 카운터는 0 그대로입니다.
  • MAC이 덮는 구간[0, 22)[38, 파일 끝)이며, mac 필드 자신만 제외한 파일 전부입니다.
  • 평문 파일에서도 nonce 12바이트와 mac 16바이트는 0으로 자리를 차지합니다.

42바이트 고정의 근거

현재 헤더는 두 가지입니다 — 평문이면 5바이트, 암호화면 22바이트. 리더가 오프셋을 flags에서 계산하고, 복호 뒤에 평문 헤더 5바이트를 덮어써서 창의 시작을 옮깁니다. 필드가 둘 더 늘면 배치는 4개가 되고, 그 계산이 모든 런타임에 각각 들어갑니다.

고정 배치의 대가는 파일당 37바이트입니다. 이전 소규모 코퍼스의 테이블 74개 기준 2.7 KB이고, 데이터셋 전체 크기의 0.01% 미만입니다. 얻는 것은 모든 런타임에서 오프셋 계산이 사라지는 것이고, 리더가 돌려주는 창이 파일 전체가 되므로 헤더를 덮어쓰는 조작도 함께 사라집니다.

magic이 앞으로 가는 이유

지금의 magic은 암호문의 첫 4바이트입니다. 그래서 평문 파일에는 magic이 없습니다 — 파일이 .tcb인지 아닌지를 바이트로 확인할 방법이 없고, file 같은 도구도 이 형식을 식별할 수 없습니다. ZIP의 PK, PNG의 8바이트가 파일의 맨 앞에 있는 것과 같은 이유로 시그니처는 오프셋 0에 둡니다.

keyCheck — magic이 하던 나머지 절반

암호문 머리의 magic은 두 가지 일을 겸하고 있었습니다. 시그니처 쪽은 위로 올라갔고, 「키가 맞는가」를 판정하던 쪽이 keyCheck입니다.

복호한 4바이트가 54 43 42 00이 아니면 「이 파일이 쓰인 키가 아닙니다」로 멈춥니다. 값을 하나도 읽기 전이고, 「파일이 손상되었다」와 구별됩니다. 같은 상수를 두 번 쓰는 것은 우연이 아니라 새 상수를 정의하지 않기 위한 선택입니다 — 시그니처와 키 검사는 목적이 다르지만 필요한 것은 양쪽 다 「미리 알고 있는 4바이트」입니다.

MAC이 있어도 keyCheck는 필요합니다. MAC 키와 암호화 키는 다른 키이므로, MAC 통과는 파일이 변조되지 않았다를 뜻하지 복호 키가 맞다를 뜻하지 않습니다.


3. MAC — 알고리즘과 범위

HMAC-SHA-256, 앞 16바이트

후보모든 런타임의 비용판정
HMAC-SHA-2567개는 표준 라이브러리, 6개는 직접 구현. 32비트 연산만 씁니다. RFC 4231에 시험 벡터가 있습니다채택
Poly1305전부 직접 구현입니다 — 표준 라이브러리가 AEAD의 일부로만 노출하고 단독으로 내주지 않습니다. 130비트 산술이라 64비트가 없는 런타임에서 26비트 limb 구현이 필요합니다채택하지 않습니다
SipHash · BLAKE3라이브러리가 없고 64비트 연산이 필요합니다. JavaScript에서 BigInt 경로가 됩니다채택하지 않습니다
CRC32 · MD5 · SHA-256 단독키가 없으므로 변조자가 다시 계산하면 그만입니다. 손상 감지이지 변조 검출이 아닙니다채택하지 않습니다

ChaCha20과 한 쌍인 Poly1305를 쓰지 않는 이유는 암호학이 아니라 이식 비용입니다. 이 형식은 같은 코드를 13번 옮겨 적어야 하고, 절반이 표준 라이브러리로 끝나는 쪽이 옮겨 적기를 6번으로 줄입니다.

앞 16바이트로 절단합니다. RFC 4868의 HMAC-SHA-256-128과 같은 절단이고, 128비트는 위조 시도를 실질적으로 배제합니다. 32바이트를 그대로 실으면 파일당 16바이트가 더 들고 얻는 것이 없습니다.

검증 비용 — 실측

파일 전체를 한 번 더 훑으므로, 그 비용이 로드 시간에 얼마나 더해지는지가 채택 조건이었습니다. 같은 기기에서 측정한 값입니다.

런타임구현1 MB8 MB처리량
C#플랫폼(HMACSHA256)0.43 ms3.63 ms약 2,250 MB/s
C직접 구현(/O2)2.90 ms23.15 ms약 345 MB/s
TypeScript직접 구현(Node 22)4.7 ms33.9 ms약 225 MB/s

C#이 6배 빠른 것은 구현이 좋아서가 아니라 CPU의 SHA 확장 명령을 쓰기 때문입니다. 직접 구현은 그 명령에 닿지 않습니다.

가장 느린 쪽이 상한입니다 — TypeScript에서 10 MB 데이터셋의 검증이 약 45 ms입니다. 암호화된 파일은 복호가 이미 전체를 한 번 훑으므로, 그 위에 붙는 몫이 이 표입니다. 로드 경로 전체에서 이 정도는 지불할 만하다고 판단하였고, 지불하고 싶지 않은 자리를 위해 verifyMac이 있습니다.

encrypt-then-MAC

MAC은 저장된 바이트(암호화되어 있으면 암호문)에 대해 계산합니다.

mac = HMAC-SHA-256(macKey, file[0..22) ‖ file[38..끝))[0..16)
  • 복호 전에 검증할 수 있습니다. 변조된 파일은 복호도 하지 않고 거부됩니다.
  • 헤더가 함께 덮입니다 — flags·cipher·nonce·version을 바꾸는 것도 검출됩니다. 평문에 MAC을 걸면 nonce를 바꿔치기해도 MAC이 통과합니다.
  • 순서가 반대인 구성(MAC-then-encrypt)은 검증 전에 복호를 강요하므로, 형식이 얻을 것이 없습니다.

결정성

MAC은 파일 바이트의 함수이고, nonce는 평문의 SHA-256에서 나오므로, 같은 입력은 같은 파일을 냅니다. 골든 트리와 형식 고정 테스트가 기대는 성질이 그대로 유지됩니다.

mac 필드가 우연히 16바이트 전부 0이 될 확률은 2⁻¹²⁸입니다. 별도 처리를 두지 않습니다.


4. 키

recipe

"Binary": [
{
"Path": "./build/data",
"EncryptionKeyVariable": "TABBIT_TCB_KEY",
"MacKeyVariable": "TABBIT_TCB_MAC_KEY"
}
]
  • MacKeyVariable · MacKeyFile — 암호화 키와 같은 형식입니다. recipe에는 키의 위치만 적고 키 자체는 적지 않습니다. 둘 다 비어 있으면 MAC을 계산하지 않고, mac 필드는 0으로 남습니다.
  • 둘을 함께 적으면 거부합니다 — 암호화 키와 같은 규칙입니다.
  • 암호화 키와 MAC 키가 같은 값이면 거부합니다. 하나의 비밀을 두 원시 함수에 쓰는 것은 알려진 취약점은 아니지만 회피 비용이 0입니다. 키가 2개 필요한 것이 이 기능의 비용입니다.
  • 키의 형식은 암호화 키와 같은 64자리 16진수이므로 tabbit --new-encryption-key로 만듭니다.
  • 키가 없거나 형식이 틀리면 첫 테이블을 쓰기 전에 멈춥니다.

생성 코드가 내는 것

언어 표기기본값
Tables.encryptionKey (기존)없음
Tables.macKey없음
Tables.verifyMac

open의 인자가 (bytes, key)에서 (bytes, key, macKey, verifyMac)으로 늘어납니다. 언어마다의 표기는 기존 encryptionKey가 놓인 자리를 그대로 따릅니다.


5. 리더의 판정

파일의 mac리더의 macKeyverifyMac결과
0없음검사하지 않고 읽습니다 — MAC을 쓰지 않는 프로젝트
0있음거부. 아래 「벗기기」
비영있음검증하고, 어긋나면 거부합니다
비영없음검사하지 않고 읽습니다 — 키가 없는 리더는 검증할 수단이 없습니다
무엇이든무엇이든거짓검사하지 않고 읽습니다

벗기기 — 키가 있는 리더가 mac 0을 거부하는 이유

「0이면 검사하지 않는다」를 문자 그대로 두면, 공격자가 16바이트를 0으로 덮는 것만으로 검사를 없앨 수 있습니다. 그 경우 MAC은 아무것도 보호하지 않습니다.

그래서 판정의 기준을 파일이 아니라 리더가 키를 갖고 있는지에 둡니다. 키가 있다는 것은 그 프로젝트가 MAC을 쓴다는 뜻이므로, MAC이 없는 파일은 그 프로젝트의 파일이 아닙니다.

그러므로 켜는 순서가 있습니다 — 데이터가 먼저, 클라이언트가 나중입니다. 클라이언트에 키를 먼저 넣으면 아직 MAC이 없는 데이터를 거부합니다.

키가 없는 리더가 mac 비영을 읽는 이유

반대 방향에는 같은 규칙을 적용하지 않습니다. 키가 없는 리더는 검증할 수단이 없고, 거부하면 이미 배포된 구버전 클라이언트가 새 데이터를 받는 순간 데이터를 전혀 읽지 못합니다. 이 형식이 지키기로 한 것 중 하나가 그 경우이므로, 검사하지 않고 읽습니다. 보호받지 못하는 것은 맞지만, 그 클라이언트는 어제까지도 보호받지 않았습니다.

verifyMac

기본이 참이고, 거짓이면 mac 필드를 보지 않습니다. 용도는 개발 도구, 테스트, 그리고 로드 시간을 재는 자리입니다.

이 스위치가 클라이언트 바이너리 안의 불 변수라는 것은 그대로 적어 둡니다. 뒤집을 수 있는 공격자는 이미 키도 꺼낼 수 있으므로 새로 생기는 취약점은 없습니다.

리더의 순서

  1. 시그니처와 버전 — magic 4바이트가 아니면 「테이블 파일이 아닙니다」, 버전이 다르면 그 번호와 함께 멈춥니다.
  2. MAC 검증 — 위 판정표. 복호 전입니다.
  3. 복호 — flags bit0이 서 있으면 제자리에서, keyCheck로 키를 확인합니다.
  4. 구조 검증과 읽기 — 기존과 같습니다.

복호 뒤 리더는 flags bit0을 내리고 cipher와 nonce를 0으로 되돌립니다 — 평문 파일이 그 자리에 갖는 값입니다. 그래서 같은 버퍼에 open을 다시 불러도 복호를 두 번 하지 않고 그대로 통과합니다.

암호화된 파일을 MAC 키와 함께 두 번 여는 것은 예외입니다. 복호가 되돌린 세 필드를 MAC이 덮고 있으므로, 두 번째 호출의 검증은 실패합니다. 실패하는 쪽이 안전한 방향이고(말없이 두 번 복호하지 않습니다) 로드 경로는 파일마다 한 번만 부르지만, 메시지는 「변조되었다」로 나옵니다. 이것을 없애려면 MAC이 envelope 필드를 덮지 않아야 하는데, 그러면 nonce 바꿔치기가 검출되지 않습니다 — 덮는 쪽이 낫다고 판단했습니다.

매니페스트 해시와의 관계

업데이터가 검사하는 매니페스트의 파일 해시는 전송 중 손상을 검출합니다. 키가 없는 해시이고 매니페스트 자신도 함께 내려받는 파일이므로, 파일을 고친 사람이 해시도 고칠 수 있습니다. MAC은 파일이 어디서 왔든 그 파일에 대해 성립합니다. 둘은 대체 관계가 아닙니다.


6. 언어별 구현 방침

암호화 절과 같은 기준입니다 — 직접 구현이 기본이고, 표준 라이브러리가 있으면 그것을 씁니다.

언어무엇을 쓰는가
C · C++ · Unreal · Rust · Dart · TypeScript직접 구현(SHA-256 + HMAC). 외부 의존성 없음
C#System.Security.Cryptography.HMACSHA256
Java · Kotlinjavax.crypto.MacHmacSHA256
Gocrypto/hmac + crypto/sha256
Pythonhmac + hashlib — 표준 라이브러리
RubyOpenSSL::HMAC
PHPhash_hmac('sha256', ...)

직접 구현은 6개이고 SHA-256 압축 함수 + HMAC 래퍼로 언어당 150줄 안팎입니다. 32비트 덧셈·회전· XOR만 쓰므로 64비트 정수가 없는 런타임에서도 같은 코드 구조가 됩니다.

PHP의 ext-sodium을 쓰지 않는 이유는 암호화 절과 다릅니다. 여기서는 hash_hmac이 코어 함수이므로 확장 여부를 따질 필요가 없습니다.

비교는 상수 시간으로 합니다. 16바이트 비교에서 조기 반환하지 않습니다 — 로컬 파일을 여는 경로라 타이밍 공격의 실효성은 낮지만, 상수 시간 비교의 비용도 0에 가깝습니다.


7. 개정된 문서

이 개정은 기존 문서의 결론을 뒤집습니다. 문장을 지우는 것이 아니라 무엇이 틀렸는지를 남겼습니다 — 두 자리에 취소선과 정정이 들어가 있습니다.

문서무엇
doc/binary-format.md 「파일 암호화」헤더 표 교체, 「MAC은 없습니다」 단락을 1절의 근거로 교체, 「스트림 XOR의 가단성」 절 추가
doc/binary-format.md 「변조 검출 — MAC」새 절. 판정표와 켜는 순서, 매니페스트 해시와의 관계
doc/binary-format.md 「실제 파일 한 개, 전부」39바이트 예제가 76바이트가 되었습니다(헤더 5 → 42)
doc/binary-format.md 「테이블 리더의 동작」·「검증 게이트」1번 단계에 MAC 검증, 게이트 3줄 추가
spec/wire/tcb-v104-composed-encodings.md §4「MAC은 없습니다」 항목에 취소선과 정정. 그 판단의 근거가 틀렸다는 사실이 기록으로 남습니다
doc/roadmap.md10번 항목. v104 절의 「값을 바꿔 넣어도 그대로 로드된다를 없앤다」에도 정정
doc/exports.md 「바이너리 익스포트의 recipe 옵션」MacKeyVariable · MacKeyFile, 두 키가 달라야 하는 것, 켜는 순서
doc/recipe.mdBinaryRecipe의 새 프로퍼티 2개
doc/troubleshooting.md새 메시지 3개 — MAC 불일치, MAC 없음, 시그니처 아님
SECURITY.md클라이언트에서 키를 꺼내는 것은 취약점이 아니라는 것과, 키 없이 통과하는 경로는 취약점이라는 것
CHANGELOG.md형식 개정과 다시 내보내야 한다는 사실

8. 검증 게이트

게이트확인하는 것
태그가 덮는 범위변환기의 태그를 독립적으로 계산한 HMAC과 대조합니다 — 이 런타임에서 틀릴 수 있는 것은 알고리즘이 아니라 범위이므로
변조 검출내보낸 파일의 f32 값 1개를 다른 유한값으로 교체한 뒤 ① MAC이 없으면 구조 검증을 통과해 바뀐 값이 그대로 읽히는 것과 ② MAC이 있으면 거부되는 것을 함께 단정합니다. ①이 없으면 이 기능의 근거가 기록되지 않습니다
암호문 변조암호화된 파일의 본문 1바이트를 뒤집고 거부되는 것. 스트림 XOR이므로 이것이 평문 1바이트 변조와 같습니다
헤더 변조nonce·flags·version을 각각 바꾸고 거부되는 것 — encrypt-then-MAC이 헤더를 덮는다는 주장의 게이트
4개 조합 왕복평문 · 암호화만 · MAC만 · 둘 다
벗기기mac을 0으로 덮은 파일을 키가 있는 리더가 거부
건너뛰기verifyMac = false가 변조된 파일을 읽음
구버전 경로mac이 있는 파일을 키가 없는 리더가 읽음
키 검사틀린 암호화 키가 keyCheck에서, MAC 통과 여부와 무관하게 거부되는 것
결정성같은 입력 → mac 포함 같은 바이트
형식 고정BinaryFormatTests의 42바이트 헤더. 오프셋을 스펙에서 다시 적습니다
시그니처모든 .tcb54 43 42 00으로 시작하는 것 — 암호화·MAC 여부와 무관하게
언어별 변조 거부 ×13적합성 코퍼스가 서명되어 나오고, 값 4바이트를 바꾼 사본을 모든 언어가 각각 MAC을 이유로 거부합니다. 검사를 건너뛰는 리더도, 키를 안 넣은 하니스도 여기서 걸립니다
언어별 read-back ×13서명된 코퍼스를 전부가 읽고 값을 대조합니다 — 검증이 오검출하지 않는다는 쪽
계측 — 3절의 표. 상한이 TypeScript의 약 225 MB/s입니다
골든 재기록·샘플 재생성헤더가 바뀌므로 모든 골든 .tcb가 움직입니다. diff가 리뷰 대상입니다

게이트를 닫은 방법 — 서명된 코퍼스와 변조된 사본

처음에는 게이트가 C#과 PHP까지였습니다. 나머지 11개는 구현 중에 손으로 벡터를 확인했을 뿐이고, 이 저장소의 기준으로 그것은 「됨」이 아닙니다. 두 가지 방법을 재고 두 번째를 택했습니다.

방법비용
13개 적합성 하니스에 시험 벡터 단정을 넣기하니스당 20줄. 벡터 하나만 고정됩니다
적합성 코퍼스를 MAC과 함께 내보내기하니스당 키 한 줄. 변환기가 만든 실제 파일로 전부가 전 경로를 밟습니다

적합성 코퍼스가 이제 서명되어 나옵니다. 키는 test/fixtures/keys/에 커밋되어 있고 아무것도 보호하지 않습니다 — 환경 변수가 아니라 파일인 이유는 이 코퍼스를 변환하는 자리가 여럿이기 때문입니다. 하니스는 환경 변수에서 키를 읽고, 그 변수는 한 자리에서 주입됩니다.

그것만으로는 부족합니다. 검사를 아예 안 하는 리더도 서명된 코퍼스는 잘 읽습니다. 그래서 ConformanceMacTests가 코퍼스를 복사해 값 4바이트를 바꾸고, 모든 언어가 각각 거부하는지와 거부 이유가 MAC인지를 단정합니다. 키를 안 넣은 하니스도 여기서 걸립니다 — 검사가 돌지 않으면 거부도 없기 때문입니다.

옮겨 적으면서 하니스 쪽 결함 3개가 드러났습니다. 셋 다 MAC과 무관하게 원래 있던 것입니다.

무엇
C++ 하니스가 예외를 잡지 않고 있었습니다. 리더가 이유를 말해도 프로세스가 메시지 없이 죽습니다
Unreal 스텁의 UE_LOG가 no-op이었습니다. 생성된 리더가 남기는 실패 이유가 전부 버려지고 있었고, 「이유를 남기지 않는 리더」와 구별되지 않았습니다
C 하니스가 /WX에서 getenv·sscanf로 막혔습니다. 리더가 fopen에 쓰던 것과 같은 분기로 고쳤습니다

스큐 코퍼스도 서명해야 했습니다. 적합성 하니스는 스큐 시나리오의 데이터도 읽는데, 그 데이터는 서명되어 있지 않았습니다 — 키를 든 리더가 MAC 없는 파일을 거부하는 규칙이 그대로 발동해서 전부가 전부 실패했습니다. 기능이 설계대로 동작한 것이고, 고칠 자리는 규칙이 아니라 픽스처였습니다. 이 형식을 켜는 프로젝트가 만나는 것과 같은 순서 문제이므로, 「데이터가 먼저」를 픽스처가 지키게 했습니다.


9. 하지 않는 것

  • 키 회전과 서명. 키가 샜다고 판단되면 할 일은 그대로입니다 — 새 키를 만들고, 데이터를 다시 내보내고, 클라이언트를 다시 배포합니다.
  • 비대칭 서명. 검증 키만 클라이언트에 두면 위조를 실제로 막지만, 모든 런타임에 타원곡선 구현이 들어갑니다. 위협 모델이 그 비용을 요구하지 않습니다.
  • 테이블별 MAC 설정. recipe 항목 단위입니다. 한 익스포트 안에서 파일마다 다르면 「이 파일에는 왜 없는가」가 리더의 판단 대상이 됩니다.
  • MAC 키의 난독화. 클라이언트 안의 키를 숨기는 것은 비용만 드는 일이라는 판단은 암호화 절과 같습니다.
  • flags에 MAC 비트 추가.mac이 0인가」와 「비트가 서 있는가」가 어긋날 수 있는 두 번째 상태를 만들지 않습니다.

10. 구현 순서

  1. 스펙 확정 — 11절의 결정 6개가 전부 권고안대로 확정되었습니다. 완료.
  2. 변환기 — 헤더 재배치, TcbMac(HMAC-SHA-256), recipe 프로퍼티 2개와 키 검증, TcbEnvelope 개정. 형식 고정 테스트와 골든 재기록. 완료.
  3. C# 런타임과 게이트lib/cs. 변조 검출 쌍, 벗기기, 4개 조합, 헤더 변조. 완료.
  4. 픽스처 확장encrypted 시나리오가 이제 암호화와 MAC을 함께 켜고, 변조된 파일을 생성된 액세서로 읽는 을 확인합니다. 완료.
  5. 직접 구현 6개 — C · C++ · Unreal · Rust · Dart · TypeScript. 완료.
  6. 표준 라이브러리 7개 — C# · Go · Java · Kotlin · Python · Ruby · PHP. 완료.
  7. 계측 — 3절의 표. 완료.
  8. 문서 개정과 샘플 재생성 — 7절의 목록. 완료.

옮겨 적으면서 알게 된 것

  • SHA-256을 직접 구현한 6개는 전부 「조각들을 한 메시지처럼」 해싱합니다. 태그가 사는 16바이트를 빼고 해싱해야 하는데, 그것을 「0으로 채운 사본을 만들어 해싱」으로 하면 파일 크기만 한 복사가 로드마다 생깁니다. 그래서 sha256(pieces) 방식이 되었고, 부분 블록이 조각 경계에 걸치는 경우가 6개 구현 전부에서 같은 코드가 되었습니다.
  • Java와 Kotlin은 복호 결과를 새 배열에 담습니다. 다른 런타임은 제자리에서 복호하지만 byte[]는 다른 배열의 창이 될 수 없고, 헤더가 고정폭이 되면서 헤더도 살아남아야 하므로 「같은 길이의 새 배열에 헤더를 복사하고 본문을 복호해 넣는」 방식이 되었습니다. 전에는 18바이트를 떼면서 한 번 할당하던 것이라 할당 횟수는 그대로입니다.
  • 전부가 같은 16바이트를 냅니다. 고정된 파일과 고정된 키의 태그 2d8298b1e59598105580e9a61d685a3f를 각 런타임이 상수로 단정합니다 — 알고리즘·덮는 범위·절단 위치가 한 번에 고정됩니다.

11. 결정 사항

승인받은 6개입니다. 전부 권고안대로 확정되었습니다.

번호결정근거
MAC 키를 암호화 키와 별도 항목으로두 기능이 독립이라는 요구를 그대로 옮기면 키도 독립입니다. 하나로 묶으려면 서브키 유도가 필요하고, 그러면 암호화만 쓰는 프로젝트도 HMAC 코드를 싣게 됩니다
키가 있는 리더는 mac 0을 거부「0이면 검사하지 않는다」를 문자 그대로 두면 16바이트를 0으로 덮는 것으로 기능이 무력화됩니다. 대가는 켜는 순서의 제약입니다(5절)
키가 없는 리더는 mac 비영을 읽음거부하면 구버전 클라이언트가 새 데이터를 못 읽습니다. 이 형식이 지키기로 한 경우입니다
헤더는 42바이트 고정파일당 37바이트 대 모든 런타임의 오프셋 계산. 가변으로 두면 헤더 배치가 4개가 됩니다
MAC은 16바이트로 절단RFC 4868과 같은 절단. 32바이트를 실어서 얻는 것이 없습니다
MAC 키는 --new-encryption-key 만들고 문서로 안내키의 형식이 같으므로 명령을 늘리면 같은 일을 하는 옵션이 2개가 됩니다

EOD