ssh_data 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|