Created
April 13, 2021 00:12
-
-
Save iximeow/c2e1cd08465642c94ab8d0b24971ad6c to your computer and use it in GitHub Desktop.
asn.1 parsing in pure bash
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /bin/bash | |
asn1=$(echo -e "\x30\x13\x02\x01\x05\x16\x0e\x41\x6e\x79\x62\x6f\x64\x79\x20\x74\x68\x65\x72\x65\x3f") | |
#asn2=$(echo -e "\x00\x01\x02\x03") | |
asn2="" | |
offset=0 | |
# while [ $offset -lt ${#asn1} ]; do | |
# curr=${asn1:offset:1} | |
# printf "$offset: %02x\n" \'$curr | |
# case "$curr" in | |
# $(echo -e "")) | |
# echo "its null" | |
# ;; | |
# esac | |
# offset=$(($offset + 1)) | |
#done | |
function byte { | |
local chr; | |
LANG=C IFS= read -n 1 -d '' chr | |
retcode=$? | |
if [ -z "$chr" ]; then | |
echo -n "00" | |
else | |
printf '%02x' "'$chr" | |
fi | |
# printf '%02x\n' "'$chr" >&2 | |
return $retcode | |
} | |
# TODO: handle indefinite and reserved lengths. handle lengths longer than 8 octets. | |
# | |
# returns two values by ref from arguments: the length that was read, and the | |
# number of octets read to read that length. | |
# | |
# returns 0 if no error was encountered. | |
# returns 1 if end of input was reached. | |
# returns 2 on some unhandled cases. | |
function read_length_der { | |
local -n read_length_length=$1 | |
local -n read_length_read_size=$2 | |
octet="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
read_length_read_size=$((read_length_read_size + 1)) | |
bit8=$((0x${octet} >> 7)) | |
lower=$((0x${octet} & 127)) | |
if [ "$bit8" -eq "0" ]; then | |
read_length_length=$lower | |
return 0 | |
else | |
# echo "length is ${lower} octets" >&2 | |
if [ "$lower" -ge 8 ]; then | |
echo "unsupported long length ${lower}" >&2 | |
return 2 | |
fi | |
read_length_length=0 | |
while [ "$lower" -gt "0" ]; do | |
octet="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
read_length_read_size=$((read_length_read_size + 1)) | |
lower=$(($lower - 1)) | |
read_length_length=$((($read_length_length << 8) + 0x${octet})) | |
done | |
return 0 | |
fi | |
} | |
function read_octet_string_der { | |
local -n octet_string=$1 | |
local -n read_integer_read_size=$2 | |
local length=0 | |
length_header="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
length_header=$((0x$length_header)) | |
read_integer_read_size=$((read_integer_read_size + 1)) | |
if [ "$length_header" -gt 127 ]; then | |
# multi-byte length trailer | |
length_length=$(($length_header & 0x7f)) | |
for i in $(seq 1 $length_length); do | |
octet="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
read_integer_read_size=$((read_integer_read_size + 1)) | |
length=$((($length << 8) + 0x$octet)) | |
done | |
else | |
length=$(($length_header & 0x7f)) | |
fi | |
echo "octet string length is $length octets" >&2 | |
for i in $(seq 1 $length); do | |
octet="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
octet_string="$octet_string$octet" | |
done | |
return 0 | |
} | |
# TODO: this does not reject trailing bits properly!! | |
function read_bit_string_der { | |
local -n bit_string=$1 | |
local -n read_integer_read_size=$2 | |
local length=0 | |
length_header="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
length_header=$((0x$length_header)) | |
read_integer_read_size=$((read_integer_read_size + 1)) | |
if [ "$length_header" -gt 127 ]; then | |
# multi-byte length trailer | |
length_length=$(($length_header & 0x7f)) | |
for i in $(seq 1 $length_length); do | |
octet="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
read_integer_read_size=$((read_integer_read_size + 1)) | |
length=$((($length << 8) + 0x$octet)) | |
done | |
else | |
length=$(($length_header & 0x7f)) | |
fi | |
echo "bit string length is $length octets" >&2 | |
for i in $(seq 1 $length); do | |
octet="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
bit_string="$bit_string$octet" | |
done | |
return 0 | |
} | |
# NOTE: returns number as ASCII HEX to support arbitrarily-sized integers | |
function read_integer_der { | |
local -n number=$1 | |
local -n read_integer_read_size=$2 | |
local length=0 | |
read_length_der length read_integer_read_size | |
number="" | |
for i in $(seq 1 "$length"); do | |
octet="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
read_integer_read_size=$(($read_integer_read_size + 1)) | |
number="${octet}${number}" | |
done | |
return 0 | |
} | |
function read_printable_string_der { | |
read_string $1 $2 | |
} | |
function read_ia5string_der { | |
read_string $1 $2 | |
} | |
function read_utctime { | |
read_string $1 $2 | |
if [ "$?" -ne "0" ]; then | |
return $? | |
fi | |
local -n time=$1 | |
time="20$time" | |
return 0 | |
} | |
function read_string { | |
local -n string=$1 | |
local -n read_ia5string_read_size=$2 | |
local length=0 | |
read_length_der length read_ia5string_read_size | |
if [ "$?" -ne "0" ]; then | |
return 1 | |
fi | |
string="" | |
for i in $(seq 1 "$length"); do | |
octet="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
read_ia5string_read_size=$(($read_ia5string_read_size + 1)) | |
string="${string}$(printf "\x${octet}")" | |
done | |
return 0 | |
} | |
function read_object_identifier_der { | |
local -n read_oid_oid=$1 | |
local -n read_oid_read_size=$2 | |
res=() | |
reading=1 | |
local length=0 | |
read_length_der length read_oid_read_size | |
if [ "$?" -ne "0" ]; then | |
return 1 | |
fi | |
read_oid_oid="" | |
octet="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
read_oid_oid="$((0x$octet / 40)).$((0x$octet % 40))" | |
length=$(($length - 1)) | |
segment=0 | |
while [ "$length" -gt "0" ]; do | |
# echo "reading oid byte. ${length} remain" >&2 | |
octet="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
length=$(($length - 1)) | |
segment=$(($segment * 128 + (0x$octet & 0x7f))) | |
# bit 8 unset, we're done reading this segment | |
if [ "$((0x$octet))" -lt 128 ]; then | |
read_oid_oid="${read_oid_oid}.$segment" | |
segment=0 | |
# echo "current oid ${read_oid_oid}" >&2 | |
fi | |
done | |
return 0 | |
} | |
function read_composite { | |
local -n read_composite_read_size=$1 | |
local length=0 | |
read_length_der length read_composite_read_size | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
} | |
function read_item { | |
local -n read_item_read_size=$1 | |
tpe="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
read_item_read_size=$((read_item_read_size + 1)) | |
tag_class=$((0x${tpe} >> 6)) | |
pc=$(((0x${tpe} >> 5) & 1)) | |
tag_num=$(printf "%02x" $((0x${tpe} & 31))) | |
echo "tag class ${tag_class}" >&2 | |
echo "p/c? ${pc}" >&2 | |
if ! [ -z "$pc" ] && [ "$tag_class" -eq 2 ]; then | |
read_composite read_item_read_size | |
else | |
# echo "type tag $tag_num" >&2 | |
case "$tag_num" in | |
00) | |
echo "type tag: end of content" >&2 | |
return 0 | |
;; | |
01) | |
echo "type tag: boolean" >&2 | |
;; | |
02) | |
echo "type tag: integer" >&2 | |
local int=0 | |
read_integer_der int read_item_read_size | |
if [ "$?" -ne "0" ]; then | |
return 1 | |
fi | |
echo " - integer: ${int}" | |
;; | |
03) | |
echo "type tag: bit string" >&2 | |
local bits="" | |
local length_length=0 | |
read_bit_string_der bits length_length | |
if [ "$?" -ne "0" ]; then | |
return 1 | |
fi | |
echo " - bits: ${bits}" | |
;; | |
04) | |
# TODO: properly support octet strings. these are composite??? | |
echo "type tag: octet string" >&2 | |
local octets="" | |
local length_length=0 | |
read_octet_string_der octets length_length | |
if [ "$?" -ne "0" ]; then | |
return 1 | |
fi | |
echo " - octets: ${octets}" | |
;; | |
05) | |
echo "type tag: null" >&2 | |
length_length=0 | |
length=0 | |
read_length_der length length_length | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
if [ "$length" -ne "0" ]; then | |
echo "rejecting invalid null, length is $length" | |
return 2 | |
fi | |
read_item_read_size=$(($read_item_read_size + $length_length)) | |
return 0 | |
;; | |
06) | |
echo "type tag: object identifier" >&2 | |
local oid="" | |
read_object_identifier_der oid read_item_read_size | |
if [ "$?" -ne "0" ]; then | |
return 1 | |
fi | |
echo " - oid: ${oid}" | |
;; | |
07) | |
echo "type tag: object descriptor" >&2 | |
;; | |
08) | |
echo "type tag: external" >&2 | |
;; | |
09) | |
echo "type tag: real" >&2 | |
;; | |
0a) | |
echo "type tag: enumerated" >&2 | |
;; | |
0b) | |
echo "type tag: embedded pdv" >&2 | |
;; | |
0c) | |
echo "type tag: utf8string" >&2 | |
;; | |
0d) | |
echo "type tag: relative-oid" >&2 | |
;; | |
0e) | |
echo "type tag: time" >&2 | |
;; | |
0f) | |
echo "type tag: reserved" >&2 | |
return 3 | |
;; | |
10) | |
echo -n "type tag: sequence" >&2 | |
# TODO: handle 0xa0 tag in sequence (specifies a sequence of ???-type items?) | |
length_length=0 | |
length=0 | |
read_length_der length length_length | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
echo ", length: ${length}" >&2 | |
read_item_read_size=$(($read_item_read_size + $length_length)) | |
;; | |
11) | |
echo -n "type tag: set" >&2 | |
length="$(byte)" | |
if [ "$?" -eq "1" ]; then | |
return 1 | |
fi | |
echo ", length: ${length}" >&2 | |
;; | |
12) | |
;; | |
13) | |
echo "type tag printable string" >&2 | |
local str="" | |
read_printable_string_der str read_item_read_size | |
if [ "$?" -ne "0" ]; then | |
return 1 | |
fi | |
echo " - string: ${str}" | |
;; | |
14) | |
;; | |
15) | |
;; | |
16) | |
echo "type tag ia5string" >&2 | |
local length_length=0 | |
local str="" | |
read_ia5string_der str read_item_read_size | |
if [ "$?" -ne "0" ]; then | |
return 1 | |
fi | |
echo " - string: ${str}" | |
read_item_read_size=$(($read_item_read_size + $length_length)) | |
;; | |
17) | |
echo "type tag utctime" >&2 | |
local length_length=0 | |
local utctime="" | |
read_utctime utctime length_length | |
if [ "$?" -ne "0" ]; then | |
return 1 | |
fi | |
echo " - time: ${utctime}" | |
read_item_read_size=$(($read_item_read_size + $length_length)) | |
;; | |
18) | |
;; | |
19) | |
;; | |
1a) | |
;; | |
1b) | |
;; | |
1c) | |
;; | |
1d) | |
;; | |
1e) | |
;; | |
1f) | |
;; | |
*) | |
echo "ERROR: unreachable tag number ${tag_num}" | |
return 127 # UNREACHABLE | |
;; | |
esac | |
fi | |
} | |
while true; do | |
item_size=0 | |
read_item item_size | |
if [ "$?" -eq "1" ]; then | |
break | |
fi | |
# echo "read an item and it was $item_size octets" | |
done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I thought to myself, "there's no way someone wrote an asn.1 (de|en)coder in pure bash"... TIL