Post

OER

OER

OER과 COER

OER(Octet Encoding Rules)은 ITU-T X.696에 정의된 ASN.1 인코딩 규칙이다. 이름 그대로 모든 값을 옥텟(octet, 8비트) 단위로 정렬해서 인코딩하는 것이 특징이다.

  • BER/DER처럼 모든 값에 Tag와 Length를 붙이지 않기 때문에 크기가 작다.
  • PER처럼 비트 단위로 쪼개서 압축하지 않기 때문에 비트 연산이 거의 필요 없어 인코딩/디코딩이 빠르다.
  • 즉, OER은 “BER보다 작고, PER보다 빠른” 중간 지점의 인코딩이라고 볼 수 있다.

OER은 두 가지 변형이 있다.

구분설명
BASIC-OER기본 OER. 일부 값은 여러 가지 방식으로 인코딩될 수 있다. (예: BOOLEAN TRUE를 0이 아닌 아무 값으로 표현)
CANONICAL-OER (COER)하나의 값이 반드시 하나의 바이트열로만 인코딩되도록 제약을 추가한 OER

COER에서 추가로 지켜야 하는 대표적인 규칙은 다음과 같다.

  • BOOLEAN TRUE는 반드시 0xFF
  • Length는 가능한 가장 짧은 형식(127 이하면 short form)과 최소 바이트 수로 인코딩
  • DEFAULT 값과 같은 필드는 인코딩하지 않음 (생략)
  • 이름이 붙은 비트(named bit)를 가진 BIT STRING은 끝의 0 비트를 제거

V2X에서 COER을 사용하는 이유

IEEE 1609.2(V2X 보안)와 IEEE 1609.2.1(SCMS 인증서 관리)은 모든 메시지를 COER로 인코딩한다.

  1. 서명 검증 : 서명은 “값”이 아니라 “인코딩된 바이트”에 대해 계산된다. 송신 측이 TRUE를 0x01로, 수신 측이 다시 인코딩할 때 0xFF로 만든다면 해시 값이 달라져 서명 검증이 실패한다. 그래서 인코딩 결과가 유일한(canonical) COER이 필요하다.
  2. 속도 : 차량은 초당 수백~수천 개의 메시지를 받아 서명을 검증해야 한다. 바이트 정렬된 OER은 디코딩이 빠르고 하드웨어 구현도 쉽다.
  3. 크기 : 무선 대역폭이 제한적인 V2X 환경에서 BER/DER보다 훨씬 작은 메시지를 만들 수 있다.

OER의 구성

BER/DER은 모든 값이 TLV(Tag-Length-Value) 구조를 가지지만, OER은 그렇지 않다. OER은 필요한 경우에만 Tag와 Length를 붙이고, 나머지는 Value만 인코딩한다.

  • TAG : CHOICE에서 어떤 대안(alternative)이 선택되었는지 표시할 때만 사용한다.
  • LENGTH : 크기가 정해지지 않은(가변 크기) 값에만 사용한다. (가변 OCTET STRING, 범위가 없는 INTEGER, open type 등)
  • VALUE : 실제 값. 고정 크기 값은 Tag/Length 없이 Value만 인코딩된다.

예를 들어 INTEGER (0..255)는 크기가 항상 1바이트로 정해져 있으므로 Tag도 Length도 없이 값 1바이트만 인코딩된다. 디코더는 ASN.1 스키마를 알고 있으므로 “여기는 1바이트를 읽으면 된다”는 것을 알 수 있기 때문이다. (대신 스키마 없이는 OER 바이트를 해석할 수 없다.)

TAG

Tag는 CHOICE 값을 인코딩할 때 선택된 대안을 식별하기 위해 사용된다.

First Octet 규칙

첫 번째 옥텟은 다음과 같이 구성된다.

1
2
3
4
  8   7   6   5   4   3   2   1
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ Class │      Tag Number       │
└───┴───┴───┴───┴───┴───┴───┴───┘
  • bit 8~7 : Tag Class

    값Class
    00UNIVERSAL
    01APPLICATION
    10CONTEXT-SPECIFIC
    11PRIVATE
  • bit 6~1 : Tag Number

    • Tag 번호가 0~62이면 bit 6~1에 그대로 표시한다.
    • Tag 번호가 63 이상이면 bit 6~1을 모두 1(111111)로 채우고, 실제 번호는 뒤따르는 옥텟(Subsequent Octets)에 인코딩한다.

BER과 달리 OER의 Tag에는 primitive/constructed 비트가 없어서 Tag Number에 6비트를 쓸 수 있다.

예시

TagClass 비트Number 비트인코딩
[0] (context)1000000080
[2] (context)1000001082
[APPLICATION 3]0100001143
[PRIVATE 5]11000101C5

AUTOMATIC TAGS를 사용하는 모듈(IEEE 1609.2 등)에서는 CHOICE의 대안들에 자동으로 [0], [1], [2]… context-specific tag가 붙기 때문에, 첫 번째 대안은 80, 두 번째는 81, 세 번째는 82로 인코딩된다.

Subsequent Octets 규칙

Tag 번호가 63 이상일 때 사용한다.

  • Tag 번호를 7비트씩 나누어 big-endian 순서로 인코딩한다.
  • 마지막 옥텟을 제외한 모든 옥텟의 bit 8은 1, 마지막 옥텟의 bit 8은 0으로 설정한다. (계속 여부 표시)
  • 첫 번째 subsequent octet의 bit 7~1은 모두 0이면 안 된다. (불필요한 앞자리 0 금지)

예시

1
2
3
4
5
6
7
8
9
10
11
[100] (context-specific)
  First Octet       : 10 111111            → BF
  100 = 0b1100100   : 0 1100100 (마지막)    → 64
  결과 : BF 64

[200] (context-specific)
  First Octet       : 10 111111            → BF
  200 = 0b11001000  → 7비트씩 : 0000001 | 1001000
                      1 0000001 (계속)     → 81
                      0 1001000 (마지막)   → 48
  결과 : BF 81 48

Length

Length determinant는 뒤따르는 값이 몇 옥텟인지를 나타낸다.

Short form (길이 0~127)

  • 1 옥텟으로 표현한다.
  • bit 8은 0, bit 7~1에 길이를 그대로 표시한다.

Long form (길이 128 이상)

  • 첫 옥텟의 bit 8은 1, bit 7~1에는 길이를 표현하는 데 필요한 옥텟 수를 표시한다.
  • 그 뒤에 실제 길이를 unsigned big-endian으로, 최소 옥텟 수로 인코딩한다.

예시

길이인코딩설명
000short form
505short form
1277Fshort form 최대값
12881 80long form, 길이 1옥텟
20081 C8long form, 길이 1옥텟
100082 03 E8long form, 길이 2옥텟

COER에서는 127 이하의 길이를 long form(81 05 등)으로 인코딩하면 안 되고, 길이 옥텟 앞에 불필요한 00을 붙여도 안 된다.

Encoding 규칙

이제 타입별로 실제 인코딩 규칙을 살펴보자. 아래 예시는 모두 COER 기준이다.

Bitstring

고정 크기

SIZE 제약이 하나의 값으로 고정되어 있으면(예: SIZE(10)) Length 없이 비트만 인코딩한다.

  • 비트를 앞에서부터 옥텟에 채운다. (첫 비트가 첫 옥텟의 bit 8)
  • 마지막 옥텟의 남는 비트는 0으로 채운다.
  • 따라서 크기는 ceil(n / 8) 옥텟이다.
1
2
3
4
5
6
7
BitsA ::= BIT STRING (SIZE(4))
값 : '1010'B
→ 1010 0000 → A0

BitsB ::= BIT STRING (SIZE(10))
값 : '1010101010'B
→ 10101010 | 10 000000 → AA 80

가변 크기

크기가 고정되지 않은 경우 다음과 같이 인코딩한다.

1
[Length] [Initial Octet: 사용하지 않는 비트 수(0~7)] [비트들]
  • Length : Initial Octet을 포함한 전체 옥텟 수
  • Initial Octet : 마지막 옥텟에서 사용하지 않는(패딩된) 비트 수
1
2
3
4
5
6
7
8
9
Bits ::= BIT STRING
값 : '1010'B
→ Length       : 02  (initial octet 1 + 비트 1옥텟)
→ Initial Octet: 04  (마지막 옥텟에서 4비트 미사용)
→ 비트         : A0  (1010 0000)
결과 : 02 04 A0

값 : ''B (빈 비트 스트링)
결과 : 01 00

Octetstring Value

고정 크기

SIZE가 고정되어 있으면 Length 없이 옥텟을 그대로 인코딩한다.

1
2
3
HashedId8 ::= OCTET STRING (SIZE(8))     -- IEEE 1609.2
값 : '0102030405060708'H
결과 : 01 02 03 04 05 06 07 08

IEEE 1609.2의 HashedId8, HashedId3, 고정 길이 서명/공개키 값 등이 이 방식으로 인코딩된다.

가변 크기

크기가 고정되지 않으면 Length + 옥텟으로 인코딩한다.

1
2
3
4
5
6
Opaque ::= OCTET STRING                  -- IEEE 1609.2
값 : 'DEADBEEF'H
결과 : 04 DE AD BE EF

값 : 200바이트 데이터
결과 : 81 C8 [200바이트]

SIZE(1..32)처럼 범위가 있더라도 크기가 고정이 아니면 가변 크기로 취급해 Length를 붙인다.

UTF8String

UTF8String은 문자마다 바이트 수가 달라지기 때문에 항상 Length + UTF-8 바이트로 인코딩한다.

1
2
3
4
5
6
7
Name ::= UTF8String

값 : "V2X"
결과 : 03 56 32 58

값 : "한"   (U+D55C → UTF-8 : ED 95 9C)
결과 : 03 ED 95 9C

Length는 문자 수가 아니라 바이트 수라는 점에 주의하자.

참고로 IA5String, PrintableString, VisibleString처럼 한 문자의 크기가 고정된 문자열 타입은 SIZE가 고정되어 있으면 Length 없이 인코딩되고, 그렇지 않으면 Length가 붙는다.

NULL Value

NULL은 아무 바이트도 인코딩하지 않는다. (0 옥텟)

값이 존재한다는 사실 자체가 의미이기 때문에, 보통 CHOICE의 대안으로 쓰인다.

1
2
3
4
5
6
7
8
9
SignerIdentifier ::= CHOICE {            -- IEEE 1609.2
  digest       HashedId8,                -- [0]
  certificate  SequenceOfCertificate,    -- [1]
  self         NULL,                     -- [2]
  ...
}

값 : self : NULL
결과 : 82        -- Tag [2]만 있고 NULL 값은 0바이트

Integer Value

INTEGER는 제약 조건(범위) 에 따라 인코딩 방식이 달라진다. OER은 고정 크기를 1, 2, 4, 8 옥텟 중에서만 선택한다는 점이 특징이다.

unsigned

하한(lower bound)이 0 이상이면 unsigned로 인코딩한다.

범위 (상한)인코딩 크기
ub ≤ 2551 옥텟
ub ≤ 65,5352 옥텟
ub ≤ 4,294,967,2954 옥텟
ub ≤ 18,446,744,073,709,551,6158 옥텟
상한 없음 / 그보다 큼Length + 최소 옥텟 (unsigned)
1
2
3
4
5
6
7
8
9
10
11
Uint8  ::= INTEGER (0..255)
값 : 200          → C8

Uint16 ::= INTEGER (0..65535)
값 : 1000         → 03 E8

Uint24 ::= INTEGER (0..16777215)   -- 3옥텟이면 충분하지만
값 : 1000         → 00 00 03 E8    -- 1/2/4/8 중 4옥텟 사용

Count ::= INTEGER (0..MAX)          -- 상한 없음
값 : 1000         → 02 03 E8       -- Length 02 + 값

signed

하한이 음수이거나 하한이 없으면 2의 보수(two’s complement)로 인코딩한다.

범위인코딩 크기
-128 ≤ lb, ub ≤ 1271 옥텟
-32,768 ≤ lb, ub ≤ 32,7672 옥텟
-2^31 ≤ lb, ub ≤ 2^31-14 옥텟
-2^63 ≤ lb, ub ≤ 2^63-18 옥텟
범위 없음 / 그보다 큼Length + 최소 옥텟 (2의 보수)
1
2
3
4
5
6
7
8
9
10
Int8 ::= INTEGER (-128..127)
값 : 100          → 64
값 : -100         → 9C

Int16 ::= INTEGER (-32768..32767)
값 : -1000        → FC 18

INTEGER (제약 없음)
값 : 100          → 01 64
값 : 128          → 02 00 80   -- 0x80만 쓰면 음수(-128)가 되므로 00 추가

-1

-1은 범위에 따라 인코딩이 어떻게 달라지는지 보기 좋은 예시다.

1
2
3
4
INTEGER (-128..127)          → FF
INTEGER (-32768..32767)      → FF FF
INTEGER (-2^31..2^31-1)      → FF FF FF FF
INTEGER (제약 없음)           → 01 FF

같은 -1이라도 스키마에 따라 1~4바이트, 또는 Length가 붙은 형태로 인코딩된다. 그래서 OER은 스키마를 정확히 알아야만 디코딩할 수 있다.

참고 : INTEGER (0..255, ...)처럼 제약 조건에 확장 마커(...)가 있으면 OER은 그 범위를 무시하고 제약 없는 INTEGER처럼(Length + 2의 보수) 인코딩한다.

Enumerated Value

ENUMERATED는 PER과 달리 인덱스가 아니라 열거값에 지정된 숫자 그 자체를 인코딩한다.

short form

값이 0~127이면 1 옥텟으로 인코딩한다. (bit 8 = 0)

1
2
3
4
Color ::= ENUMERATED { red(0), green(1), blue(5) }

값 : green   → 01
값 : blue    → 05

long form

값이 음수이거나 127보다 크면 long form을 사용한다.

  • 첫 옥텟 : bit 8 = 1, bit 7~1 = 뒤따르는 옥텟 수
  • 뒤따르는 옥텟 : 값을 2의 보수로 최소 옥텟에 인코딩
1
2
3
4
5
Code ::= ENUMERATED { minus(-1), big(128), bigger(1000) }

값 : minus (-1)    → 81 FF
값 : big (128)     → 82 00 80
값 : bigger (1000) → 82 03 E8

COER에서는 0~127 범위의 값을 long form으로 인코딩하면 안 된다.

Choice Value

CHOICE는 선택된 대안의 Tag + 값으로 인코딩한다.

root alternative

확장 마커(...) 앞에 정의된 대안(root alternative)은 Tag + 값의 인코딩을 그대로 붙인다.

1
2
3
4
5
6
7
8
9
10
11
12
Ieee1609Dot2Content ::= CHOICE {         -- IEEE 1609.2 (AUTOMATIC TAGS)
  unsecuredData             Opaque,       -- [0]
  signedData                SignedData,   -- [1]
  encryptedData             EncryptedData,-- [2]
  signedCertificateRequest  Opaque,       -- [3]
  ...
}

값 : unsecuredData : 'DEADBEEF'H
→ Tag    : 80
→ Opaque : 04 DE AD BE EF
결과 : 80 04 DE AD BE EF

extension alternative

확장 마커 뒤에 추가된 대안(extension alternative)은 Tag + open type 으로 인코딩한다. open type은 값의 인코딩 앞에 Length를 한 번 더 붙인 것이다.

이전 버전의 디코더는 새로 추가된 대안의 타입을 모르지만, Length를 보고 해당 바이트를 건너뛸 수 있다. 이것이 확장성(extensibility)이 동작하는 원리다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Example ::= CHOICE {
  a  INTEGER (0..255),      -- [0]
  b  BOOLEAN,               -- [1]
  ...,
  c  INTEGER (0..255)       -- [2]  확장 대안
}

값 : a : 200
결과 : 80 C8

값 : c : 200
→ Tag       : 82
→ Open type : 01 (Length) + C8 (값)
결과 : 82 01 C8

Sequence Value

overall structure

SEQUENCE는 다음과 같은 구조로 인코딩된다.

1
[Preamble] [Root 컴포넌트들] [Extension part]
  • Preamble : 확장 여부와 OPTIONAL/DEFAULT 필드의 존재 여부를 나타내는 비트맵
  • Root 컴포넌트 : 확장 마커 앞에 정의된 필드들을 순서대로 인코딩 (Tag 없음)
  • Extension part : 확장 필드가 존재할 때만 추가
preamble

Preamble은 다음 비트들로 구성된다.

  1. Extension bit (SEQUENCE에 ...가 있을 때만) : 확장 필드가 하나라도 있으면 1, 없으면 0
  2. Presence bit : root의 OPTIONAL / DEFAULT 필드마다 1비트씩, 정의 순서대로. 값이 있으면 1, 없으면 0

비트는 앞에서부터 채우고, 옥텟 단위로 맞추기 위해 남는 비트는 0으로 채운다.

1
2
3
4
  bit 8        bit 7         bit 6         ...
┌──────────┬────────────┬────────────┬─────────────┐
│ Ext bit  │ Optional 1 │ Optional 2 │ ... padding │
└──────────┴────────────┴────────────┴─────────────┘

extension part

Extension bit가 1이면 root 컴포넌트 뒤에 extension part가 붙는다.

1
[Extension Presence Bitmap] [확장 필드 1 (open type)] [확장 필드 2 (open type)] ...
  • Extension Presence Bitmap : 확장 필드마다 1비트씩 존재 여부를 표시한다. 가변 크기 BIT STRING과 같은 방식(Length + 미사용 비트 수 + 비트)으로 인코딩된다.
  • 확장 필드 : 존재하는 확장 필드만 open type(Length + 값)으로 인코딩한다.

optional

OPTIONAL 필드가 있는 예시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Msg ::= SEQUENCE {
  a  INTEGER (0..255),
  b  INTEGER (0..255) OPTIONAL,
  c  INTEGER (0..255) OPTIONAL
}

값 : { a 1, c 3 }      -- b 생략
→ Preamble : b=0, c=1 → 01 000000 → 40
→ a        : 01
→ c        : 03
결과 : 40 01 03

값 : { a 1, b 2, c 3 }
→ Preamble : 11 000000 → C0
결과 : C0 01 02 03

no preamble

확장 마커도 없고 OPTIONAL/DEFAULT 필드도 없으면 Preamble 자체가 없다. 각 필드를 순서대로 이어 붙이기만 한다.

1
2
3
4
5
6
7
Point ::= SEQUENCE {
  x  INTEGER (0..255),
  y  INTEGER (0..65535)
}

값 : { x 1, y 2 }
결과 : 01 00 02

실제 IEEE 1609.2의 최상위 구조도 이 경우에 해당한다.

1
2
3
4
5
6
7
8
9
Ieee1609Dot2Data ::= SEQUENCE {
  protocolVersion  Uint8(3),
  content          Ieee1609Dot2Content
}

값 : { protocolVersion 3, content unsecuredData : 'DEADBEEF'H }
→ protocolVersion : 03
→ content         : 80 04 DE AD BE EF
결과 : 03 80 04 DE AD BE EF

그래서 1609.2 메시지를 hex로 보면 보통 03 80(unsecuredData), 03 81(signedData), 03 82(encryptedData)로 시작한다. 덤프를 볼 때 유용한 팁이다.

no extension value

확장 마커가 있지만 확장 필드가 없는 경우, Preamble의 Extension bit만 0이 되고 extension part는 붙지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Msg2 ::= SEQUENCE {
  a  INTEGER (0..255),
  b  INTEGER (0..255) OPTIONAL,
  ...
}

값 : { a 1, b 2 }
→ Preamble : ext=0, b=1 → 0 1 000000 → 40
→ a, b     : 01 02
결과 : 40 01 02

값 : { a 1 }
→ Preamble : ext=0, b=0 → 00
결과 : 00 01

root part

확장 필드가 있는 경우를 보자. 먼저 root part까지의 인코딩이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Msg3 ::= SEQUENCE {
  a  INTEGER (0..255),
  b  INTEGER (0..255) OPTIONAL,
  ...,
  c  INTEGER (0..255),
  d  INTEGER (0..65535)
}

값 : { a 1, b 2, d 1000 }     -- 확장 필드 c는 없고 d만 있음

→ Preamble : ext=1, b=1 → 1 1 000000 → C0
→ a        : 01
→ b        : 02
Root part까지 : C0 01 02

extension part

이어서 extension part를 인코딩한다.

1
2
3
4
5
6
7
8
9
10
11
12
→ Extension Presence Bitmap
    확장 필드 2개 (c=0, d=1) → 비트 '01'
    Length        : 02   (initial octet + 비트 1옥텟)
    Unused bits   : 06   (8비트 중 2비트만 사용)
    Bitmap        : 40   (01 000000)
    → 02 06 40

→ d (open type)
    값 1000 → 03 E8, Length 02
    → 02 03 E8

결과 : C0 01 02 | 02 06 40 | 02 03 E8

이전 버전 디코더(c, d를 모르는 디코더)도 Preamble에서 확장 필드가 있다는 것을 확인하고, Bitmap으로 확장 필드 개수를 알고, 각 open type의 Length만큼 건너뛰면 되므로 root part는 정상적으로 디코딩할 수 있다.

sequence of value

SEQUENCE OF는 요소 개수(quantity) + 각 요소의 인코딩으로 구성된다.

요소 개수는 다음과 같이 인코딩한다.

1
[개수를 표현하는 옥텟 수 (Length)] [개수 (unsigned, 최소 옥텟)]
1
2
3
4
5
6
7
8
9
10
11
12
13
Numbers ::= SEQUENCE OF INTEGER (0..255)

값 : { 1, 2, 3 }
→ 개수 : 01 03     (개수 3을 1옥텟으로 표현)
→ 요소 : 01 02 03
결과 : 01 03 01 02 03

값 : { }  (빈 리스트)
결과 : 01 00

값 : 300개의 요소
→ 개수 : 02 01 2C  (300 = 0x012C, 2옥텟)
결과 : 02 01 2C [요소 300개]

Length가 전체 바이트 수가 아니라 요소의 개수라는 점이 OCTET STRING과 다르다. IEEE 1609.2의 SequenceOfCertificate(인증서 체인), SequenceOfPsidSsp(권한 목록) 등이 이 방식으로 인코딩된다.

정리

타입인코딩
고정 크기 (BIT/OCTET STRING, 범위 있는 INTEGER)Value만
가변 크기 (OCTET STRING, UTF8String, 범위 없는 INTEGER)Length + Value
NULL0 옥텟
ENUMERATED0~127은 1옥텟, 그 외 long form
CHOICETag + Value (확장 대안은 Tag + Length + Value)
SEQUENCEPreamble(있을 때) + Root + Extension part(있을 때)
SEQUENCE OF개수(Length + Count) + 요소들

OER은 “스키마로 알 수 있는 정보는 인코딩하지 않는다”는 원칙으로 설계되어 있다. 그래서 크기가 작고 빠르지만, 반대로 ASN.1 스키마 없이는 바이트를 해석할 수 없다. V2X 메시지를 디버깅할 때는 항상 해당 1609.2 / 1609.2.1 ASN.1 모듈을 옆에 띄워두고 보는 것이 좋다.

This post is licensed under CC BY 4.0 by the author.