ssh_data 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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