SigV4 canonical URI 이중 인코딩 — Bedrock 403
SigV4 서명을 문서대로 맞춰 구현했지만 계속 403 이 났습니다. 서버는 서명이 안 맞는다고만 하고 어느 단계에서 어긋났는지는 안 알려줍니다.
브라우저 확장에서 AWS Bedrock 을 직접 부르는 구조라, 서버 없이 호출이 사용자 브라우저에서 바로 나갑니다. AWS API 는 요청에 SigV4 서명이 붙어 있어야 받아 주고, 평소에는 SDK 가 알아서 만들어 주니 들여다볼 일이 없습니다.
그런데 확장은 번들 크기가 그대로 배포 파일 크기가 됩니다. 사용자가 설치할 때 받는 용량이라 가볍게 유지하고 싶었고, Bedrock 호출 하나 때문에 SDK 를 통째로 넣기가 아까웠습니다.
그래서 서명을 직접 구현했고, 해시와 HMAC 은 브라우저에 있는 WebCrypto 로 만들면 외부 라이브러리가 필요 없습니다.
canonical request URI의 이중 인코딩
SigV4 서명은 요청을 정해진 형식(canonical request)으로 정규화한 뒤 그걸 해싱해서 만들고, 서버도 받은 요청을 같은 방식으로 정규화해 서명을 다시 만들고 둘을 비교합니다. 그러니 한 글자라도 다르게 정규화하면 해시가 통째로 달라지고, 그게 403 입니다.
어긋난 자리는 canonical request 의 URI path 였습니다. S3 를 제외한 모든 서비스는 URI path 를 두 번 URI 인코딩해야 합니다.
Bedrock 의 호출 경로에는 모델 ID 가 들어갑니다.
/model/us.amazon.nova-lite-v1:0/converse
모델 ID 의 콜론(:)은 URI 인코딩 대상입니다. 한 번 인코딩하면 %3A. canonical request 에서는
그 %3A 의 % 를 다시 인코딩해서 %253A 가 돼야 합니다. 저는 한 번만 인코딩해서 %3A 로
서명했고, 서버는 %253A 로 서명했으니 맞을 리가 없습니다.
내가 서명한 경로: /model/us.amazon.nova-lite-v1%3A0/converse (단일 인코딩)
서버가 기대한 경로: /model/us.amazon.nova-lite-v1%253A0/converse (이중 인코딩)
→ 서명 불일치 → 403
%3A 와 %253A. 한 글자 차이지만 해시는 통째로 달라집니다.
이중 인코딩의 이유
이상하게 들리지만 canonical request 는 이미 URI 인코딩된 경로를 받아서 그걸 또 한 번 인코딩한 값으로 정규화하도록 스펙에 정해져 있습니다. 실제 HTTP 요청으로 나가는 경로는 한 번 인코딩한 것이고, 서명 계산에만 두 번 인코딩한 값을 씁니다.
S3 만 예외고, 객체 키에 슬래시나 특수문자가 그대로 들어갈 수 있어서 키를 원문대로 다뤄야 하기 때문입니다. 그래서 “S3 는 단일, 나머지는 이중” 이라는 규칙이 나왔습니다.
SDK 를 쓰면 이걸 알아서 처리해주니 평생 모르고 지나갑니다. 직접 구현하면 정확히 여기서 막힙니다.
직접 구현할 때 확인 순서
SigV4 를 손으로 짜다 403 이 나면 먼저 모든 요청이 실패하는지 일부만 실패하는지를 봅니다. 전부 실패하면 서명 자체가 틀린 것이라 서명 키 유도(HMAC 체이닝)와 서명 스코프(날짜·리전·서비스)를 봐야 하고, 단순한 경로는 되면서 특수문자가 든 경로에서만 403 이 나면 canonical request 쪽입니다. URI 이중 인코딩, 쿼리 파라미터 정렬, 헤더 소문자화와 정렬, SignedHeaders 목록이 여기 해당합니다.
콜론이나 공백, 슬래시가 들어간 경로로 한 번 던져 보면 둘 중 어느 쪽인지가 바로 갈립니다. Bedrock 모델 ID 처럼 콜론이 든 경로가 딱 이 지점에서 걸립니다.
댓글 0