Skip to content

Instantly share code, notes, and snippets.

@shgeta
Last active October 10, 2021 14:39
Show Gist options
  • Save shgeta/b81d936eb10be7e0a0e8 to your computer and use it in GitHub Desktop.
Save shgeta/b81d936eb10be7e0a0e8 to your computer and use it in GitHub Desktop.
ssh接続用の公開鍵( ssh-rsa public key )をopensslで使える形式( X.509 PEM )にshellscriptで変換する Mac OS X ( Convert ssh-rsa public key to pem )

#ssh接続用の公開鍵( ssh-rsa public key )をopensslで使える形式( X.509 PEM )にshellscriptで変換する Mac OS X (Convert ssh-rsa public key to pem.)

概要

ssh-rsa公開鍵からpem公開鍵への変換。 rsaで公開鍵を使って暗号化するためにはopensslを使えばいいんだが、ssh-keygenで作ったssh接続用のrsa公開鍵そのままではうまくいかない。opensslで使える形式に変換する必要がある。 ssh接続用の公開鍵をopensslで使える形式に変換するのはopenssh付属のssh-keygen で

ssh-keygen -e -m PKCS8 -f ~/.ssh/id_rsa.pub >/tmp/x1sfsdpem.pub

こんな感じでできる。はずなんだけど、現在osx標準搭載のopensshのバージョンでは、なんでかうまく変換できない。そのためこの変換をする場合、最新版のopensshをインストールする必要がある。 このスクリプトは__最新版opensslのインストールなしに__ 変換を実現するもの。 使っているコマンドは下のsandboxの所をみる。

やっていること : ssh-rsa の公開鍵を分解して pkcs#1 のバイナリを作り x.509に組み入れてからPEMにする。

文字列の暗号化までしたい人はこちら

「ssh-rsaの公開鍵」

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNckEuuAeiPesx99s/ivWoXQGdJIMKRl0I0HiSLMCdK/dUrXy5ycyy51cEtv0t/AGQCkAxqiYBCWiVhLV/1qpZuONL9UT8cTa4TO749lFVaucxLNN7nvUtbtA4InKqRjsjqK27vCzyWxiMVIMX0jNpD0rPCwkTK2Ja6knCRN7kA2c3UyNmX4IoQ0xqT0vaUNuxtOe9SkmT3DLizDMbYByzJWVgotbZfOu1QbbClpLt/TbDd5l3fcGNsRzT8Cnd8zdvXk5ZsiUDKKhynvA4Tt/LN9LjZgxTyoEYJewYzf51E8gH057A9zXguBTTAiHMgD8xgeGzh4AVEEFJ1ZO6oCft [email protected]

これを 「pem (x.509) の公開鍵」

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzXJBLrgHoj3rMffbP4r1
qF0BnSSDCkZdCNB4kizAnSv3VK18ucnMsudXBLb9LfwBkApAMaomAQlolYS1f9aq
WbjjS/VE/HE2uEzu+PZRVWrnMSzTe571LW7QOCJyqkY7I6itu7ws8lsYjFSDF9Iz
aQ9KzwsJEytiWupJwkTe5ANnN1MjZl+CKENMak9L2lDbsbTnvUpJk9wy4swzG2Ac
syVlYKLW2XzrtUG2wpaS7f02w3eZd33BjbEc0/Ap3fM3b15OWbIlAyiocp7wOE7f
yzfS42YMU8qBGCXsGM3+dRPIB9OewPc14LgU0wIhzIA/MYHhs4eAFRBBSdWTuqAn
7QIDAQAB
-----END PUBLIC KEY-----
[email protected]

こうします

メモ

  • バイトの切り張りで変換を行う。
  • big endianで抜き出し、書き込みできるように。
  • (16進数、10進数) >バイナリ は printf をつかう
  • とりあえずPKCS#1のほうのnのバイト数はLong Definite、eの長さはただのバイト数と決め打ちする。(つまりSEQUENCEのながさはssh-rsaのほうのeのバイト数+nのバイト数+6バイト)pkcs#1
  • _seq_lenの長さは256以上と決め打ちする。pkcs#1
  • ssh-rsa > pkcs#1 > x.509 と作っていく。pkcs#1はどうもopensslでは使えないみたい。
  • BIT STRINGの長さには、ビットのあまり指定部を含む。x.509
  • とにかくASN.1。

TODO

  • 実用上nはいいけど、eは念のためLong Definiteに対応しておきたい。Long Definiteは0x80のビットが立っている場合のこと。0x82なら直後の2バイトで長さを表すってこと。128未満はそのバイトに直接長さを入れる。。
  • _seq_lenの長さは256以上と決め打ちする。とした部分。
  • function にもう少し使い方など説明をつける。
  • とりあえずコピペした参考資料をなんとかする。
  • 10進数を4byte 2byte(ネットワークバイトオーダー)でバイナリに出すfunctionをつくる。

使い方

functionのコードをターミナルに貼り付ける。(この時点では何も起きない。) それから下のように sshpubkey2pem コマンドを打つ。

usage: 
  sshpubkey2pem <ssh-rsa から始まるssh接続用公開鍵文字列 pathではない>
  or
  sshpubkey2pem $(cat < path to ssh-rsa pub >)
  

(例)
sshpubkey2pem ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNckEuuAeiPesx99s/ivWoXQGdJIMKRl0I0HiSLMCdK/dUrXy5ycyy51cEtv0t/AGQCkAxqiYBCWiVhLV/1qpZuONL9UT8cTa4TO749lFVaucxLNN7nvUtbtA4InKqRjsjqK27vCzyWxiMVIMX0jNpD0rPCwkTK2Ja6knCRN7kA2c3UyNmX4IoQ0xqT0vaUNuxtOe9SkmT3DLizDMbYByzJWVgotbZfOu1QbbClpLt/TbDd5l3fcGNsRzT8Cnd8zdvXk5ZsiUDKKhynvA4Tt/LN9LjZgxTyoEYJewYzf51E8gH057A9zXguBTTAiHMgD8xgeGzh4AVEEFJ1ZO6oCft [email protected]

sshpubkey2pem $(cat ~/.ssh/id_rsa.pub)

  * ssh接続用公開鍵は ~/.ssh/id_rsa.pub の中身をもらうか、https://github.com/<ユーザー名>.keys
  から取得しておく。
result:
-----BEGIN PUBLIC KEY-----
MII........................
....
-----END PUBLIC KEY-----


shell function (貼り付けて使う)

#shell script で ssh-rsa 形式の公開鍵を opensslで使える形式、x509のPEMにする。
#エディターの都合上インデントなしで勘弁。
#usageもまだ。sshpubkey2pem $(cat ~/.ssh/id_rsa.pub ) こんな感じでつかって!
function sshpubkey2pem () 
{
(


#functions 
{

function bit_to_decimal ()
{
echo "obase=10; ibase=2; $1" | bc
}

function decimal_to_bit ()
{
echo "obase=2; ibase=10; $1" | bc
}

function decimal_to_bin_2byte ()
{
(
#単純な方が見やすいのでこれでいいや。
_num="$1"
printf "%b" "\x$(printf '%04x' $_num | cut -b1-2)"
printf "%b" "\x$(printf '%04x' $_num | cut -b3-4)"

)
}

function decimal_to_bin_4byte ()
{
(
_num="$1"
printf "%b" "\x$(printf '%08x' $_num | cut -b1-2)"
printf "%b" "\x$(printf '%08x' $_num | cut -b3-4)"
printf "%b" "\x$(printf '%08x' $_num | cut -b5-6)"
printf "%b" "\x$(printf '%08x' $_num | cut -b7-8)"

)
}

function decimal_to_hex ()
{
(
_num="$1"
_hex=$(printf '%x' $_num)
_hex_length=${#_hex}

if test $(expr $_hex_length % 2 ) -eq 1
then
_hex_length=$(expr $_hex_length + 1)
_hex="0$_hex"
fi

echo $_hex
)
}

function decimal_to_bin()
{
(
_num="$1"
_hex=$( decimal_to_hex $_num )
_bytes=$(expr ${#_hex} / 2 )
_pos=1
while test $_pos -le ${#_hex}
do
printf "%b" "\x$(printf $_hex | cut -b${_pos}-$(expr $_pos + 1) )"
_pos=$(expr $_pos + 2)
done

)
}



{
#DER functions

{
#common
function bytes_of_decimal ()
{
(
_num="$1"
_hex=$(decimal_to_hex "$_num")
_bytes=$(expr ${#_hex} / 2 )

echo $_bytes
)
}

function bytes_of_file ()
{
echo $(wc -c "$1" | tr -s " ") | cut -f1 -d " "
}
}


function der_tag ()
{
(
_class="00"  
_primitive_or_constructed="$1"
_tag="$2"

#10進で受け取ったタグをビット列に、5桁になるようにパディング
_tag_bit=$( printf "%05s" $( decimal_to_bit $_tag ) )

#ビット列を作る
#Class:7-6	P/C:5	Tag number:4-0
_bits="${_class}${_primitive_or_constructed}${_tag_bit}"
#10進数にしてバイナリに
decimal_to_bin $( bit_to_decimal $_bits )
)
}
function der_tag_primitive ()
{
(
#タグを出力
_primitive_or_constructed=0
_tag="$1"
der_tag "$_primitive_or_constructed" "$_tag"
)
}

function der_tag_constructed ()
{
#SEQUENCE など用
(
#タグを出力
_primitive_or_constructed=1
_tag="$1"
der_tag "$_primitive_or_constructed" "$_tag"
)
}
function der_length ()
{
(
#usage 
#der_length <number(length)>
#128未満はそのまま、128以上は8桁目のビットを立てて、長さの長さを指定するバイト数を出力、その後長さ本体を出力。
_num="$1"
if test $_num -lt 128
then
decimal_to_bin $_num
else
# Long Definite
_bytes=$(bytes_of_decimal "$_num")
#長さの指定が128バイト以上かかることは考慮しない。
decimal_to_bin $( expr $_bytes + 128 )
decimal_to_bin "$_num"
fi
)
}


function der_length_value ()
{
#print der tlv's lv 
#usage
#der_length_value  <filepath to binaryfile of value>

_file="$1"
_length=$(bytes_of_file "$_file")

der_length $_length
cat $_file

}

function der_bitstring ()
{

#bitstringにしたい中身のfilepathを引数に指定します。
_file_bitstring="$1"

_bytes_bitstring=$(bytes_of_file $_file_bitstring)

# tag BIT STRING
der_tag_primitive 3
# 長さ ビットのあまり指定部の分を加算
der_length $(expr $_bytes_bitstring + 1 ) 

#ビットのあまり指定部
printf "%b" "\x00"
cat $_file_bitstring

}

}

}

PUBKEY="$@"
NUMBER_BYTE_SIZE=4
_head=0
_file_tmp_files=$(mktemp -t convrsa) ; echo $_file_tmp_files >>$_file_tmp_files

_file_binnakami=$(mktemp -t convrsa) ; echo $_file_binnakami >>$_file_tmp_files
_file_e=$(mktemp -t convrsa) ; echo $_file_e >>$_file_tmp_files
_file_n=$(mktemp -t convrsa) ; echo $_file_n >>$_file_tmp_files
_file_pkcs1_bin=$(mktemp -t convrsa) ; echo $_file_pkcs1_bin >>$_file_tmp_files
_file_x509_nakami=$(mktemp -t convrsa) ; echo $_file_x509_nakami >>$_file_tmp_files
_file_x509_bin=$(mktemp -t convrsa) ; echo $_file_x509_bin >>$_file_tmp_files

echo $PUBKEY | cut -d" " -f2 | base64 -D >>$_file_binnakami
_nokori=$(echo $PUBKEY | cut -d" " -f3- )

{
#ssh-rsa 解析
{
#ssh-rsaの文字列の長さ
_type_string_size=$(printf "%d" $( cat $_file_binnakami | dd bs=1 skip=$_head count=$NUMBER_BYTE_SIZE | od -t x1 | head -n1 | sed -e's/^0000000 *\(..\) *\(..\) *\(..\) *\(..\)/0x\1\2\3\4/')
)
_head=$(expr $NUMBER_BYTE_SIZE  + $_head)
_head=$(expr $_head + $_type_string_size)

#eのバイト数
_e_length=$(printf "%d" $( cat $_file_binnakami | dd bs=1 skip=$_head count=$NUMBER_BYTE_SIZE | od -t x1 | head -n1 | sed -e's/^0000000 *\(..\) *\(..\) *\(..\) *\(..\)/0x\1\2\3\4/')
)
_head=$(expr $NUMBER_BYTE_SIZE  + $_head)

#eの値
cat $_file_binnakami |dd bs=1 skip=$_head count=$_e_length >$_file_e
_head=$(expr $_e_length + $_head)

#nのバイト数 
_n_length=$(printf "%d" $( cat $_file_binnakami | dd bs=1 skip=$_head count=$NUMBER_BYTE_SIZE | od -t x1 | head -n1 | cut -d" " -f2- | tr -d " " | sed -e's/^/0x/' )
)
_head=$(expr $NUMBER_BYTE_SIZE + $_head)

#nの値
cat $_file_binnakami | dd bs=1 skip=$_head count=$_n_length >$_file_n
_head=$(expr $_n_length + $_head)

} 2>/dev/null


#############################################

# PKCS1の中身作成
{
_seq_len=$(expr $_n_length + $_e_length + 6)
(
# tag SEQUENCE
#printf "%b" "\x30"
der_tag_constructed 16

#length 
der_length $_seq_len 

{
#n

#tag Integer
#printf "%b" "\x02"
der_tag_primitive 2
#length and value
der_length_value $_file_n

}

{
#e
#tag Integer
#printf "%b" "\x02"
der_tag_primitive 2
#length and value
der_length_value $_file_e

}

) >$_file_pkcs1_bin

}

#PKCS1部のバイト数
#_bytes_pkcs1=$(echo $(wc -c $_file_pkcs1_bin | tr -s " ") | cut -f1 -d " ")


#x509バイナリ作成
{

#中身
{

# AlgorithmIdentifier
{
# tag SEQUENCE
der_tag_constructed 16
# SEQUENCEの長さ
der_length 13
{
{
# tag OBJECT IDENTIFIER
der_tag_primitive 6
# length
der_length 9
# value
(
for _char in 2a 86 48 86 f7 0d 01 01 01
do
printf "%b" "\x"$_char
done
)
}
{
# tag NULL
der_tag_primitive 5
# length
der_length 0
}
}
}

#pkcs1部をビットストリングに埋め込む
der_bitstring $_file_pkcs1_bin

} >$_file_x509_nakami

#_bytes_x509_nakami=$(echo $(wc -c $_file_x509_nakami | tr -s " ") | cut -f1 -d " ")

#x509バイナリ
{
# tag SEQUENCE
#printf "%b" "\x30"
der_tag_constructed 16
#length and value
der_length_value $_file_x509_nakami

} >$_file_x509_bin
}

#PEMに
{
echo "-----BEGIN PUBLIC KEY-----"
cat $_file_x509_bin | base64 -b 64
echo "-----END PUBLIC KEY-----"
echo "$_nokori"
}

for _file_tmp in $(cat $_file_tmp_files)
do
rm $_file_tmp; 
done
}
)
}

sandbox

一時フォルダなど環境で違うかも。 sshpubkey2pem $(cat ~/.ssh/id_rsa.pub )を動かすのに最低必要な環境を定義してみた。どんなコマンド使われてんのかななどと思ったときに見ると良い。

sandbox-exec -p '
(version 1)
(deny default)

(import "bsd.sb")
(allow file-read*
    (regex #"^/Users/.+/.bashrc$")
    (regex #"^/Users/.+/.rnd$"))
(allow file* 
    (literal "/dev/tty")
    (regex #"^/Users/.+/.bash_history$")
    )

(allow process-fork)
(allow process-exec* (literal "/bin/bash"))


(allow file-read*
    (literal "/Users/shigeta/.ssh/id_rsa.pub"))
(allow file* 
    (regex #"^/private/var/folders/[^/]+/[^/]+/T/convrsa"))
(allow process-exec*
       (literal "/bin/cat")
       (literal "/bin/dd")
       (literal "/bin/expr")
       (literal "/bin/rm")
       (literal "/usr/bin/base64")
       (literal "/usr/bin/cut")
       (literal "/usr/bin/head")
       (literal "/usr/bin/mktemp")
       (literal "/usr/bin/od")
       (literal "/usr/bin/sed")
       (literal "/usr/bin/tr")
       (literal "/usr/bin/wc"))
' /bin/bash

テストとか確認とか。

#注意 このスクリプトは暗号解読時に秘密鍵へのアクセスがあります。opensslへの引数。
# ~/.ssh/id_rsa ~/.ssh/id_rsa.pub が対である前提。

(
_file_pem=$(mktemp -t convrsa)
_file_target=$(mktemp -t convrsa)
_file_tmp=$(mktemp -t convrsa)

(
sshpubkey2pem $(cat ~/.ssh/id_rsa.pub ) >$_file_pem
openssl asn1parse -inform PEM -in $_file_pem -dump || {
echo "error" 
exit 1 
}

openssl rsa -inform PEM -pubin -text -in $_file_pem || {
echo "error" 
exit 1 
}
echo test >$_file_target
_encryptedstr=$(openssl rsautl -pkcs -encrypt -pubin -inkey $_file_pem < $_file_target >$_file_tmp && cat $_file_tmp | base64)
echo $_encryptedstr

_decryptedstr=$(echo $_encryptedstr | base64 -D | openssl rsautl -decrypt -inkey ~/.ssh/id_rsa ||{
echo "error" 
exit 1 
}
)
echo $_decryptedstr

if test "$_decryptedstr" != "test"
then 
echo error
exit 1
fi
) && echo success 

rm $_file_pem $_file_target $_file_tmp
) 


test for decimal_to_bin (function decimal_to_hex と function decimal_to_binを貼り付けてから)

(
echo "decimal_to_bin がちゃんと動いてるか"
echo 65536
_val="$(echo $(decimal_to_bin 65536 | od -tx1 | head -n1 ) )"
if test "$_val" == '0000000 01 00 00'
then
echo "success"
else 
echo "fail : $_val" 
fi

echo 255
_val="$(echo $(decimal_to_bin 255 | od -tx1 | head -n1 ) )"
if test "$_val" == '0000000 ff'
then
echo "success"
else 
echo "fail : $_val" 
fi
)

test for DER util

(
test $(echo $(der_tag_primitive 16 | od -tx1 | head -n1 |cut -d" " -f2- ) ) -eq 10 || echo error der_tag_primitive 16
test $(echo $(der_tag_constructed 16 | od -tx1 | head -n1 |cut -d" " -f2- ) ) -eq 30 || echo error der_tag_constructed 16
test $(echo $(der_tag_constructed 1 | od -tx1 | head -n1 |cut -d" " -f2- ) ) -eq 21 || echo error der_tag_constructed 16
)

わかったこと

  • ssh接続に使っているrsa公開鍵はssh-rsaと言われるopenssh独自のもの。中身が同じで一般的なものはPKCS#1。
  • 公開鍵の中にはe(publicExponent)とn(modulus)が入っている。
  • ssh-rsaとPKCS#1は入れ物の形が違うが、中に入ってる重要なものは同じ。
  • opensslではPKCS#1のままでは使えなくて、x.509にすれば使える。
  • x.509のなかにはPKCS#1が入っている。
  • x.509はDERで符号化されている。たぶんPKCS#1もそうだと思う。
  • DERはBERの符号化方法に制限をかけた符号化方法。
  • BERはASN.1で定義されたコード化方法。TLV符号化という技術を使っている。
  • TLVは Type,Length,Valueで構成される。入れ子にできる。
  • 値はネットワークバイトオーダー。(頭から順番に並んでるやつ)
  • DERではBIT STRINGの未使用ビットは常に0。
  • TLVのLが128以上の場合Long Definiteをつかう。これはLの0x80のビットを立てて、下7bitに「長さを入れる箱」の長さを指定する。Lが0x82ならLong Definiteで直後の2バイトに長さが入っているという意味。0x03ならそのままVの長さは3バイト。
  • PEMはx.509とかpkcs#1とかのバイナリをbase64デコードで64バイトずつ改行しながら出力したものをヘッダ、フッター(-----BEGIN PUBLIC KEY-----など)ではさむ。

資料

rsa鍵のフォーマットの参考資料

shellからのバイト操作の参考資料

pkcsの参考資料

ASN.1の参考資料

BERの参考資料

ASN.1 Table 1 – Universal class tag assignments

複合型とプリミティブの説明


The "ssh-rsa" key format has the following specific encoding:

      string    "ssh-rsa"
      mpint     e
      mpint     n

   Here the 'e' and 'n' parameters form the signature key blob.

mpint
      Represents multiple precision integers in two's complement format,
      stored as a string, 8 bits per byte, MSB first.  Negative numbers
      have the value 1 as the most significant bit of the first byte of
      the data partition.  If the most significant bit would be set for
      a positive number, the number MUST be preceded by a zero byte.
      Unnecessary leading bytes with the value 0 or 255 MUST NOT be
      included.  The value zero MUST be stored as a string with zero
      bytes of data.

なので、正の整数を入れたいときに、最初のバイトがff,a0など一番頭のビットが立ちそうなら、00のバイトを頭にくっつけて負の数になるのを防ぐって感じだと思う。ff > 00 ff ,a0 > 00 a0

binary	意味
00 00 00 07	文字列 "ssh-rsa"の長さ
73 73 68 2d 72 73 61	"ssh-rsa"
00 00 00 03	数値のバイト数(3)
01 00 01	eの値(0x10001)
00 00 01 01	数値のバイト数(0x101)
00 ef 3d a8...	nの値(0xEF3DA8...)

PKCS#1

A.1.1 RSA public key syntax

   An RSA public key should be represented with the ASN.1 type
   RSAPublicKey:

      RSAPublicKey ::= SEQUENCE {
          modulus           INTEGER,  -- n
          publicExponent    INTEGER   -- e
      }

   The fields of type RSAPublicKey have the following meanings:

    * modulus is the RSA modulus n.

    * publicExponent is the RSA public exponent e.

INTEGERの定義はまだ探していないが、上のssh-rsaのmpintの値をそのまま入れて問題なく動くことは確認した。ので、負の整数になりそうな場合の処理は同じなのかなと思う。

binary	意味
30    	SEQUENCE
82	次の2バイトがSEQUENCEの長さ
01 0a	0x10Aバイト
02	INTEGER
82	次の2バイトがINTEGERの長さ
01 01	nの長さ(0x101バイト)
00 ef 3d a8...	nの値(0xEF3DA8...)
02	INTEGER
03	INTEGERの長さ(3バイト)
01 00 01	eの値(0x10001)

つまりSEQUENCEのながさはssh-rsaのほうのssh-rsaより後ろの部分のの長さ+ 決め打ち nのバイト数はロングレングスエンコーディングeの長さはただのバイト数。 TODO 実用上nはいいけど、eは念のためロングレングスエンコーディングに対応しておきたい。 ロングレングスエンコーディングは0x82の場合のこと。2以外で0x80が立っていたらエラーにするか?

だめだった。rsa public key(pkcs#1)に対応してなかった。(OpenSSL 0.9.8zd 8 Jan 2015) さらにX.509で包む。

binary	意味 
30    	SEQUENCE
82	次の2バイトがSEQUENCEの長さ
01 22	0x122バイト
30    	SEQUENCE 00 1 10000(constructedの16) 
0d    	SEQUENCEの長さ 13バイト
06    	OBJECT IDENTIFIER 00 0 0110(6) 中身はAlgorithmIdentifier
09    	OBJECT IDENTIFIERの長さ(9バイト)
2a 86 48 86 f7 0d 01 01 01	PKCS#1 rsaEncryption これは決められた値(RFC 3279)。
05    	NULL  The parameters field MUST have ASN.1 type NULL for this algorithm identifier. (RFC 3279)
00    	NULLの長さ(0バイト)
03    	BIT STRING
82	次の2バイトがBIT STRINGの長さ
01 0f    	BIT STRINGの長さ(0x10Fバイト) 未使用ビットの長さ部のバイトを含む
00    	未使用ビットの長さ(0ビット)
30 82 01 0a...	PKCS#1 公開鍵

5バイト目からが

AlgorithmIdentifier  ::=  SEQUENCE  {
        algorithm               OBJECT IDENTIFIER,
        parameters              ANY DEFINED BY algorithm OPTIONAL  }

で、rsaのばあいparametersはASN.1のNULLで固定。 30 はSequence and Sequence-of typesの複合型(bit5 (6bit目)あり)

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