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.
@@ -0,0 +1,7 @@
1
+ module SSHData
2
+ Error = Class.new(StandardError)
3
+ DecodeError = Class.new(Error)
4
+ VerifyError = Class.new(Error)
5
+ AlgorithmError = Class.new(Error)
6
+ DecryptError = Class.new(Error)
7
+ end
@@ -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