Skip to content

Instantly share code, notes, and snippets.

@ddlsmurf
Last active February 6, 2022 01:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ddlsmurf/cd12da946bc1fe1a00b409e1ef627e78 to your computer and use it in GitHub Desktop.
Save ddlsmurf/cd12da946bc1fe1a00b409e1ef627e78 to your computer and use it in GitHub Desktop.
SSH private key file and signature file reader
defmodule T do
def read_ssh_sig(filename) do
{:ok, raw_sig} = File.read(filename)
raw_sig = String.replace(raw_sig, ~r/.*-----BEGIN SSH SIGNATURE-----\n/s, "")
raw_sig = String.replace(raw_sig, ~r/\n-----END SSH SIGNATURE-----.*/s, "")
Base.decode64(raw_sig, ignore: :whitespace)
end
defp shift_string(<<len::unsigned-integer-32, str::binary-size(len), rest::binary>>), do: { str, rest }
defp parse_ssh_sig_body(sig_body) do
# ssh_sig_magic = 'SSHSIG'
ssh_sig_version = 1
<<"SSHSIG", ^ssh_sig_version::unsigned-integer-32, sig_body::binary>> = sig_body
{ pk, sig_rest } = shift_string(sig_body)
{ ns, sig_rest } = shift_string(sig_rest)
{ reserved, sig_rest } = shift_string(sig_rest)
{ hash, sig_rest } = shift_string(sig_rest)
{ sig, "" } = shift_string(sig_rest)
{ sig_format, sig_inner_rest } = shift_string(sig)
{ sig, "" } = shift_string(sig_inner_rest)
%{pk: :public_key.ssh_decode(pk, :ssh2_pubkey),
ns: ns,
hash: hash,
reserved: reserved,
sig_format: sig_format,
sig: sig}
end
defp parse_ssh_private_key_rnd_prv_comment_pad(rnd_prv_comment_pad) do
<<dummy::unsigned-integer-32, rest::binary>> = rnd_prv_comment_pad
<<^dummy::unsigned-integer-32, rest::binary>> = rest
# https://coolaj86.com/articles/the-openssh-private-key-format/
{ key_type, rest } = shift_string(rest)
"ssh-ed25519" = key_type
{ pub0, rest } = shift_string(rest) # pub0 may be repeated depending on key type
{ prv0, rest } = shift_string(rest) # prv0 may be repeated depending on key type
{ comment, rest } = shift_string(rest)
# IO.inspect(key_type: key_type)
# IO.inspect(pub0: pub0, sz: byte_size(pub0))
# IO.inspect(prv0: prv0, sz: byte_size(prv0))
# IO.inspect(comment: comment, sz: byte_size(comment))
if byte_size(rest) > 3, do: raise "Unexpected byte count left #{byte_size(rest)}"
<<private_key::binary-size(32), _::binary-size(32)>> = prv0 # no idea why the public key is again here
{ key_type, pub0, private_key, comment }
end
def parse_ssh_private_key_file(filename) do
{:ok, raw_private_key} = File.read(filename)
[ private_key_pem ] = :public_key.pem_decode(raw_private_key)
{{:no_asn1, :new_openssh}, data, :not_encrypted} = private_key_pem
<<"openssh-key-v1", 0, rest::binary>> = data
{ ciphername, rest } = shift_string(rest)
{ kdfname, rest } = shift_string(rest)
{ kdf, rest } = shift_string(rest)
[ "none", "none", "" ] = [ciphername, kdfname, kdf]
number_of_keys = 1
<<^number_of_keys::unsigned-integer-32, rest::binary>> = rest
{ public_key, rest } = shift_string(rest)
{ rnd_prv_comment_pad, rest } = shift_string(rest)
if byte_size(rest) > 0, do: raise "Unexpected byte count left #{byte_size(rest)}"
{ "ssh-ed25519", public, private, comment } = parse_ssh_private_key_rnd_prv_comment_pad(rnd_prv_comment_pad)
{ _, _, ^public } = decoded_public_key = :public_key.ssh_decode(public_key, :ssh2_pubkey) # just checking they're the same key
# IO.inspect(:public_key.ssh_decode(private, :ssh2_pubkey))
%{
public: decoded_public_key,
private: {:ed_pri, :ed25519, public, private},
comment: comment,
}
end
def test_ed25519_sig() do
msg = "foo\n"
hash = :sha512 # :ignored
{ public, private } = :crypto.generate_key(:eddsa, :ed25519)
IO.inspect(pub: public, pub_sz: byte_size(public), priv: private, priv_sz: byte_size(private))
key_public = {:ed_pub, :ed25519, public}
key_private = {:ed_pri, :ed25519, public, private}
sig = :public_key.sign(msg, hash, key_private)
IO.inspect(sig: sig, sig_sz: byte_size(sig))
validation = :public_key.verify(msg, hash, sig, key_public)
IO.inspect(validation: validation)
end
defp to_ssh_string(str), do: <<byte_size(str)::unsigned-integer-32>> <> str
defp build_ssh_sig_body(msg, namespace, reserved, "sha512" = hash) do
"SSHSIG" <>
to_ssh_string(namespace) <>
to_ssh_string(reserved) <>
to_ssh_string(hash) <>
to_ssh_string(:crypto.hash(:sha512, msg))
end
def run() do
msg = "foo\n"
{:ok, raw_sig} = read_ssh_sig("sig")
sig_file = parse_ssh_sig_body(raw_sig)
%{hash: "sha512", sig_format: "ssh-ed25519"} = sig_file
# IO.inspect(sig_file)
signature_cleartext = build_ssh_sig_body(msg, sig_file.ns, sig_file.reserved, sig_file.hash)
IO.inspect(:public_key.verify(signature_cleartext, :sha512, sig_file.sig, sig_file.pk))
private_key_file = parse_ssh_private_key_file("id_otp_test")
# IO.inspect(private_key_file: private_key_file)
try_sign = :public_key.sign(signature_cleartext, :sha512, private_key_file.private)
if try_sign != result.sig, do: raise "Signature isn't identical when resigning with private key"
Process.exit(self(), :done)
end
end
# T.test_ed25519_sig()
T.run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment