폐쇄망 현장 — 방산·국방 도메인 · 증빙은 서명만 맞으면 되는 것이 아니다 · 이론
서명은 통과했는데 그 증빙은 다른 파일 얘기였다
한 줄 요약
공급망 증빙(SBOM · in-toto Statement · SLSA Provenance)은 서명이 맞는지가 아니라 그 문서가 가리키는 subject 의 digest 가 내가 지금 손에 쥔 파일의 지문과 같은지를 확인한 뒤에야 근거가 된다. 서명은 문서가 바뀌지 않았다는 말이고, 산출물과 문서를 잇는 것은 서명이 아니라 digest 다.
왜 이게 필요했나
폐쇄망 반입 심사는 대개 이렇게 돌아간다. 매체 하나가 들어오고, 그 안에 산출물과 함께 문서 몇 장이 들어 있다. 부품 목록(SBOM), 빌드 내력(Provenance), 시험 통과 확인서. 심사 담당자는 서명 검증 명령을 한 번 돌리고 "검증 통과" 도장을 찍는다. 절차서에도 그렇게 적혀 있다.
그런데 그 명령이 실제로 무엇을 확인했는지 뜯어보면, 확인한 것은 문서 파일 하나가 서명된 뒤로 바뀌지 않았다는 사실뿐이다. 산출물은 그 명령에 등장하지도 않는다. 그러니 서명이 유효한 증빙 한 장을 지난 분기에 이미 승인받은 옛 판에 붙여 두고 새 바이너리를 함께 넣어 보내면, 서명만 보는 절차는 그것을 그대로 통과시킨다. 문서를 고칠 수 있는 쪽은 서명도 다시 붙일 수 있다는 사실이, 서명을 절차의 마지막 관문으로 두는 순간 통째로 빠진다.
증빙이 늘어나면서 이 함정은 오히려 커졌다. 문서가 한 장일 때는 사람이 열어 보지만, 세 장 다섯 장이 되면 도구가 훑고 사람은 결과만 본다. 도구가 무엇을 확인하고 무엇을 확인하지 않는지 아무도 다시 묻지 않는다.
어떻게 동작하나
증빙은 세 층으로 나눠 보면 헷갈리지 않는다.
- 문서 층 — 이 문서가 서명된 뒤로 바뀌지 않았는가. 서명 검증이 답한다.
- 결속 층 — 이 문서가 말하는 대상이 내가 받은 파일인가. subject 의 digest 가 답한다.
- 주장 층 — 그래서 무엇을 주장하는가. predicate 가 답한다.
[in-toto Statement v1](https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md)은 네 필드로 이 구조를 그대로 담는다. _type 은 늘 https://in-toto.io/Statement/v1 이고, subject 는 이 증빙이 적용되는 산출물의 배열, predicateType 은 주장의 종류를 가리키는 URI, predicate 는 주장의 내용이다. 문서는 subject 의 모든 원소가 digest 를 반드시 가져야 한다고 못박고, 산출물은 이름이 아니라 오로지 digest 로 맞춘다고 따로 적어 둔다. 이름은 구분용 꼬리표일 뿐이라 바꿔 적어도 아무 일이 일어나지 않는다. subject 의 원소는 [ResourceDescriptor](https://github.com/in-toto/attestation/blob/main/spec/v1/resource_descriptor.md) 라는 공용 형식이고, uri · digest · content 중 최소 하나는 있어야 한다.
predicateType 이 https://slsa.dev/provenance/v1 이면 predicate 는 [SLSA Provenance v1](https://slsa.dev/spec/v1.0/provenance) 이다. 여기에는 buildDefinition 과 runDetails 두 덩어리가 들어간다. buildDefinition 에는 빌드의 틀을 가리키는 buildType, 밖에서 들어온 입력인 externalParameters, 플랫폼이 스스로 넣은 internalParameters, 빌드가 끌어다 쓴 것들의 목록인 resolvedDependencies 가 있다. runDetails 에는 빌드를 돌린 플랫폼을 가리키는 builder.id, 그리고 metadata 의 invocationId · startedOn · finishedOn 이 있다. 명세는 externalParameters 는 신뢰하지 말고 아래쪽에서 반드시 검증하라고, internalParameters 는 플랫폼을 이미 믿는 것이므로 검증할 필요가 없다고 나눠 적는다. 이 구분이 중요한 이유는 하나다 — 검증할 자리와 믿는 자리를 문서가 미리 갈라 놓았다는 뜻이기 때문이다.
그리고 Provenance 가 말하지 않는 것이 있다. [SLSA 레벨 문서](https://slsa.dev/spec/v1.0/levels)의 Build 트랙은 "산출물이 기대한 대로 빌드됐는가" 를 검증할 수 있게 하는 것이 목적이라고 적는다. 누가 어떤 과정으로 만들었는지는 다루지만, 그 소스가 검토를 거쳤는지, 의존성에 알려진 취약점이 없는지는 Build 트랙의 일이 아니다. L1 은 증빙이 존재하기만 하면 되고 위조도 쉽다고 문서가 스스로 말한다. L2 에서 전용 인프라와 서명이 붙고, L3 에서 서명 키가 빌드 단계에서 닿지 않게 격리된다. 그러니 "SLSA 증빙이 있다" 는 말은 레벨을 빼면 거의 아무 말도 아니다.
부품 목록은 [CycloneDX](https://cyclonedx.org/specification/overview/) 나 [SPDX](https://spdx.github.io/spdx-spec/v2.3/) 로 온다. CycloneDX 1.6 JSON 은 bomFormat 이 CycloneDX, specVersion 이 판 번호, components 가 구성품 배열이고, 구성품마다 type 과 name 이 필수다. 지문은 hashes 배열에 {"alg": "SHA-256", "content": "..."} 꼴로 들어간다. 여기서 사람들이 자주 걸리는 자리가 metadata.component 다. 그것은 이 SBOM 이 설명하는 대상 자체이지 구성품 목록의 원소가 아니다.
마지막으로 폐쇄망이 하나를 더 뺏는다. [Sigstore](https://docs.sigstore.dev/) 식 흐름은 서명을 투명성 로그에 올리고 검증하는 쪽이 그 로그를 조회해 확인하는 것을 전제로 하는데, 망 밖으로 나가는 길이 없으면 그 조회가 성립하지 않는다. 그래서 반입 심사 보고서에는 "확인했다" 와 "이 망에서는 확인할 길이 없다" 를 반드시 갈라 적어야 한다.
문서 층 서명 검증 → 이 문서가 안 바뀌었다결속 층 subject.digest → 이 문서는 '이 파일' 얘기다 ← 여기가 비면 나머지가 다 헛것주장 층 predicate → 그래서 이렇게 만들어졌다고 주장한다현장에서 만나는 모습
가장 흔한 사고는 증빙 바꿔치기다. 서명은 유효한데 subject 가 다른 파일을 가리킨다. 옛 판을 가리키게 해 두면 "지난번에 통과한 그 증빙" 이라 더 잘 통과한다. 이걸 잡는 방법은 하나뿐이다. 받은 파일에서 지문을 다시 계산해 subject 와 맞춰 보는 것. 이 한 줄이 절차서에 없으면 나머지 검증은 장식이다.
두 번째는 SBOM 을 한쪽으로만 대조하는 것이다. 문서를 기준으로 훑으면 문서가 말하지 않는 파일이 조용히 들어오고, 트리를 기준으로 훑으면 문서에만 있는 유령 항목을 못 본다. 이름이 양쪽에 다 있는데 지문만 다른 경우는 또 다른 종류라, 목록만 맞춰 보는 대조는 그것을 통째로 놓친다.
세 번째는 모르는 술어를 만났을 때다. predicateType 이 우리가 읽을 줄 모르는 URI 면, 그 문서는 통과의 근거가 될 수 없다. 그렇다고 거절의 근거도 아니다. in-toto 의 파싱 규칙은 정책을 단조롭게 짜라고 권한다 — "나쁜 증빙이 있으면 막기" 가 아니라 "좋은 증빙이 있어야 통과" 로 쓰라는 것이다. 그렇게 짜 두면 증빙 한 장을 못 읽어서 통과가 막힐 뿐, 못 읽었다는 이유로 통과가 열리지는 않는다.
네 번째는 공개키다. 검증에 쓴 공개키가 증빙과 같은 매체로 왔다면, 그 매체를 쥔 쪽이 문서와 서명과 공개키를 한꺼번에 바꿔 넣을 수 있다. 서명 검증 결과는 여전히 통과다. 그래서 심사 기록에는 서명이 유효하다는 사실과 그 키가 발주처의 것이라는 확인을 따로 적는다. 뒤엣것은 별도 경로로 받은 지문이 없으면 확인되지 않는다.
다음 실습에서 할 것
발주처가 보내 온 산출물과 증빙 세 종을 망 안에 재현하고, 여덟 단계로 대조한다. 산출물 지문과 subject 대조, SBOM 양방향 대조, Provenance 로 답할 수 있는 질문과 없는 질문 가르기, 서명 검증 한 번, 그리고 서명이 유효한 채로 subject 만 옛 판을 가리키는 증빙을 직접 만들어 "서명 OK · 대조 FAIL" 을 두 층으로 재현한다. 마지막에는 확인한 것과 확인 못 한 것을 나눠 적은 반입 심사 보고서를 남긴다.