RFC 8785 JSON 정규화 방식 2020년 6월
Rundgren, et al. Informational [Page]
스트림:
Independent Submission
RFC:
8785
분류:
Informational
게시:
ISSN:
2070-1721
저자:
A. Rundgren
Independent
B. Jordan
Broadcom
S. Erdtman
Spotify AB

RFC 8785

JSON 정규화 방식(JCS)

초록

해싱 및 서명과 같은 암호화 작업은, 해당 작업을 안정적으로 반복 가능하게 하기 위해 데이터가 불변 형식으로 표현되어야 한다. 이를 해결하는 한 가지 방법은 데이터의 정규 표현을 만드는 것이다. 정규화는 또한 데이터가 "wire"에서 원래 형식으로 교환되도록 허용하는 동시에, 생산자와 소비자 엔드포인트에서 데이터의 정규화된 대응물에 대해 수행되는 암호화 작업이 일관된 결과를 생성하도록 한다.

이 문서는 JSON Canonicalization Scheme(JCS)을 설명한다. 이 명세는 ECMAScript가 정의한 JSON 기본값의 엄격한 직렬화 방법을 기반으로 하고, JSON 데이터를 Internet JSON(I-JSON) 하위 집합으로 제한하며, 결정적 속성 정렬을 사용하여 JSON 데이터의 정규 표현을 만드는 방법을 정의한다.

이 메모의 상태

이 문서는 인터넷 표준 트랙 명세가 아니며, 정보 제공 목적으로 게시된다.

이는 다른 어떤 RFC 스트림과도 독립적으로 RFC 시리즈에 기여한 문서이다. RFC Editor는 재량에 따라 이 문서를 게시하기로 선택했으며, 구현 또는 배포에 대한 가치에 대해서는 어떠한 진술도 하지 않는다. RFC Editor가 게시를 승인한 문서는 어떤 수준의 인터넷 표준 후보도 아니다. RFC 7841의 Section 2를 참조하라.

이 문서의 현재 상태, 정오표, 피드백 제공 방법에 대한 정보는 https://www.rfc-editor.org/info/rfc8785에서 얻을 수 있다.

목차

1. 소개

이 문서는 JSON Canonicalization Scheme(JCS)을 설명한다. 이 명세는 ECMAScript [ECMA-262]가 정의한 JSON 기본값의 엄격한 직렬화 방법을 기반으로 하고, JSON 데이터를 I-JSON [RFC7493] 하위 집합으로 제한하며, 결정적 속성 정렬을 사용하여 JSON [RFC8259] 데이터의 정규 표현을 만드는 방법을 정의한다. JCS의 출력은 암호화 방법에서 사용할 수 있는 JSON 데이터의 "해시 가능" 표현이다. 이어지는 문단은 주요 설계 고려사항을 개략적으로 설명한다.

해싱 및 서명과 같은 암호화 작업은, 해당 작업을 안정적으로 반복 가능하게 하기 위해 데이터가 불변 형식으로 표현되어야 한다. 이를 달성하는 한 가지 방법은 데이터를 base64url [RFC4648]처럼 단순하고 고정된 표현을 가진 형식으로 변환하는 것이다. JSON Web Signature(JWS) [RFC7515]는 이 문제를 이런 방식으로 해결했다. 또 다른 해결책은 XML signature [XMLDSIG] 표준에서 수행된 것과 유사하게, 데이터의 정규 버전을 만드는 것이다.

정규화 방식의 주요 장점은 데이터를 원래 형식으로 유지할 수 있다는 점이다. 이것이 JCS의 핵심 근거이다. 달리 말하면, 정규화를 사용하면 JSON 객체가 서명된 후에도 JSON 객체로 남을 수 있다. 이는 시스템 설계, 문서화, 로깅을 단순화할 수 있다.

"바퀴를 다시 발명하는" 일을 피하기 위해, JCS는 버전 6부터 ECMAScript(일명 JavaScript) [ECMA-262]가 정의한 JSON 기본값 (문자열, 숫자, 리터럴)의 직렬화에 의존한다.

숙련된 XML 개발자는 XML 서명을 검증하는 데 어려움을 겪었던 일을 기억할 수 있다. 이는 보통 매우 복잡한 XML 정규화 규칙과 마찬가지로 복잡한 Web Services 보안 표준에 대한 서로 다른 해석 때문이었다. JCS가 유사한 문제를 겪지 않아야 하는 이유는 다음과 같다.

JCS는 JSON Web Key(JWK) Thumbprint [RFC7638] 및 Keybase [KEYBASE]처럼 JSON 정규화에 의존하는 일부 기존 시스템과 호환된다.

암호화 외의 잠재적 사용에 대해서는 [JSONCOMP]를 참조하라.

이 문서의 의도된 독자는 JSON 도구 공급업체와 JSON 기반 암호화 솔루션 설계자이다. 독자는 "JSON" 객체를 포함한 ECMAScript에 대한 지식이 있다고 가정한다.

2. 용어

이 문서는 IETF 표준 트랙에 있지 않다는 점에 유의하라. 그러나 적합한 구현은 보안 및 상호운용성 이유로 지정된 동작을 준수해야 한다. 이 텍스트는 필요한 동작을 설명하기 위해 BCP 14를 사용한다.

이 문서의 핵심 단어 "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", "OPTIONAL"은 여기에 표시된 것처럼 모두 대문자로 나타날 때, 그리고 오직 그럴 때에만 BCP 14 [RFC2119] [RFC8174]에 설명된 대로 해석되어야 한다.

3. 상세 동작

이 섹션은 정규 JSON 표현 생성과 관련된 세부 사항 및 JCS가 이를 어떻게 다루는지를 설명한다.

부록 F는 기존 JSON 도구에 JCS 지원을 추가하는 RECOMMENDED 방식을 설명한다.

3.1. 입력 데이터 생성

정규 직렬화될 데이터는 보통 다음 방식으로 생성된다.

  • 이전에 생성된 JSON 데이터 파싱.
  • 프로그래밍 방식으로 데이터 생성.

사용된 방법과 관계없이, 직렬화될 데이터는 I-JSON [RFC7493] 형식에 맞게 조정되어야 하며(MUST), 이는 다음을 의미한다.

  • JSON 객체는 중복 속성 이름을 보여서는 안 된다 (MUST NOT).
  • JSON 문자열 데이터는 Unicode [UNICODE]로 표현 가능해야 한다(MUST).
  • JSON 숫자 데이터는 IEEE 754 [IEEE754] 배정밀도 값으로 표현 가능해야 한다(MUST). IEEE 754 배정밀도가 제공하는 것보다 높은 정밀도나 더 긴 정수가 필요한 애플리케이션에서는 이러한 숫자를 JSON 문자열로 표현하는 것이 권장된다(RECOMMENDED). 이를 상호운용 가능하고 확장 가능한 방식으로 수행하는 방법에 대한 자세한 내용은 부록 D를 참조하라.

추가 제약은 파싱된 JSON 문자열 데이터가 이후 직렬화 중에 변경되어서는 안 된다는 것이다(MUST NOT). 자세한 정보는 부록 E를 참조하라.

참고: 유니코드 표준은 특정 문자 시퀀스를 재배열할 수 있는 가능성을 제공하며, 이를 "Unicode Normalization" [UCNORM]이라고 부르지만, JCS 준수 문자열 처리는 이를 고려하지 않는다. 즉, JCS에 의존하는 방식에 관여하는 모든 구성요소는 Unicode 문자열 데이터를 "있는 그대로" 보존해야 한다(MUST).

3.2. 정규 JSON 데이터 생성

다음 하위 섹션들은 이전 섹션에서 자세히 다룬 데이터의 정규 JSON 표현을 만들기 위해 필요한 단계를 설명한다.

부록 A는 JCS 명세와 일치하는 ECMAScript 기반 정규화기의 샘플 코드를 보여준다.

3.2.1. 공백

JSON 토큰 사이의 공백은 출력되어서는 안 된다(MUST NOT).

3.2.2. 기본 데이터 타입 직렬화

다음 JSON 객체가 파싱된다고 가정하라.

  {
    "numbers": [333333333.33333329, 1E30, 4.50,
                2e-3, 0.000000000000000000000000001],
    "string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
    "literals": [null, true, false]
  }

파싱된 데이터가 이후 ECMAScript의 "JSON.stringify()"를 준수하는 직렬화기를 사용하여 직렬화되면, 결과는 (표시 목적으로만 줄바꿈을 추가하면) 원래 데이터와 상당히 달라진다.

  {"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27],"string":
  "€$\u000f\nA'B\"\\\\\"/","literals":[null,true,false]}

파싱된 데이터와 직렬화된 대응물 사이의 차이는, 입력 데이터는(JSON [RFC8259]에 정의된 대로) 넓은 허용 범위를 가지는 반면 출력 데이터는(ECMAScript가 정의한 대로) 고정된 표현을 가지기 때문이다. 예제에서 볼 수 있듯이 숫자도 반올림의 대상이 된다.

다음 하위 섹션들은 JCS에 따른 기본 JSON 데이터 타입의 직렬화를 설명한다. 이 부분은 ECMAScript의 해당 부분과 동일하다. (가능성은 낮지만) ECMAScript의 향후 버전이 다음 직렬화 방법 중 어느 하나를 무효화하는 경우, 이 명세를 고수할지 새 명세를 만들지는 개발자 커뮤니티에 달려 있다.

3.2.2.1. 리터럴 직렬화

JSON [RFC8259]에 따라, 리터럴 "null", "true", "false"는 각각 null, true, false로 직렬화되어야 한다(MUST).

3.2.2.2. 문자열 직렬화

JSON 문자열 데이터(JSON 객체 속성 이름도 포함)의 경우, 각 Unicode 코드 포인트는 아래에 설명된 대로 직렬화되어야 한다(MUST). [ECMA-262]의 Section 24.3.2.2를 참조하라.

  • Unicode 값이 전통적인 ASCII 제어 문자 범위(U+0000부터 U+001F까지)에 속하면, 미리 정의된 JSON 제어 문자 U+0008, U+0009, U+000A, U+000C, U+000D 집합에 속하지 않는 한, 소문자 16진수 Unicode 표기(\uhhhh)를 사용하여 직렬화되어야 한다(MUST). 해당 집합에 속하는 경우에는 각각 \b, \t, \n, \f, \r로 직렬화되어야 한다 (MUST).
  • Unicode 값이 ASCII 제어 문자 범위 밖에 있으면, U+005C(\) 또는 U+0022(")와 동등한 경우를 제외하고 "있는 그대로" 직렬화되어야 한다(MUST). 해당 두 경우에는 각각 \\ 및 \"로 직렬화되어야 한다(MUST).

마지막으로, 결과 Unicode 코드 포인트 시퀀스는 큰따옴표(")로 둘러싸여야 한다(MUST).

참고: "lone surrogates"(예: U+DEAD)와 같은 유효하지 않은 Unicode 데이터는 깨진 서명을 포함한 상호운용성 문제를 초래할 수 있으므로, 이러한 데이터가 나타나면 JCS 준수 구현은 적절한 오류와 함께 종료해야 한다(MUST).

3.2.2.3. 숫자 직렬화

ECMAScript는 JSON 숫자 데이터를 표현하기 위해 IEEE 754 [IEEE754] 배정밀도 표준을 기반으로 한다. 이러한 데이터는 "Note 2" 개선사항을 포함하여 [ECMA-262]의 Section 7.1.12.1에 따라 직렬화되어야 한다(MUST).

이 부분의 상대적 복잡성 때문에 알고리즘 자체는 이 문서에 포함되지 않는다. JCS 준수 숫자 직렬화 구현자에게는 V8 [V8]의 Google 구현이 참고가 될 수 있다. 또 다른 호환 숫자 직렬화 참조 구현은 Ryu [RYU]이며, 부록 G에 언급된 JCS 오픈 소스 Java 구현에서 사용된다. 부록 B에는 IEEE 754 샘플 값과 그에 해당하는 JSON 직렬화 집합이 있다.

참고: Not a Number(NaN) 및 Infinity는 JSON에서 허용되지 않으므로, NaN 또는 Infinity가 나타나면 JCS 준수 구현은 적절한 오류와 함께 종료해야 한다(MUST).

3.2.3. 객체 속성 정렬

이전 단계가 기본 JSON 데이터 타입의 표현을 정규화했지만, JSON 객체 속성이 사전식(알파벳) 순서가 아니므로 결과는 아직 "정규"로 인정되지 않는다.

Section 3.2.2의 샘플에 적용하면, 적절히 정규화된 버전은 (표시 목적으로만 줄바꿈을 추가하면) 다음과 같아야 한다.

  {"literals":[null,true,false],"numbers":[333333333.3333333,
  1e+30,4.5,0.002,1e-27],"string":"€$\u000f\nA'B\"\\\\\"/"}

JCS에 따른 JSON 객체 속성의 사전식 정렬 규칙은 다음과 같다.

  • JSON 객체 속성은 재귀적으로 정렬되어야 한다(MUST). 이는 JSON 자식 객체도 해당 속성을 정렬해야 함을 의미한다 (MUST).
  • JSON 배열 데이터도 JSON 객체의 존재 여부를 스캔해야 한다 (MUST). 객체가 발견되면 그 속성은 정렬되어야 한다 (MUST). 그러나 배열 요소 순서는 변경되어서는 안 된다(MUST NOT).

JSON 객체의 속성을 정렬하려고 할 때 다음 조치를 준수해야 한다 (MUST).

  • 정렬 프로세스는 속성 이름 문자열의 "원시"(이스케이프되지 않은) 형식에 적용된다. 즉, 줄바꿈 문자는 U+000A로 처리된다.
  • 정렬될 속성 이름 문자열은 UTF-16 [UNICODE] 코드 단위의 배열로 형식화된다. 정렬은 코드 단위를 부호 없는 정수로 취급하는 순수 값 비교에 기반하며, 로케일 설정과 독립적이다.
  • 속성 이름 문자열은 두 문자열 모두에 유효한 어떤 인덱스에서 서로 다른 값을 가지거나, 길이가 다르거나, 또는 둘 다이다. 하나 이상의 인덱스 위치에서 값이 다르면, k를 그러한 가장 작은 인덱스라고 하자. 그러면 위치 k의 값이 더 작은 문자열이 "<" 연산자를 사용하여 결정된 대로 다른 문자열보다 사전식으로 앞선다. 서로 다른 인덱스 위치가 없으면, 더 짧은 문자열이 더 긴 문자열보다 사전식으로 앞선다.

    평이한 영어로 말하면, 이는 속성 이름이 다음과 같은 오름차순으로 정렬됨을 의미한다.

            ""
            "a"
            "aa"
            "ab"
    

정렬 알고리즘을 UTF-16 코드 단위에 기반시키는 이유는 이것이 ECMAScript(웹 브라우저와 Node.js에서 제공됨), Java, .NET의 문자열 타입에 직접 매핑되기 때문이다. 또한 JSON은 UTF-16 코드 단위로 표현된 이스케이프 시퀀스만 지원하므로, 이러한 데이터에 대한 지식과 처리는 어차피 필요하다. 다른 내부 문자열 데이터 표현을 사용하는 시스템은 정렬하기 전에 JSON 속성 이름 문자열을 UTF-16 코드 단위 배열로 변환해야 한다. UTF-8 또는 UTF-32에서 UTF-16으로의 변환은 Unicode [UNICODE] 표준에 의해 정의된다.

다음 JSON 테스트 데이터는 JCS 구현에서 정렬 방식의 정확성을 검증하는 데 사용할 수 있다.

  {
    "\u20ac": "Euro Sign",
    "\r": "Carriage Return",
    "\ufb33": "Hebrew Letter Dalet With Dagesh",
    "1": "One",
    "\ud83d\ude00": "Emoji: Grinning Face",
    "\u0080": "Control",
    "\u00f6": "Latin Small Letter O With Diaeresis"
  }

속성 문자열 정렬 후 예상되는 인수 순서:

  "Carriage Return"
  "One"
  "Control"
  "Latin Small Letter O With Diaeresis"
  "Euro Sign"
  "Emoji: Grinning Face"
  "Hebrew Letter Dalet With Dagesh"

참고: 결정적 속성 순서를 얻기 위한 목적으로는 UTF-8 또는 UTF-32로 인코딩된 데이터를 정렬해도 동작하지만, 위와 같은 JSON 데이터의 결과는 달라지므로 이 명세와 호환되지 않는다. 그러나 실제로 속성 이름은 7비트 ASCII 밖에서 정의되는 일이 드물기 때문에, UTF-16으로 변환하지 않고 UTF-8 또는 UTF-32 형식의 문자열 데이터를 정렬하면서도 JCS와 호환될 수 있다. 이것이 실행 가능한 선택지인지 여부는 JCS가 사용되는 환경에 따라 달라진다.

3.2.4. UTF-8 생성

마지막으로, 플랫폼 독립적 표현을 만들기 위해 앞 단계의 결과는 UTF-8로 인코딩되어야 한다(MUST).

Section 3.2.3의 샘플에 적용하면, 여기서는 16진수 표기로 표시된 다음 바이트가 생성되어야 한다.

  7b 22 6c 69 74 65 72 61 6c 73 22 3a 5b 6e 75 6c 6c 2c 74 72
  75 65 2c 66 61 6c 73 65 5d 2c 22 6e 75 6d 62 65 72 73 22 3a
  5b 33 33 33 33 33 33 33 33 33 2e 33 33 33 33 33 33 33 2c 31
  65 2b 33 30 2c 34 2e 35 2c 30 2e 30 30 32 2c 31 65 2d 32 37
  5d 2c 22 73 74 72 69 6e 67 22 3a 22 e2 82 ac 24 5c 75 30 30
  30 66 5c 6e 41 27 42 5c 22 5c 5c 5c 5c 5c 22 2f 22 7d

이 데이터는 암호화 방법의 입력으로 사용할 수 있도록 의도되어 있다.

4. IANA 고려사항

이 문서에는 IANA 조치가 없다.

5. 보안 고려사항

시스템의 무결성에 영향을 줄 수 있는 버퍼 오버플로우와 유사한 문제를 피하기 위해 입력 데이터에 대한 정상성 검사를 수행하는 것이 중요하다.

JCS가 부록 F에 설명된 것과 같은 서명 방식에 적용될 때, 애플리케이션은 수신된 데이터에 따라 동작하기 전에 다음 작업을 수행해야 한다(MUST).

  1. JSON 데이터를 파싱하고 I-JSON을 준수하는지 확인한다.
  2. 데이터가 사용될 생태계에서 정의한 관례에 따라 데이터의 정확성을 검증한다. 여기에는 서명 데이터를 보유한 속성을 찾는 것도 포함된다.
  3. 서명을 검증한다.

이러한 단계 중 하나라도 실패하면 진행 중인 작업은 중단되어야 한다(MUST).

6. 참고문헌

6.1. 규범 참고문헌

[ECMA-262]
ECMA International, "ECMAScript 2019 Language Specification", Standard ECMA-262 10th Edition, , <https://www.ecma-international.org/ecma-262/10.0/index.html>.
[IEEE754]
IEEE, "IEEE Standard for Floating-Point Arithmetic", IEEE 754-2019, DOI 10.1109/IEEESTD.2019.8766229, <https://ieeexplore.ieee.org/document/8766229>.
[RFC2119]
Bradner, S., "RFC에서 요구 수준을 나타내기 위해 사용하는 핵심 단어", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/info/rfc2119>.
[RFC7493]
Bray, T., Ed., "I-JSON 메시지 형식", RFC 7493, DOI 10.17487/RFC7493, , <https://www.rfc-editor.org/info/rfc7493>.
[RFC8174]
Leiba, B., "RFC 2119 핵심 단어에서 대문자와 소문자의 모호성", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/info/rfc8174>.
[RFC8259]
Bray, T., Ed., "JavaScript Object Notation (JSON) 데이터 교환 형식", STD 90, RFC 8259, DOI 10.17487/RFC8259, , <https://www.rfc-editor.org/info/rfc8259>.
[UCNORM]
The Unicode Consortium, "Unicode Normalization Forms", <https://www.unicode.org/reports/tr15/>.
[UNICODE]
The Unicode Consortium, "유니코드 표준", <https://www.unicode.org/versions/latest/>.

6.2. 정보 참고문헌

[JSONCOMP]
Rundgren, A., ""Comparable" JSON (JSONCOMP)", Work in Progress, Internet-Draft, draft-rundgren-comparable-json-04, , <https://tools.ietf.org/html/draft-rundgren-comparable-json-04>.
[KEYBASE]
Keybase, "JSON 및 Msgpack을 위한 정규 패킹", <https://keybase.io/docs/api/1.0/canonical_packings>.
[NODEJS]
OpenJS Foundation, "Node.js", <https://nodejs.org>.
[OPENAPI]
OpenAPI Initiative, "OpenAPI Specification: 현대 API를 설명하기 위해 널리 채택된 업계 표준", <https://www.openapis.org/>.
[RFC4648]
Josefsson, S., "Base16, Base32 및 Base64 데이터 인코딩", RFC 4648, DOI 10.17487/RFC4648, , <https://www.rfc-editor.org/info/rfc4648>.
[RFC7515]
Jones, M., Bradley, J., and N. Sakimura, "JSON Web Signature (JWS)", RFC 7515, DOI 10.17487/RFC7515, , <https://www.rfc-editor.org/info/rfc7515>.
[RFC7638]
Jones, M. and N. Sakimura, "JSON Web Key (JWK) Thumbprint", RFC 7638, DOI 10.17487/RFC7638, , <https://www.rfc-editor.org/info/rfc7638>.
[RYU]
"Ryu 부동소수점 숫자 직렬화 알고리즘", commit 27d3c55, , <https://github.com/ulfjack/ryu>.
[V8]
Google LLC, "V8이란 무엇인가?", <https://v8.dev/>.
[XMLDSIG]
W3C, "XML Signature Syntax and Processing Version 1.1", W3C Recommendation, , <https://www.w3.org/TR/xmldsig-core1/>.

부록 A. ECMAScript 샘플 정규화기

아래는 ECMAScript 기반 시스템에서 사용하기 위한 JCS 정규화기의 예제이다.

  ////////////////////////////////////////////////////////////
  // Since the primary purpose of this code is highlighting //
  // the core of the JCS algorithm, error handling and      //
  // UTF-8 generation were not implemented.                 //
  ////////////////////////////////////////////////////////////
  var canonicalize = function(object) {

      var buffer = '';
      serialize(object);
      return buffer;

      function serialize(object) {
          if (object === null || typeof object !== 'object' ||
              object.toJSON != null) {
              /////////////////////////////////////////////////
              // Primitive type or toJSON, use "JSON"        //
              /////////////////////////////////////////////////
              buffer += JSON.stringify(object);

          } else if (Array.isArray(object)) {
              /////////////////////////////////////////////////
              // Array - Maintain element order              //
              /////////////////////////////////////////////////
              buffer += '[';
              let next = false;
              object.forEach((element) => {
                  if (next) {
                      buffer += ',';
                  }
                  next = true;
                  /////////////////////////////////////////
                  // Array element - Recursive expansion //
                  /////////////////////////////////////////
                  serialize(element);
              });
              buffer += ']';

          } else {
              /////////////////////////////////////////////////
              // Object - Sort properties before serializing //
              /////////////////////////////////////////////////
              buffer += '{';
              let next = false;
              Object.keys(object).sort().forEach((property) => {
                  if (next) {
                      buffer += ',';
                  }
                  next = true;
                  /////////////////////////////////////////////
                  // Property names are strings, use "JSON"  //
                  /////////////////////////////////////////////
                  buffer += JSON.stringify(property);
                  buffer += ':';
                  //////////////////////////////////////////
                  // Property value - Recursive expansion //
                  //////////////////////////////////////////
                  serialize(object[property]);
              });
              buffer += '}';
          }
      }
  };

부록 B. 숫자 직렬화 샘플

다음 표는 일부 경계 사례를 포함하여 ECMAScript 호환 숫자 직렬화 샘플 집합을 담고 있다. "IEEE 754" 열은 "Number" 데이터 타입의 내부 ECMAScript 표현을 가리키며, 이는 64비트(배정밀도) 값을 사용하는 IEEE 754 [IEEE754] 표준에 기반하고, 여기서는 16진수로 표현되어 있다.

Table 1: ECMAScript 호환 JSON 숫자 직렬화 샘플
IEEE 754 JSON 표현 설명
0000000000000000 0 Zero
8000000000000000 0 Minus zero
0000000000000001 5e-324 Min pos number
8000000000000001 -5e-324 Min neg number
7fefffffffffffff 1.7976931348623157e+308 Max pos number
ffefffffffffffff -1.7976931348623157e+308 Max neg number
4340000000000000 9007199254740992 Max pos int (1)
c340000000000000 -9007199254740992 Max neg int (1)
4430000000000000 295147905179352830000 ~2**68 (2)
7fffffffffffffff NaN (3)
7ff0000000000000 Infinity (3)
44b52d02c7e14af5 9.999999999999997e+22
44b52d02c7e14af6 1e+23
44b52d02c7e14af7 1.0000000000000001e+23
444b1ae4d6e2ef4e 999999999999999700000
444b1ae4d6e2ef4f 999999999999999900000
444b1ae4d6e2ef50 1e+21
3eb0c6f7a0b5ed8c 9.999999999999997e-7
3eb0c6f7a0b5ed8d 0.000001
41b3de4355555553 333333333.3333332
41b3de4355555554 333333333.33333325
41b3de4355555555 333333333.3333333
41b3de4355555556 333333333.3333334
41b3de4355555557 333333333.33333343
becbf647612f3696 -0.0000033333333333333333
43143ff3c1cb0959 1424953923781206.2 Round to even (4)

참고:

(1)
ECMAScript "JSON" 객체와 최대한 호환되도록, 참 정수로 해석될 값은 -9007199254740991부터 9007199254740991까지의 범위에 있는 것이 좋다(SHOULD). 그러나 애플리케이션에서 숫자가 어떻게 사용되는지는 JCS 알고리즘에 영향을 주지 않는다.
(2)
2**68과 같은 특정 정수 집합은 확장 정밀도를 가진 것으로 간주될 수 있지만, JCS/ECMAScript 숫자 직렬화 알고리즘은 이를 고려하지 않는다.
(3)
범위를 벗어난 값은 JSON에서 허용되지 않는다. Section 3.2.2.3을 참조하라.
(4)
이 숫자는 정확히 1424953923781206.25이지만, Section 3.2.2.3에 언급된 "Note 2" 규칙 이후 가장 가까운 짝수 값으로 절단 및 반올림된다.

JCS 숫자 직렬화기의 더 포괄적인 검증을 위해, 개발 포털 (부록 I 참조)에서 (현재) 제공되는 대량의 샘플 값 파일에 대해 테스트할 수 있다. 또 다른 옵션은 상당량의 무작위 IEEE 754 값을 생성하는 프로그램과 함께 V8 [V8]을 실시간 참조로 실행하는 것이다.

부록 C. "Wire Format"으로서의 정규화된 JSON

정규화 프로세스의 결과(Section 3.2.4 참조)는 완전히 유효한 JSON이므로 "Wire Format"으로도 사용할 수 있다. 그러나 대부분의 경우 JCS에 기반한 암호화 방식은 외부에서 제공된 JSON 데이터가 이미 정규화되어 있는지에 의존하지 않으므로, 이는 단지 선택사항일 뿐이다.

실제로 "JSON.stringify()"를 사용해 객체를 직렬화하는 ECMAScript 표준 방식은 속성이 생성되거나 수신된 순서로 유지되는 더 "논리적인" 형식을 생성한다. 아래 예제는 ECMAScript 표준 직렬화의 이점을 얻을 수 있는 주소 레코드를 보여준다.

  {
    "name": "John Doe",
    "address": "2000 Sunset Boulevard",
    "city": "Los Angeles",
    "zip": "90001",
    "state": "CA"
  }

정규화를 사용하면 위 속성은 "address", "city", "name", "state", "zip" 순서로 출력되며, 이는 사람(개발자 또는 기술 지원)의 관점에서 데이터에 모호함을 더한다. 정규화는 또한 JSON 데이터를 한 줄의 텍스트로 변환하므로, 디버깅과 로깅에는 덜 이상적일 수 있다.

부록 D. 큰 숫자 다루기

JSON 숫자 타입과 관련된 여러 문제가 있으며, 여기서는 다음 샘플 객체로 이를 설명한다.

  {
    "giantNumber": 1.4e+9999,
    "payMeThis": 26000.33,
    "int64Max": 9223372036854775807
  }

위 샘플이 JSON [RFC8259]을 준수하더라도, 애플리케이션은 보통 "giantNumber"와 "int64Max"를 저장하기 위해 서로 다른 네이티브 데이터 타입을 사용한다. 또한 "payMeThis"와 같은 금전 데이터는 십진 산술과 관련된 반올림 문제 때문에 부동소수점 데이터 타입에 의존하지 않을 것으로 추정된다.

JSON 숫자 타입의 이러한 종류의 "오버로딩"을 처리하는 확립된 방식(적어도 확장 가능한 방식)은 매핑 메커니즘을 통하는 것이며, 이는 파서가 속성 이름을 기반으로 각기 다른 속성에 대해 무엇을 해야 하는지 지시한다. 그러나 이는 원래의 다소 제한된 JavaScript 맥락 밖에서 JSON 숫자 타입을 사용하는 가치를 크게 제한한다. ECMAScript "JSON" 객체도 JSON 숫자 타입에 대한 매핑을 지원하지 않는다.

위와 같은 이유로, 현재 JSON 생태계에서 자연스러운 위치를 갖지 않는 숫자는 JSON 문자열 타입을 사용하여 감싸야 한다 (MUST). 이는 개방형 시스템에서 사실상의 표준에 가깝다. 이는 부록 E에 설명된 "DateTime" 객체처럼 JSON에서 직접 지원하지 않는 다른 데이터 타입에도 적용된다.

JSON 문자열 타입을 사용하는 시스템의 도움을 받으면, 예를 들어 다음과 같은 프로그래밍 방식이든

  var obj = JSON.parse('{"giantNumber": "1.4e+9999"}');
  var biggie = new BigNumber(obj.giantNumber);

OpenAPI [OPENAPI]와 같은 선언적 방식이든, JCS는 ECMAScript를 사용할 때를 포함하여 애플리케이션에 제한을 부과하지 않는다.

부록 E. 문자열 하위 타입 처리

JSON에서 제공되는 데이터 타입 집합이 제한되어 있기 때문에, JSON 문자열 타입은 일반적으로 하위 타입을 담는 데 사용된다. 이는 JSON 파싱 방법에 따라 상호운용성 문제로 이어질 수 있으며, 더 넓은 독자를 대상으로 하는 JCS 준수 애플리케이션은 이를 처리해야 한다(MUST).

스키마 설계자가 "big" 속성을 "BigInt" 하위 타입을 담도록, "time" 속성을 "DateTime" 하위 타입을 담도록 할당했으며, "val"은 JCS를 준수하는 JSON 숫자라고 가정한 JSON 객체를 파싱하려 한다고 하자. 다음 예제는 그러한 객체를 보여준다.

  {
    "time": "2019-01-28T07:45:10Z",
    "big": "055",
    "val": 3.5
  }

이 객체의 파싱은 다음 ECMAScript 문장으로 수행할 수 있다.

  var object = JSON.parse(JSON_object_featured_as_a_string);

파싱 후 실제 데이터를 추출할 수 있으며, 하위 타입의 경우에는 파싱 프로세스의 결과(ECMAScript 객체)를 입력으로 사용하는 변환 단계도 포함된다.

  ... = new Date(object.time); // Date object
  ... = BigInt(object.big);    // Big integer
  ... = object.val;            // JSON/JS number

"BigInt" 데이터 타입은 현재 V8 [V8]에서만 네이티브로 지원된다는 점에 유의하라.

부록 A의 샘플 코드를 사용하여 "object"를 정규화하면 다음 문자열이 반환된다.

  {"big":"055","time":"2019-01-28T07:45:10Z","val":3.5}

이것은 (JCS와 관련해서는) 기술적으로 올바르지만, JSON 데이터를 파싱하는 또 다른 방법이 있으며, 이는 아래에 보인 것처럼 ECMAScript와 함께 사용할 수도 있다.

  // "BigInt" requires the following code to become JSON serializable
  BigInt.prototype.toJSON = function() {
      return this.toString();
  };

  // JSON parsing using a "stream"-based method
  var object = JSON.parse(JSON_object_featured_as_a_string,
      (k,v) => k == 'time' ? new Date(v) : k == 'big' ? BigInt(v) : v
  );

이제 부록 A의 정규화기를 "object"에 적용하면 다음 문자열이 생성된다.

  {"big":"55","time":"2019-01-28T07:45:10.000Z","val":3.5}

이 경우 "big" 및 "time"의 문자열 인수가 원본과 비교해 변경되어, JCS에 의존하는 애플리케이션이 실패하게 만들 것으로 추정된다.

이러한 차이가 발생하는 이유는 스트림 기반 및 스키마 기반 JSON 파서에서 원래 문자열 인수가 보통 즉석에서 네이티브 하위 타입으로 대체되며, 직렬화될 때 다른 플랫폼 의존적 패턴을 보일 수 있기 때문이다.

즉, 스트림 기반 및 스키마 기반 파싱은 하위 타입을 "순수한" (불변) JSON 문자열 타입으로 취급해야 하며(MUST), 지정된 네이티브 타입으로의 실제 변환은 이후 단계에서 수행해야 한다. Go, Java, C#과 같은 현대 프로그래밍 플랫폼에서는 어노테이션, getter, setter를 결합하여 적당한 노력으로 이를 달성할 수 있다. 아래는 JSON 객체로 직렬화 가능한 클래스 일부를 보여주는 C#/Json.NET 예제이다.

  // The "pure" string solution uses a local
  // string variable for JSON serialization while
  // exposing another type to the application
  [JsonProperty("amount")]
  private string _amount;

  [JsonIgnore]
  public decimal Amount {
      get { return decimal.Parse(_amount); }
      set { _amount = value.ToString(); }
  }

애플리케이션에서 "Amount"는 다른 속성과 마찬가지로 접근할 수 있지만, JSON 컨텍스트에서는 실제로 따옴표로 묶인 문자열로 표현된다.

참고: 위 예제는 I-JSON이 암시하는 숫자 데이터 제약도 다룬다 (C#의 "decimal" 데이터 타입은 IEEE 754 배정밀도와 비교해 상당히 다른 특성을 가진다).

E.1. 배열의 하위 타입

JSON 배열 구성은 임의의 JSON 데이터 타입 혼합을 허용하므로, 하위 타입을 처리하기 위해 어차피 사용자 정의 파싱 및 직렬화 코드가 필요할 수 있다.

부록 F. 구현 지침

최적의 해결책은 JCS 지원을 JSON 직렬화기에 직접 통합하는 것이다(파서는 변경할 필요가 없다). 즉, 정규화는 JSON 직렬화기의 추가 "모드"일 뿐이다. 그러나 현재는 그렇지 않다. 다행히 JCS 지원은 기존 JSON 직렬화기에 대한 후처리기로 동작하는 외부 제공 정규화기 소프트웨어를 통해 도입될 수 있다. 이 구성은 또한 JCS 구현자가 기본 데이터가 JSON에서 어떻게 표현되어야 하는지를 다룰 필요를 덜어준다.

후처리기 개념은 다음과 같은 서명 생성 방식을 가능하게 한다.

  1. 서명할 데이터를 생성한다.
  2. 기존 JSON 도구를 사용하여 데이터를 직렬화한다.
  3. 외부 정규화기가 직렬화된 데이터를 처리하고 정규화된 결과 데이터를 반환하도록 한다.
  4. 정규화된 데이터에 서명한다.
  5. 결과 서명 값을 지정된 서명 속성을 통해 원래 JSON 데이터에 추가한다.
  6. 기존 JSON 도구를 사용하여 완료된(이제 서명된) JSON 객체를 직렬화한다.

호환되는 서명 검증 방식은 다음과 같다.

  1. 기존 JSON 도구를 사용하여 서명된 JSON 데이터를 파싱한다.
  2. 지정된 서명 속성에서 서명 값을 읽고 저장한다.
  3. 파싱된 JSON 객체에서 서명 속성을 제거한다.
  4. 기존 JSON 도구를 사용하여 남은 JSON 데이터를 직렬화한다.
  5. 외부 정규화기가 직렬화된 데이터를 처리하고 정규화된 결과 데이터를 반환하도록 한다.
  6. 서명 생성에 사용된 알고리즘과 키를 사용하여 정규화된 데이터가 저장된 서명 값과 일치하는지 검증한다.

위와 같은 정규화기는 사실상 "필터"일 뿐이며, 매우 다양한 암호화 방식과 함께 사용할 수 있을 가능성이 있다.

JCS 지원이 통합된 JSON 직렬화기를 사용하면, 정규화 단계 전에 수행되는 직렬화를 두 프로세스 모두에서 제거할 수 있다.

부록 G. 오픈 소스 구현

다음 오픈 소스 구현은 JCS와 호환되는 것으로 검증되었다.

부록 H. 기타 JSON 정규화 노력

"Canonical JSON"을 만들려는 다른 노력들이 존재하며(또는 존재해 왔으며), 아래는 그중 일부 URL 목록이다.

나열된 노력들은 모두 텍스트 수준의 JSON-to-JSON 변환을 기반으로 한다. 텍스트 수준 정규화의 주요 특징은 사용되는 JSON의 종류에 대해 중립적으로 만들 수 있다는 것이다. 그러나 이러한 방식은 JSON 파싱 프로세스에 중대한 변경을 암시하므로, 채택의 장애물이 될 가능성이 높다. 특정 JSON 및 애플리케이션 제약을 감수하더라도, JCS는 기존 JSON 도구와 호환되도록 설계되었다.

부록 I. 개발 포털

JCS 명세는 현재 다음 위치에서 개발되고 있다. <https://github.com/cyberphone/ietf-json-canon>.

JCS 소스 코드와 광범위한 테스트 데이터는 다음 위치에서 제공된다. <https://github.com/cyberphone/json-canonicalization>.

감사의 말

ECMAScript 숫자 직렬화를 기반으로 하는 것은 원래 James Manger가 제안했다. 이는 결국 JSON 기본값에 대한 전체 ECMAScript 직렬화 방식의 채택으로 이어졌다.

이 명세에 귀중한 의견을 제공한 다른 사람들에는 Scott Ananian, Tim Bray, Ben Campbell, Adrian Farell, Richard Gibson, Bron Gondwana, John-Mark Gurney, Mike Jones, John Levine, Mark Miller, Matthew Miller, Mark Nottingham, Mike Samuel, Jim Schaad, Robert Tupelo-Schneck, and Michal Wadas가 포함된다.

실제 세계의 개념 검증을 수행하는 데 있어, Ulf Adams, Tanner Gooding, and Remy Oudompheng가 제공한 숫자 직렬화용 소프트웨어와 지원이 매우 도움이 되었다.

저자 주소

Anders Rundgren
Independent
Montpellier
France
Bret Jordan
Broadcom
1320 Ridder Park Drive
San Jose, CA 95131
United States of America
Samuel Erdtman
Spotify AB
Birger Jarlsgatan 61, 4tr
SE-113 56 Stockholm
Sweden