Skip to content

Instantly share code, notes, and snippets.

@iximeow
Created April 13, 2021 00:12
Show Gist options
  • Save iximeow/c2e1cd08465642c94ab8d0b24971ad6c to your computer and use it in GitHub Desktop.
Save iximeow/c2e1cd08465642c94ab8d0b24971ad6c to your computer and use it in GitHub Desktop.
asn.1 parsing in pure bash
#! /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
@mtthlm
Copy link

mtthlm commented Nov 14, 2022

I thought to myself, "there's no way someone wrote an asn.1 (de|en)coder in pure bash"... TIL

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment