Skip to content

Commit f07d827

Browse files
authored
Merge pull request #8 from github/re-encode-certs
Re-encode Certificate in OpenSSH authorized_keys format
2 parents f7468f4 + 78dbb1b commit f07d827

23 files changed

Lines changed: 943 additions & 414 deletions

lib/ssh_data/certificate.rb

Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
require "securerandom"
2+
13
module SSHData
24
class Certificate
5+
# Special values for valid_before and valid_after.
6+
BEGINNING_OF_TIME = Time.at(0)
7+
END_OF_TIME = Time.at((2**64)-1)
8+
39
# Integer certificate types
410
TYPE_USER = 1
511
TYPE_HOST = 2
@@ -59,41 +65,12 @@ def self.parse_rfc4253(raw, unsafe_no_verify: false)
5965
end
6066

6167
# Parse data into better types, where possible.
62-
valid_after = Time.at(data.delete(:valid_after))
63-
valid_before = Time.at(data.delete(:valid_before))
64-
public_key = PublicKey.from_data(data.delete(:key_data))
65-
valid_principals, _ = Encoding.decode_strings(data.delete(:valid_principals))
66-
critical_options, _ = Encoding.decode_options(data.delete(:critical_options))
67-
extensions, _ = Encoding.decode_options(data.delete(:extensions))
68-
69-
# The signature key is encoded as a string, but we can parse it.
70-
sk_raw = data.delete(:signature_key)
71-
sk_data, read = Encoding.decode_public_key(sk_raw)
72-
if read != sk_raw.bytesize
73-
raise DecodeError, "unexpected trailing data"
74-
end
75-
ca_key = PublicKey.from_data(sk_data)
68+
public_key = PublicKey.from_data(data.delete(:public_key))
69+
ca_key = PublicKey.from_data(data.delete(:signature_key))
7670

77-
unless unsafe_no_verify
78-
# The signature is the last field. The signature is calculated over all
79-
# preceding data.
80-
signed_data_len = raw.bytesize - data[:signature].bytesize - 4
81-
signed_data = raw.byteslice(0, signed_data_len)
82-
83-
unless ca_key.verify(signed_data, data[:signature])
84-
raise VerifyError
85-
end
71+
new(**data.merge(public_key: public_key, ca_key: ca_key)).tap do |cert|
72+
raise VerifyError unless unsafe_no_verify || cert.verify
8673
end
87-
88-
new(**data.merge(
89-
valid_after: valid_after,
90-
valid_before: valid_before,
91-
public_key: public_key,
92-
valid_principals: valid_principals,
93-
critical_options: critical_options,
94-
extensions: extensions,
95-
ca_key: ca_key,
96-
))
9774
end
9875

9976
# Intialize a new Certificate instance.
@@ -120,9 +97,9 @@ def self.parse_rfc4253(raw, unsafe_no_verify: false)
12097
# signature: - The certificate's String signature field.
12198
#
12299
# Returns nothing.
123-
def initialize(algo:, nonce:, public_key:, serial:, type:, key_id:, valid_principals:, valid_after:, valid_before:, critical_options:, extensions:, reserved:, ca_key:, signature:)
124-
@algo = algo
125-
@nonce = nonce
100+
def initialize(public_key:, key_id:, algo: nil, nonce: nil, serial: 0, type: TYPE_USER, valid_principals: [], valid_after: BEGINNING_OF_TIME, valid_before: END_OF_TIME, critical_options: {}, extensions: {}, reserved: "", ca_key: nil, signature: "")
101+
@algo = algo || Encoding::CERT_ALGO_BY_PUBLIC_KEY_ALGO[public_key.algo]
102+
@nonce = nonce || SecureRandom.random_bytes(32)
126103
@public_key = public_key
127104
@serial = serial
128105
@type = type
@@ -136,5 +113,74 @@ def initialize(algo:, nonce:, public_key:, serial:, type:, key_id:, valid_princi
136113
@ca_key = ca_key
137114
@signature = signature
138115
end
116+
117+
# OpenSSH certificate in authorized_keys format (see sshd(8) manual page).
118+
#
119+
# comment - Optional String comment to append.
120+
#
121+
# Returns a String key.
122+
def openssh(comment: nil)
123+
[algo, Base64.strict_encode64(rfc4253), comment].compact.join(" ")
124+
end
125+
126+
# RFC4253 binary encoding of the certificate.
127+
#
128+
# Returns a binary String.
129+
def rfc4253
130+
Encoding.encode_fields(
131+
[:string, algo],
132+
[:string, nonce],
133+
[:raw, public_key_without_algo],
134+
[:uint64, serial],
135+
[:uint32, type],
136+
[:string, key_id],
137+
[:list, valid_principals],
138+
[:time, valid_after],
139+
[:time, valid_before],
140+
[:options, critical_options],
141+
[:options, extensions],
142+
[:string, reserved],
143+
[:string, ca_key.rfc4253],
144+
[:string, signature],
145+
)
146+
end
147+
148+
# Sign this certificate with a private key.
149+
#
150+
# private_key - An SSHData::PrivateKey::Base subclass instance.
151+
#
152+
# Returns nothing.
153+
def sign(private_key)
154+
@ca_key = private_key.public_key
155+
@signature = private_key.sign(signed_data)
156+
end
157+
158+
# Verify the certificate's signature.
159+
#
160+
# Returns boolean.
161+
def verify
162+
ca_key.verify(signed_data, signature)
163+
end
164+
165+
private
166+
167+
# The portion of the certificate over which the signature is calculated.
168+
#
169+
# Returns a binary String.
170+
def signed_data
171+
siglen = self.signature.bytesize + 4
172+
rfc4253.byteslice(0...-siglen)
173+
end
174+
175+
# Helper for getting the RFC4253 encoded public key with the first field
176+
# (the algorithm) stripped off.
177+
#
178+
# Returns a String.
179+
def public_key_without_algo
180+
key = public_key.rfc4253
181+
_, algo_len = Encoding.decode_string(key)
182+
key.byteslice(algo_len..-1)
183+
end
184+
139185
end
140186
end

0 commit comments

Comments
 (0)