1+ require "securerandom"
2+
13module 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
140186end
0 commit comments