Cheondi
개발 · 보안

빌드 산출물의 출처와 SLSA

같은 소스에서도 다른 산출물이 나오는 빌드 문제를 계기로 source SHA와 artifact SHA를 provenance로 연결하는 SLSA를 정리했습니다.

  • #slsa
  • #supply-chain
  • #build
  • #provenance
  • #security

Android 라이브러리나 웹 번들을 전달할 때 파일명만으로 버전을 구분한 적이 있다. release-final.aar, release-final2.aar처럼 이름이 늘어나면 어느 커밋에서 어떤 설정으로 만들었는지 나중에 확인하기 어렵다. 소스는 같아 보여도 JDK, 의존성, 빌드 옵션이 다르면 산출물은 달라질 수 있다.

처음에는 Git 커밋 SHA를 파일명에 붙이는 것으로 충분하다고 생각했다. 그런데 SHA는 “어떤 소스를 의도했는가”는 알려 줘도, 실제 파일을 누가 어떤 과정으로 만들었는지는 증명하지 못한다. 공급망 보안 문서에서 SLSA와 provenance를 접한 뒤 이 간격을 조금 더 구체적으로 볼 수 있었다.

산출물과 빌드 기록을 연결하기

작은 프로젝트에서 시작할 수 있는 기록은 복잡하지 않다.

{
  "source": {
    "repository": "https://github.com/example/example-app",
    "revision": "8d12e7f"
  },
  "build": {
    "workflow": "android-library-release",
    "builder": "example-ci",
    "startedAt": "2025-03-15T04:10:00Z"
  },
  "artifact": {
    "name": "example-sdk.aar",
    "sha256": "4ec0...example...9a1f"
  }
}

이 JSON 자체가 강한 보안 보증은 아니다. 누군가 산출물과 기록을 같이 바꿀 수 있기 때문이다. 그래도 소스 버전, 빌드 방식, 결과 해시를 한 묶음으로 남기는 것부터 재현과 확인이 쉬워진다.

산출물을 받은 쪽에서는 파일 해시를 다시 계산해 기록과 비교할 수 있다.

$artifact = 'path/to/example-sdk.aar'
$actual = (Get-FileHash -Algorithm SHA256 $artifact).Hash.ToLowerInvariant()
$expected = '4ec0...example...9a1f'

if ($actual -ne $expected) {
    throw 'Artifact digest mismatch'
}

파일명이 같아도 바이트가 다르면 바로 알 수 있다. 반대로 해시가 같다면 최소한 검증한 파일과 기록된 파일이 동일하다고 말할 수 있다.

SLSA에서 보는 단계

SLSA는 소프트웨어 산출물의 무결성을 높이기 위한 프레임워크다. v1.0에서는 빌드 트랙의 수준을 나누어, provenance가 존재하는지부터 격리되고 강화된 빌드 플랫폼인지까지 점진적으로 본다.

확인 질문 얻는 것 아직 남는 것
provenance가 있는가 소스·빌드·결과 연결 기록 위조 가능성
빌드 서비스가 서명했는가 생성 주체 검증 빌더 자체의 신뢰
빌드가 격리됐는가 다른 작업의 간섭 감소 의존성 취약점
배포 전 검증하는가 잘못된 산출물 차단 런타임 설정 위험

중요한 점은 서명된 provenance가 모든 공급망 위험을 해결하지 않는다는 것이다. 악성 소스에서 정직하게 빌드한 산출물도 provenance는 유효할 수 있다. 취약한 의존성, 잘못된 배포 설정, 유출된 서명 키는 별도 통제가 필요하다.

빌드 과정에서 자동으로 만들기

사람이 JSON을 수동 작성하면 실수가 많다. CI가 체크아웃한 정확한 SHA와 산출물 해시를 빌드가 끝난 직후 생성하는 편이 낫다.

SOURCE_SHA="$(git rev-parse HEAD)"
ARTIFACT="build/outputs/example-sdk.aar"
ARTIFACT_SHA="$(sha256sum "$ARTIFACT" | cut -d' ' -f1)"

printf '%s %s\n' "$SOURCE_SHA" "$ARTIFACT_SHA"

실제 운영에서는 이 값을 provenance 포맷에 넣고, 빌드 서비스의 신원으로 서명하며, 배포 단계가 신뢰한 빌더의 서명인지 확인하도록 구성할 수 있다. 비밀 키를 빌드 스크립트나 저장소에 넣는 방식은 피해야 한다.

내가 이해한 흐름은 다음과 같다.

검토된 source SHA
  → 고정된 workflow와 builder
  → artifact 생성
  → artifact SHA 계산
  → provenance 발급·서명
  → 배포 전에 정책 검증

지금 바로 적용할 작은 기준

  • 릴리스 파일은 Git SHA와 빌드 번호를 함께 기록한다.
  • 산출물마다 SHA-256을 계산하고 전달 기록에 남긴다.
  • 로컬 빌드와 CI 빌드를 같은 결과라고 부르지 않는다.
  • CI 로그 성공과 실제 배포된 파일을 별도 증거로 확인한다.
  • provenance 서명 키와 배포 권한을 소스 저장소에서 분리한다.
  • 재빌드 가능 여부와 바이트 단위 재현 가능 여부를 구분한다.

SLSA를 처음부터 가장 높은 수준으로 도입하기는 어렵다. 그래도 “이 파일이 어디서 왔는가”라는 질문을 소스 SHA 하나로 끝내지 않는 습관은 바로 시작할 수 있다. 빌드 산출물의 출처는 파일명에 적힌 버전이 아니라, 소스와 빌더와 결과 해시가 이어진 증거에 있었다.

검증 기록은 배포 사고를 조사할 때도 도움이 된다. 서버가 제공하는 파일의 해시를 다시 계산해 provenance와 비교하면 “소스에는 수정이 있다”와 “그 수정이 들어간 산출물이 실제 제공된다”를 분리할 수 있다. 두 값이 다르면 애플리케이션 로직을 더 분석하기 전에 빌드·전달 경로부터 확인한다. 이처럼 출처 정보는 보안 문서이면서 동시에 배포 디버깅을 짧게 만드는 운영 자료였다.

릴리스 보관 정책도 함께 정해야 했다. provenance만 남고 산출물이 사라지거나, 산출물만 남고 빌더 기록이 만료되면 나중에 검증할 수 없다. 최소한 지원 기간 동안은 산출물, provenance, 검증에 필요한 공개 키와 workflow 버전을 함께 찾을 수 있어야 한다.

참고: SLSA v1.0 최종 명세 안내