ssh_data 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.md +21 -0
- data/lib/ssh_data.rb +36 -0
- data/lib/ssh_data/certificate.rb +240 -0
- data/lib/ssh_data/encoding.rb +666 -0
- data/lib/ssh_data/error.rb +7 -0
- data/lib/ssh_data/private_key.rb +73 -0
- data/lib/ssh_data/private_key/base.rb +39 -0
- data/lib/ssh_data/private_key/dsa.rb +75 -0
- data/lib/ssh_data/private_key/ecdsa.rb +95 -0
- data/lib/ssh_data/private_key/ed25519.rb +68 -0
- data/lib/ssh_data/private_key/rsa.rb +106 -0
- data/lib/ssh_data/public_key.rb +78 -0
- data/lib/ssh_data/public_key/base.rb +71 -0
- data/lib/ssh_data/public_key/dsa.rb +122 -0
- data/lib/ssh_data/public_key/ecdsa.rb +151 -0
- data/lib/ssh_data/public_key/ed25519.rb +74 -0
- data/lib/ssh_data/public_key/rsa.rb +79 -0
- data/lib/ssh_data/version.rb +3 -0
- metadata +116 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
module SSHData
|
2
|
+
module PrivateKey
|
3
|
+
OPENSSH_PEM_TYPE = "OPENSSH PRIVATE KEY"
|
4
|
+
RSA_PEM_TYPE = "RSA PRIVATE KEY"
|
5
|
+
DSA_PEM_TYPE = "DSA PRIVATE KEY"
|
6
|
+
ECDSA_PEM_TYPE = "EC PRIVATE KEY"
|
7
|
+
ENCRYPTED_PEM_TYPE = "ENCRYPTED PRIVATE KEY"
|
8
|
+
|
9
|
+
# Parse an SSH private key.
|
10
|
+
#
|
11
|
+
# key - A PEM or OpenSSH encoded private key.
|
12
|
+
#
|
13
|
+
# Returns an Array of PrivateKey::Base subclass instances.
|
14
|
+
def self.parse(key)
|
15
|
+
pem_type = Encoding.pem_type(key)
|
16
|
+
case pem_type
|
17
|
+
when OPENSSH_PEM_TYPE
|
18
|
+
parse_openssh(key)
|
19
|
+
when RSA_PEM_TYPE
|
20
|
+
[RSA.from_openssl(OpenSSL::PKey::RSA.new(key, ""))]
|
21
|
+
when DSA_PEM_TYPE
|
22
|
+
[DSA.from_openssl(OpenSSL::PKey::DSA.new(key, ""))]
|
23
|
+
when ECDSA_PEM_TYPE
|
24
|
+
[ECDSA.from_openssl(OpenSSL::PKey::EC.new(key, ""))]
|
25
|
+
when ENCRYPTED_PEM_TYPE
|
26
|
+
raise DecryptError, "cannot decode encrypted private keys"
|
27
|
+
else
|
28
|
+
raise AlgorithmError, "unknown PEM type: #{pem_type.inspect}"
|
29
|
+
end
|
30
|
+
rescue OpenSSL::PKey::PKeyError => e
|
31
|
+
raise DecodeError, "bad private key. maybe encrypted?"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Parse an OpenSSH formatted private key.
|
35
|
+
#
|
36
|
+
# key - An OpenSSH encoded private key.
|
37
|
+
#
|
38
|
+
# Returns an Array of PrivateKey::Base subclass instances.
|
39
|
+
def self.parse_openssh(key)
|
40
|
+
raw = Encoding.decode_pem(key, OPENSSH_PEM_TYPE)
|
41
|
+
|
42
|
+
data, read = Encoding.decode_openssh_private_key(raw)
|
43
|
+
unless read == raw.bytesize
|
44
|
+
raise DecodeError, "unexpected trailing data"
|
45
|
+
end
|
46
|
+
|
47
|
+
from_data(data)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.from_data(data)
|
51
|
+
data[:private_keys].map do |priv|
|
52
|
+
case priv[:algo]
|
53
|
+
when PublicKey::ALGO_RSA
|
54
|
+
RSA.new(**priv)
|
55
|
+
when PublicKey::ALGO_DSA
|
56
|
+
DSA.new(**priv)
|
57
|
+
when PublicKey::ALGO_ECDSA256, PublicKey::ALGO_ECDSA384, PublicKey::ALGO_ECDSA521
|
58
|
+
ECDSA.new(**priv)
|
59
|
+
when PublicKey::ALGO_ED25519
|
60
|
+
ED25519.new(**priv)
|
61
|
+
else
|
62
|
+
raise DecodeError, "unkown algo: #{priv[:algo].inspect}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
require "ssh_data/private_key/base"
|
70
|
+
require "ssh_data/private_key/rsa"
|
71
|
+
require "ssh_data/private_key/dsa"
|
72
|
+
require "ssh_data/private_key/ecdsa"
|
73
|
+
require "ssh_data/private_key/ed25519"
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SSHData
|
2
|
+
module PrivateKey
|
3
|
+
class Base
|
4
|
+
attr_reader :algo, :comment, :public_key
|
5
|
+
|
6
|
+
def initialize(**kwargs)
|
7
|
+
@algo = kwargs[:algo]
|
8
|
+
@comment = kwargs[:comment]
|
9
|
+
end
|
10
|
+
|
11
|
+
# Generate a new private key.
|
12
|
+
#
|
13
|
+
# Returns a PublicKey::Base subclass instance.
|
14
|
+
def self.generate(**kwargs)
|
15
|
+
raise "implement me"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Make an SSH signature.
|
19
|
+
#
|
20
|
+
# signed_data - The String message over which to calculated the signature.
|
21
|
+
# algo: - Optionally specify the signature algorithm to use.
|
22
|
+
#
|
23
|
+
# Returns a binary String signature.
|
24
|
+
def sign(signed_data, algo: nil)
|
25
|
+
raise "implement me"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Issue a certificate using this private key.
|
29
|
+
#
|
30
|
+
# signature_algo: - Optionally specify the signature algorithm to use.
|
31
|
+
# kwargs - See SSHData::Certificate.new.
|
32
|
+
#
|
33
|
+
# Returns a SSHData::Certificate instance.
|
34
|
+
def issue_certificate(signature_algo: nil, **kwargs)
|
35
|
+
Certificate.new(**kwargs).tap { |c| c.sign(self, algo: signature_algo) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module SSHData
|
2
|
+
module PrivateKey
|
3
|
+
class DSA < Base
|
4
|
+
attr_reader :p, :q, :g, :x, :y, :openssl
|
5
|
+
|
6
|
+
# Generate a new private key.
|
7
|
+
#
|
8
|
+
# Returns a PublicKey::Base subclass instance.
|
9
|
+
def self.generate
|
10
|
+
from_openssl(OpenSSL::PKey::DSA.generate(1024))
|
11
|
+
end
|
12
|
+
|
13
|
+
# Import an openssl private key.
|
14
|
+
#
|
15
|
+
# key - An OpenSSL::PKey::DSA instance.
|
16
|
+
#
|
17
|
+
# Returns a DSA instance.
|
18
|
+
def self.from_openssl(key)
|
19
|
+
new(
|
20
|
+
algo: PublicKey::ALGO_DSA,
|
21
|
+
p: key.params["p"],
|
22
|
+
q: key.params["q"],
|
23
|
+
g: key.params["g"],
|
24
|
+
y: key.params["pub_key"],
|
25
|
+
x: key.params["priv_key"],
|
26
|
+
comment: "",
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(algo:, p:, q:, g:, x:, y:, comment:)
|
31
|
+
unless algo == PublicKey::ALGO_DSA
|
32
|
+
raise DecodeError, "bad algorithm: #{algo.inspect}"
|
33
|
+
end
|
34
|
+
|
35
|
+
@p = p
|
36
|
+
@q = q
|
37
|
+
@g = g
|
38
|
+
@x = x
|
39
|
+
@y = y
|
40
|
+
|
41
|
+
super(algo: algo, comment: comment)
|
42
|
+
|
43
|
+
@openssl = OpenSSL::PKey::DSA.new(asn1.to_der)
|
44
|
+
|
45
|
+
@public_key = PublicKey::DSA.new(algo: algo, p: p, q: q, g: g, y: y)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Make an SSH signature.
|
49
|
+
#
|
50
|
+
# signed_data - The String message over which to calculated the signature.
|
51
|
+
#
|
52
|
+
# Returns a binary String signature.
|
53
|
+
def sign(signed_data, algo: nil)
|
54
|
+
algo ||= self.algo
|
55
|
+
raise AlgorithmError unless algo == self.algo
|
56
|
+
openssl_sig = openssl.sign(OpenSSL::Digest::SHA1.new, signed_data)
|
57
|
+
raw_sig = PublicKey::DSA.ssh_signature(openssl_sig)
|
58
|
+
Encoding.encode_signature(algo, raw_sig)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def asn1
|
64
|
+
OpenSSL::ASN1::Sequence.new([
|
65
|
+
OpenSSL::ASN1::Integer.new(0),
|
66
|
+
OpenSSL::ASN1::Integer.new(p),
|
67
|
+
OpenSSL::ASN1::Integer.new(q),
|
68
|
+
OpenSSL::ASN1::Integer.new(g),
|
69
|
+
OpenSSL::ASN1::Integer.new(y),
|
70
|
+
OpenSSL::ASN1::Integer.new(x),
|
71
|
+
])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module SSHData
|
2
|
+
module PrivateKey
|
3
|
+
class ECDSA < Base
|
4
|
+
attr_reader :curve, :public_key_bytes, :private_key_bytes, :openssl
|
5
|
+
|
6
|
+
# Generate a new private key.
|
7
|
+
#
|
8
|
+
# curve - The String curve to use. One of SSHData::PublicKey::NISTP256,
|
9
|
+
# SSHData::PublicKey::NISTP384, or SSHData::PublicKey::NISTP521.
|
10
|
+
#
|
11
|
+
# Returns a PublicKey::Base subclass instance.
|
12
|
+
def self.generate(curve)
|
13
|
+
openssl_curve = PublicKey::ECDSA::OPENSSL_CURVE_NAME_FOR_CURVE[curve]
|
14
|
+
raise AlgorithmError, "unknown curve: #{curve}" if openssl_curve.nil?
|
15
|
+
|
16
|
+
openssl_key = OpenSSL::PKey::EC.new(openssl_curve).tap(&:generate_key)
|
17
|
+
from_openssl(openssl_key)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Import an openssl private key.
|
21
|
+
#
|
22
|
+
# key - An OpenSSL::PKey::EC instance.
|
23
|
+
#
|
24
|
+
# Returns a DSA instance.
|
25
|
+
def self.from_openssl(key)
|
26
|
+
curve = PublicKey::ECDSA::CURVE_FOR_OPENSSL_CURVE_NAME[key.group.curve_name]
|
27
|
+
algo = "ecdsa-sha2-#{curve}"
|
28
|
+
|
29
|
+
new(
|
30
|
+
algo: algo,
|
31
|
+
curve: curve,
|
32
|
+
public_key: key.public_key.to_bn.to_s(2),
|
33
|
+
private_key: key.private_key,
|
34
|
+
comment: "",
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(algo:, curve:, public_key:, private_key:, comment:)
|
39
|
+
unless [PublicKey::ALGO_ECDSA256, PublicKey::ALGO_ECDSA384, PublicKey::ALGO_ECDSA521].include?(algo)
|
40
|
+
raise DecodeError, "bad algorithm: #{algo.inspect}"
|
41
|
+
end
|
42
|
+
|
43
|
+
unless algo == "ecdsa-sha2-#{curve}"
|
44
|
+
raise DecodeError, "bad curve: #{curve.inspect}"
|
45
|
+
end
|
46
|
+
|
47
|
+
@curve = curve
|
48
|
+
@public_key_bytes = public_key
|
49
|
+
@private_key_bytes = private_key
|
50
|
+
|
51
|
+
super(algo: algo, comment: comment)
|
52
|
+
|
53
|
+
@openssl = begin
|
54
|
+
OpenSSL::PKey::EC.new(asn1.to_der)
|
55
|
+
rescue ArgumentError
|
56
|
+
raise DecodeError, "bad key data"
|
57
|
+
end
|
58
|
+
|
59
|
+
@public_key = PublicKey::ECDSA.new(
|
60
|
+
algo: algo,
|
61
|
+
curve: curve,
|
62
|
+
public_key: public_key_bytes
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Make an SSH signature.
|
67
|
+
#
|
68
|
+
# signed_data - The String message over which to calculated the signature.
|
69
|
+
#
|
70
|
+
# Returns a binary String signature.
|
71
|
+
def sign(signed_data, algo: nil)
|
72
|
+
algo ||= self.algo
|
73
|
+
raise AlgorithmError unless algo == self.algo
|
74
|
+
openssl_sig = openssl.sign(public_key.digest.new, signed_data)
|
75
|
+
raw_sig = PublicKey::ECDSA.ssh_signature(openssl_sig)
|
76
|
+
Encoding.encode_signature(algo, raw_sig)
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def asn1
|
82
|
+
unless name = PublicKey::ECDSA::OPENSSL_CURVE_NAME_FOR_CURVE[curve]
|
83
|
+
raise DecodeError, "unknown curve: #{curve.inspect}"
|
84
|
+
end
|
85
|
+
|
86
|
+
OpenSSL::ASN1::Sequence.new([
|
87
|
+
OpenSSL::ASN1::Integer.new(1),
|
88
|
+
OpenSSL::ASN1::OctetString.new(private_key_bytes.to_s(2)),
|
89
|
+
OpenSSL::ASN1::ObjectId.new(name, 0, :EXPLICIT, :CONTEXT_SPECIFIC),
|
90
|
+
OpenSSL::ASN1::BitString.new(public_key_bytes, 1, :EXPLICIT, :CONTEXT_SPECIFIC)
|
91
|
+
])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module SSHData
|
2
|
+
module PrivateKey
|
3
|
+
class ED25519 < Base
|
4
|
+
attr_reader :pk, :sk, :ed25519_key
|
5
|
+
|
6
|
+
# Generate a new private key.
|
7
|
+
#
|
8
|
+
# Returns a PublicKey::Base subclass instance.
|
9
|
+
def self.generate
|
10
|
+
PublicKey::ED25519.ed25519_gem_required!
|
11
|
+
from_ed25519(Ed25519::SigningKey.generate)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Create from a ::Ed25519::SigningKey instance.
|
15
|
+
#
|
16
|
+
# key - A ::Ed25519::SigningKey instance.
|
17
|
+
#
|
18
|
+
# Returns a ED25519 instance.
|
19
|
+
def self.from_ed25519(key)
|
20
|
+
new(
|
21
|
+
algo: PublicKey::ALGO_ED25519,
|
22
|
+
pk: key.verify_key.to_bytes,
|
23
|
+
sk: key.to_bytes + key.verify_key.to_bytes,
|
24
|
+
comment: "",
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(algo:, pk:, sk:, comment:)
|
29
|
+
unless algo == PublicKey::ALGO_ED25519
|
30
|
+
raise DecodeError, "bad algorithm: #{algo.inspect}"
|
31
|
+
end
|
32
|
+
|
33
|
+
# openssh stores the pk twice, once as half of the sk...
|
34
|
+
if sk.bytesize != 64 || sk.byteslice(32, 32) != pk
|
35
|
+
raise DecodeError, "bad sk"
|
36
|
+
end
|
37
|
+
|
38
|
+
@pk = pk
|
39
|
+
@sk = sk
|
40
|
+
|
41
|
+
super(algo: algo, comment: comment)
|
42
|
+
|
43
|
+
if PublicKey::ED25519.enabled?
|
44
|
+
@ed25519_key = Ed25519::SigningKey.new(sk.byteslice(0, 32))
|
45
|
+
|
46
|
+
if @ed25519_key.verify_key.to_bytes != pk
|
47
|
+
raise DecodeError, "bad pk"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
@public_key = PublicKey::ED25519.new(algo: algo, pk: pk)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Make an SSH signature.
|
55
|
+
#
|
56
|
+
# signed_data - The String message over which to calculated the signature.
|
57
|
+
#
|
58
|
+
# Returns a binary String signature.
|
59
|
+
def sign(signed_data, algo: nil)
|
60
|
+
PublicKey::ED25519.ed25519_gem_required!
|
61
|
+
algo ||= self.algo
|
62
|
+
raise AlgorithmError unless algo == self.algo
|
63
|
+
raw_sig = ed25519_key.sign(signed_data)
|
64
|
+
Encoding.encode_signature(algo, raw_sig)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module SSHData
|
2
|
+
module PrivateKey
|
3
|
+
class RSA < Base
|
4
|
+
attr_reader :n, :e, :d, :iqmp, :p, :q, :openssl
|
5
|
+
|
6
|
+
|
7
|
+
# Generate a new private key.
|
8
|
+
#
|
9
|
+
# size - The Integer key size to generate.
|
10
|
+
# unsafe_allow_small_key: - Bool of whether to allow keys of less than
|
11
|
+
# 2048 bits.
|
12
|
+
#
|
13
|
+
# Returns a PublicKey::Base subclass instance.
|
14
|
+
def self.generate(size, unsafe_allow_small_key: false)
|
15
|
+
unless size >= 2048 || unsafe_allow_small_key
|
16
|
+
raise AlgorithmError, "key too small"
|
17
|
+
end
|
18
|
+
|
19
|
+
from_openssl(OpenSSL::PKey::RSA.generate(size))
|
20
|
+
end
|
21
|
+
|
22
|
+
# Import an openssl private key.
|
23
|
+
#
|
24
|
+
# key - An OpenSSL::PKey::DSA instance.
|
25
|
+
#
|
26
|
+
# Returns a DSA instance.
|
27
|
+
def self.from_openssl(key)
|
28
|
+
new(
|
29
|
+
algo: PublicKey::ALGO_RSA,
|
30
|
+
n: key.params["n"],
|
31
|
+
e: key.params["e"],
|
32
|
+
d: key.params["d"],
|
33
|
+
iqmp: key.params["iqmp"],
|
34
|
+
p: key.params["p"],
|
35
|
+
q: key.params["q"],
|
36
|
+
comment: "",
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(algo:, n:, e:, d:, iqmp:, p:, q:, comment:)
|
41
|
+
unless algo == PublicKey::ALGO_RSA
|
42
|
+
raise DecodeError, "bad algorithm: #{algo.inspect}"
|
43
|
+
end
|
44
|
+
|
45
|
+
@n = n
|
46
|
+
@e = e
|
47
|
+
@d = d
|
48
|
+
@iqmp = iqmp
|
49
|
+
@p = p
|
50
|
+
@q = q
|
51
|
+
|
52
|
+
super(algo: algo, comment: comment)
|
53
|
+
|
54
|
+
@openssl = OpenSSL::PKey::RSA.new(asn1.to_der)
|
55
|
+
|
56
|
+
@public_key = PublicKey::RSA.new(algo: algo, e: e, n: n)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Make an SSH signature.
|
60
|
+
#
|
61
|
+
# signed_data - The String message over which to calculated the signature.
|
62
|
+
#
|
63
|
+
# Returns a binary String signature.
|
64
|
+
def sign(signed_data, algo: nil)
|
65
|
+
algo ||= self.algo
|
66
|
+
digest = PublicKey::RSA::ALGO_DIGESTS[algo]
|
67
|
+
raise AlgorithmError if digest.nil?
|
68
|
+
raw_sig = openssl.sign(digest.new, signed_data)
|
69
|
+
Encoding.encode_signature(algo, raw_sig)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# CRT coefficient for faster RSA operations. Used by OpenSSL, but not
|
75
|
+
# OpenSSH.
|
76
|
+
#
|
77
|
+
# Returns an OpenSSL::BN instance.
|
78
|
+
def dmp1
|
79
|
+
d % (p - 1)
|
80
|
+
end
|
81
|
+
|
82
|
+
# CRT coefficient for faster RSA operations. Used by OpenSSL, but not
|
83
|
+
# OpenSSH.
|
84
|
+
#
|
85
|
+
# Returns an OpenSSL::BN instance.
|
86
|
+
def dmq1
|
87
|
+
d % (q - 1)
|
88
|
+
end
|
89
|
+
|
90
|
+
def asn1
|
91
|
+
OpenSSL::ASN1::Sequence.new([
|
92
|
+
OpenSSL::ASN1::Integer.new(0),
|
93
|
+
OpenSSL::ASN1::Integer.new(n),
|
94
|
+
OpenSSL::ASN1::Integer.new(e),
|
95
|
+
OpenSSL::ASN1::Integer.new(d),
|
96
|
+
OpenSSL::ASN1::Integer.new(p),
|
97
|
+
OpenSSL::ASN1::Integer.new(q),
|
98
|
+
OpenSSL::ASN1::Integer.new(dmp1),
|
99
|
+
OpenSSL::ASN1::Integer.new(dmq1),
|
100
|
+
OpenSSL::ASN1::Integer.new(iqmp),
|
101
|
+
])
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|