핵심 정리
const a = '{"name":"Kim","id":1}';
const b = '{\n "id": 1,\n "name": "Kim"\n}';
// text 자체 hash는 다름
hash(a) !== hash(b);
// 같은 canonicalization 규칙을 적용한 뒤 hash해야 안정적
hash(canonicalize(JSON.parse(a))) === hash(canonicalize(JSON.parse(b)));직렬화 기준
JSON 값이 같아도 text가 같다는 뜻은 아니다
JSON은 공백, 줄바꿈, object member 순서에 의미를 두지 않습니다. 두 문서가 같은 데이터를 표현하더라도 byte 단위 text는 다를 수 있습니다. hash, 서명, 캐시 키를 JSON text에 바로 걸면 같은 데이터가 다른 결과를 만들 수 있습니다.
{"id":1,"name":"Kim"}{
"name": "Kim",
"id": 1
}위 두 문서는 데이터 의미상 같게 다뤄질 수 있지만 문자열은 다릅니다. 그래서 서명이나 hash에는 "어떤 JSON text를 기준으로 삼을지"를 먼저 고정해야 합니다.
canonicalization은 하나의 표준 표현으로 정렬하는 단계다
canonicalization은 같은 JSON 데이터를 항상 같은 byte sequence로 직렬화하기 위한 규칙입니다. 공백 제거, object member 정렬, 문자열 escape 방식, 숫자 표현 같은 세부 사항을 고정합니다. RFC 8785의 JSON Canonicalization Scheme은 이런 목적의 표준화된 방식입니다.
입력 JSON
-> parse
-> canonicalization 규칙으로 다시 serialize
-> hash 또는 signature중요한 점은 "대충 JSON.stringify() 한 번 호출"이 항상 canonicalization이라는 뜻은 아니라는 것입니다. 언어와 라이브러리마다 key 순서, 숫자 출력, Unicode escape 처리 방식이 다를 수 있습니다.
object member 순서에 의존하면 구현 차이에 흔들린다
RFC 8259 기준에서 object는 순서를 가진 record가 아닙니다. 어떤 parser는 입력 순서를 보존하고, 어떤 serializer는 key를 정렬하고, 어떤 구현은 map 내부 순서에 따라 출력합니다. 사람이 보기 위한 JSON에서는 큰 문제가 아니지만 hash나 서명에서는 치명적입니다.
const value = { b: 2, a: 1 };
JSON.stringify(value); // 구현과 생성 과정에 따라 기대 순서를 고정하기 어려움stable hash가 필요하면 object key 정렬 규칙을 명시한 canonical serializer를 써야 합니다. 단순히 "현재 런타임에서 이렇게 나오더라"를 계약으로 삼으면 다른 언어나 버전에서 깨질 수 있습니다.
선택 기준
| 목적 | 선택 |
|---|---|
| 사람에게 보여 줄 pretty JSON | JSON.stringify(value, null, 2) 같은 포맷 |
| API 응답 전송 | 일반 serializer, 계약은 schema로 관리 |
| hash, signature, Merkle tree | canonical JSON 규칙 고정 |
| 언어 간 검증이 필요 | RFC 8785 같은 공개 규칙 사용 |
| 숫자 정밀도가 중요 | number 대신 string 또는 정수 단위 고려 |
주의사항
pretty print 결과를 서명 대상으로 삼으면 공백만 달라져도 서명이 깨집니다. 서명 대상은 사람이 보기 좋은 형식이 아니라 기계가 항상 재현할 수 있는 canonical byte sequence여야 합니다.
{ "id": 1, "name": "Kim" }{
"id": 1,
"name": "Kim"
}canonicalization 전에 중복 key가 있는 JSON을 허용하면 결과가 정의되지 않을 수 있습니다. parser마다 어느 값을 남길지 다를 수 있으므로, 서명이나 hash 대상 JSON은 생성 단계에서 중복 key를 차단해야 합니다.
참고 링크
2 sources