Skip to content

Instantly share code, notes, and snippets.

@mlebkowski
Last active April 17, 2026 13:23
Show Gist options
  • Select an option

  • Save mlebkowski/9699686debb0df798728d52666f58a9b to your computer and use it in GitHub Desktop.

Select an option

Save mlebkowski/9699686debb0df798728d52666f58a9b to your computer and use it in GitHub Desktop.
Parse RSA/ECDSA public key PEM into ASN1 JSON representation in pure bash

Parse PEM public keys into JSON

openssl already provides two ways of reading public keys in PEM format:

  • openssl ec -pubin -text -noout -in key.pem
  • openssl asn1parse < key.pem

Unfortunately, both provide text output which is hard to parse and use in an automated manner. This script provides a third way, and the output is structured JSON, using pure bash. See examples below.

Usage

bash public-key-to-json.sh id_ecdsa_prime256v1.pem id_ecdsa_secp384r1.pem id_rsa.pub 

This example outputs three json documents (one can easily consume them using jq -s):

{
  "type": "SEQUENCE",
  "value": [
    {
      "type": "SEQUENCE",
      "value": [
        {
          "type": "OBJECT",
          "value": "id-ecPublicKey"
        },
        {
          "type": "OBJECT",
          "value": "prime256v1"
        }
      ]
    },
    {
      "type": "BIT STRING",
      "value": "AARMEQEDYHJtefNb/Xa/nbhJpX/HpkQG/FlK41IZoIpzcrdVbILBHnCymxvfJAQBWzfkvGfJTBsiqx6wG0E89O9t"
    }
  ]
}
{
  "type": "SEQUENCE",
  "value": [
    {
      "type": "SEQUENCE",
      "value": [
        {
          "type": "OBJECT",
          "value": "id-ecPublicKey"
        },
        {
          "type": "OBJECT",
          "value": "secp384r1"
        }
      ]
    },
    {
      "type": "BIT STRING",
      "value": "AAQS7OEfjX47JZJcFmTwgGM7rg8xC6iCFKikvYWmQO9MBVjp4YW/LPP+2pMYLbr0yADhItUGJBFYIbb7ZW+/0YOeeXo43fk+UAaaAwzJdlU/C+CKgqrIKiWciwyzA/GtA7Y="
    }
  ]
}
{
  "type": "SEQUENCE",
  "value": [
    {
      "type": "SEQUENCE",
      "value": [
        {
          "type": "OBJECT",
          "value": "rsaEncryption"
        },
        {
          "type": "NULL",
          "value": ""
        }
      ]
    },
    {
      "type": "BIT STRING",
      "value": {
        "type": "SEQUENCE",
        "value": [
          {
            "type": "INTEGER",
            "value": "nwafJsdFdDZCKG9bgw7AJ9So/622Q/Gt9ClWH+uvYtv+ZwPQF3Bna4ibiAxT7kKlRMz1xusFFGNnb5wpI+r+AJa2H9gUcRbdNQurHTReB3W95hV3IeOlETbZcHDxglOqKaRU8QM92xhjiwD2eNfGNgNlYagGw5PsgpiJQkfA/KKAr8qI91FZMr6mtEvVb00eZHd6CqjM3HiNAIadLJAaE0gOXkGFXqRo+dEdpNHfuvTkwc5oHZPXNugVlZ9ncrlBAXYmI4TSN5eC6Kq0B92xK4mw64TqfKsvsMpcKg1Ek7SIVcYEmthdBTKtwc5NCEtoiWGmKvxWEUy4Wq+0AfRJlw=="
          },
          {
            "type": "INTEGER",
            "value": "AQAB"
          }
        ]
      }
    }
  ]
}

Details

The ASN1 structure for public keys is the same for both RSA and ECDSA key types:

SEQUENCE
  SEQUENCE
     OBJECT # type of key
     OBJECT # key params
  BIT STRING # key payload

This means that you can reliably get the key details from the resulting JSON. For example, to get the key types:

bash public-key-to-json.sh id_ecdsa_prime256v1.pem id_ecdsa_secp384r1.pem id_rsa.pub | jq -r '.value[0].value[0].value'
# outputs: 
# id-ecPublicKey
# id-ecPublicKey
# rsaEncryption

ECDSA key payload

The ECDSA key payload will be represented as a base64-encoded string. After decoding you can expect the following:

  • First byte is always 0x00 an artifact of the ASN1 encoding
  • Second byte is always 0x04 indicating the „compressed” format, which basically tells you that the following data is x and y values concatenated
  • The rest is two integers x and y, both taking half of the remaining payload. Their length depends on the curve type, and for example:
    • P-256 is 256 bits, thus 32-byte points
    • P-384 is 384 bits, using 48-byte points
    • and P-521 is 521 bits, so — you guessed it — each point uses 66 bytes

It’s left as an excercise to the reader to extract these values.

RSA payload

The RSA key payload is different. It contains the modulus and the exponent, but the BIT STRING encodes them using DER/ASN1 format, so the script recursively parses it and embeds in the JSON structure. You can expect this field to have two children (wrapped in a SEQUENCE), both of type INTEGER, and their values are base64-encoded.

System requirements

type openssl jq base64 xxd

Supported formats

  • RSA public key in PEM format (file starts with -----BEGIN PUBLIC KEY-----)
  • ECDSA public key in PEM format

Since the script does not impose any output structure and just represents the ASN1 structure, it can technically be used to parse any PEM file, for example an SSL certificate.

Issues

Leave a comment below if there are any issues.

#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
trim() {
echo "$@" | xargs;
}
# stdin: key=value pairs separated by whitespace
# output: json object
parse_params() {
local mode=key ifs="="
local key value token
while read -r -d"$ifs" token; do
case "$mode" in
"key")
key="$(trim "$token")"
mode="value"
ifs=" "
;;
"value")
value="$(trim "$token")"
if [ -z "$token" ]; then
continue
fi
# don’t eat my input! that’s not for you:
: | jq -Ms --arg key "$key" --arg value "$value" '{ $key: $value }'
mode="key"
ifs="="
;;
esac
done | jq -Ms 'add'
}
# stdin: hex dump as printed by openssl asn1parse -dump
# param $1: expected length of the hex dump
# output base64 encoded contents of the dump
parse_hex_dump() {
declare length="$1"
local line i
local bytes_per_row=$((0x10))
# math trick to get the ceiling of the division result:
local rows=$(( (length + bytes_per_row - 1) / bytes_per_row ))
# hex dump width: 2 columns per byte, and one for gaps in between:
local hex_columns_length=$((bytes_per_row * 2 + bytes_per_row - 1))
for ((i = 0; i < rows; i++)); do
read -r line
echo "${line:7:hex_columns_length}"
done | xxd -r -p | base64
}
# stdin: none, but some of the called functions will consume hex dump
# param $1: line to be parsed
# output: json line representing that ASN1 line
parse_asn1_line() {
declare line="$1"
local offset params type value
IFS=: read -r offset params type value <<<"$line"
local json_params depth length
json_params="$(parse_params <<<"$params")"
depth="$(jq -r .d <<<"$json_params")"
length="$(jq -r .l <<<"$json_params")"
type="$(trim "$type")"
local json_value="null"
case "$type" in
"BIT STRING"|"OCTET STRING")
value="$(parse_hex_dump $((length)))"
local bitstring_parsed
# special case for bit/octet strings containing DER
if bitstring_parsed="$(openssl asn1parse -inform B64 -strparse 1 <<<"$value" 2>/dev/null)"; then
json_value="$(parse_asn1 <<<"$bitstring_parsed")"
fi
;;
"INTEGER")
value="$(xxd -r -p <<<"$value" | base64)"
;;
esac
jq -ncM \
--arg type "$type" \
--arg value "$value" \
--argjson json_value "$json_value" \
--arg depth $((depth)) \
'{ $type, $depth, value: ($json_value//$value) }'
}
# input: json lines with individual parsed ASN1 lines
# output: json object combined into hierarchy based on the `depth` key
stack() {
jq -s '
reduce .[] as $item (
{root: null, stack: []};
($item.depth | tonumber) as $d |
# trim stack to current depth
.stack |= .[:$d] |
# compute path for this node
(if $d == 0 then
["root"]
else
(.stack[-1] + ["children", (getpath(.stack[-1] + ["children"]) | length)])
end
) as $path |
({
type: $item.type,
value: $item.value,
children: []
}) as $node |
# set node at computed path
.root |= (if . == null and $d == 0 then $node else setpath($path[1:]; $node) end) |
# push path onto stack
.stack += [$path]
)
| .root
| walk(
if type == "object" and (.children? | type == "array" and length == 0)
then del(.children)
else .
end
)
| walk(
if type == "object" and (.children? | type == "array" and length > 0) and (.value? == "")
then (.value = .children | del(.children))
else .
end
)
'
}
# stdin: result of openssl asn1parse -dump
# output: json object
parse_asn1() {
local line
while read -r line; do
parse_asn1_line "$line"
done | stack
}
main() {
if [ $# -eq 0 ]; then
echo "Usage: $0 <pubkey.pem> [...pubkey.pem]" >&2
return 1
fi
for key; do
openssl asn1parse -dump < "$key" | parse_asn1
done
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment