@@ -39,16 +39,21 @@ def verify(materials:, policy:)
3939 )
4040 end
4141
42- chain = store_ctx . chain || raise
43- chain . drop ( 1 )
42+ chain = store_ctx . chain || raise ( "no chain found" )
43+ chain . shift # remove the cert itself
4444
45- _sct = precertificate_signed_certificate_timestamps ( materials . certificate ) [ 0 ]
46- # verify_sct(
47- # sct,
48- # materials.certificate,
49- # chain,
50- # @rekor_client._ct_keyring
51- # )
45+ sct_list = precertificate_signed_certificate_timestamps ( materials . certificate )
46+ raise "no SCTs found" if sct_list . empty?
47+
48+ sct_list . each do |sct |
49+ verified = verify_sct (
50+ sct ,
51+ materials . certificate ,
52+ chain ,
53+ @rekor_client . ct_keyring
54+ )
55+ return VerificationFailure . new ( "SCT verification failed" ) unless verified
56+ end
5257
5358 usage_ext = materials . certificate . find_extension ( "keyUsage" )
5459 unless usage_ext . value == "Digital Signature"
@@ -66,7 +71,7 @@ def verify(materials:, policy:)
6671 signing_key = materials . certificate . public_key
6772
6873 raise "missing hashed input" unless materials . hashed_input
69- raise "missing signature" unless materials . signature
74+ raise "missing signature" unless materials . signature # TODO: handle DSSE envelope
7075 raise "missing input bytes" unless materials . input_bytes
7176
7277 verified = signing_key . verify ( materials . hashed_input . name , materials . signature ,
@@ -95,12 +100,110 @@ def verify(materials:, policy:)
95100
96101 private
97102
103+ def verify_sct ( sct , certificate , chain , ct_keyring )
104+ # TODO: validate hash & signature algorithm match the key in the keyring
105+ hash = sct . fetch ( :hash )
106+ signature_algorithm = sct . fetch ( :signature_algorithm )
107+ unless hash == "sha256" && signature_algorithm == "ecdsa"
108+ # TODO: support more algorithms
109+ raise "only sha256 edcsa supported, got #{ hash } #{ signature_algorithm } "
110+ end
111+
112+ issuer_key_id = nil
113+ if sct [ :entry_type ] == 1
114+ issuer_cert = find_issuer_cert ( chain )
115+ issuer_pubkey = issuer_cert . public_key
116+ unless VerificationMaterials . cert_is_ca? ( issuer_cert )
117+ raise "Invalid issuer pubkey basicConstraint (not a CA): #{ issuer_cert . to_text } "
118+ end
119+ raise "unsupported issuer pubkey" unless case issuer_pubkey
120+ when OpenSSL ::PKey ::RSA , OpenSSL ::PKey ::EC
121+ true
122+ else
123+ false
124+ end
125+
126+ issuer_key_id = OpenSSL ::Digest ::SHA256 . digest ( issuer_pubkey . public_to_der )
127+ end
128+
129+ digitally_signed = pack_digitally_signed ( sct , certificate , issuer_key_id ) . b
130+
131+ ct_keyring . verify ( key_id : sct [ :log_id ] , signature : sct [ :signature ] , data : digitally_signed )
132+ end
133+
134+ def pack_digitally_signed ( sct , certificate , issuer_key_id = nil )
135+ # https://datatracker.ietf.org/doc/html/rfc6962#section-3.4
136+ # https://datatracker.ietf.org/doc/html/rfc6962#section-3.5
137+ #
138+ # digitally-signed struct {
139+ # Version sct_version;
140+ # SignatureType signature_type = certificate_timestamp;
141+ # uint64 timestamp;
142+ # LogEntryType entry_type;
143+ # select(entry_type) {
144+ # case x509_entry: ASN.1Cert;
145+ # case precert_entry: PreCert;
146+ # } signed_entry;
147+ # CtExtensions extensions;
148+ # };
149+
150+ signed_entry =
151+ case sct [ :entry_type ]
152+ when 0 # x509_entry
153+ cert_der = certificate . to_public_der
154+ cert_len = cert_der . bytesize
155+ unused , len1 , len2 , len3 = [ cert_len ] . pack ( "N" ) . unpack ( "C4" )
156+ raise "invalid cert_len #{ cert_len } #{ cert_der . inspect } " if unused != 0
157+
158+ [ len1 , len2 , len3 , cert_der ] . pack ( "CCC a#{ cert_len } " )
159+ when 1 # precert_entry
160+ unless issuer_key_id &.bytesize == 32
161+ raise "issuer_key_id must be 32 bytes for precert, given #{ issuer_key_id . inspect } "
162+ end
163+
164+ tbs_cert = tbs_certificate_der ( certificate )
165+ tbs_cert_len = tbs_cert . bytesize
166+ unused , len1 , len2 , len3 = [ tbs_cert_len ] . pack ( "N" ) . unpack ( "C4" )
167+ raise "invalid tbs_cert_len #{ tbs_cert_len } #{ tbs_cert . inspect } " if unused != 0
168+
169+ [ issuer_key_id , len1 , len2 , len3 , tbs_cert ] . pack ( "a32 CCC a#{ tbs_cert_len } " )
170+ else
171+ raise "only x509_entry and precert_entry supported, given #{ sct [ :entry_type ] . inspect } "
172+ end
173+
174+ [ sct [ :version ] , 0 , sct [ :timestamp ] , sct [ :entry_type ] , signed_entry , 0 ] . pack ( <<~PACK )
175+ C # version
176+ C # signature_type
177+ Q> # timestamp
178+ n # entry_type
179+ a#{ signed_entry . bytesize } # signed_entry
180+ n # extensions length
181+ PACK
182+ end
183+
184+ def tbs_certificate_der ( certificate )
185+ tbs_cert = certificate . dup
186+ oid = OpenSSL ::X509 ::Extension . new ( "1.3.6.1.4.1.11129.2.4.2" , "" ) . oid
187+ tbs_cert . extensions = tbs_cert . extensions . reject do |ext |
188+ ext . oid == oid
189+ end
190+ # ensure the underlying certificate is marked as modified
191+ tbs_cert . serial = tbs_cert . serial + 1
192+ tbs_cert . serial = tbs_cert . serial - 1
193+
194+ raise "no #{ oid } extension found" unless certificate . extensions . size == tbs_cert . extensions . size + 1
195+
196+ OpenSSL ::ASN1 . decode ( tbs_cert . to_der ) . value [ 0 ] . to_der . b
197+ end
198+
199+ # https://letsencrypt.org/2018/04/04/sct-encoding.html
98200 def precertificate_signed_certificate_timestamps ( certificate )
99201 # this is cursed. can't always find_extension(oid) because #oid can return a string or an OID
100202 oid = OpenSSL ::X509 ::Extension . new ( "1.3.6.1.4.1.11129.2.4.2" , "" ) . oid
101203 precert_scts_extension = certificate . find_extension ( oid )
204+
102205 unless precert_scts_extension
103- raise "No PrecertificateSignedCertificateTimestamps (#{ oid . inspect } ) found for the certificate #{ certificate . extensions . join ( " \n " ) } "
206+ raise "No PrecertificateSignedCertificateTimestamps (#{ oid . inspect } ) found for the certificate #{ certificate . to_text } "
104207 end
105208
106209 # TODO: parse the extension properly
@@ -109,22 +212,94 @@ def precertificate_signed_certificate_timestamps(certificate)
109212 os1 = OpenSSL ::ASN1 . decode ( precert_scts_extension . value_der )
110213
111214 len = os1 . value . unpack1 ( "n" )
112- string = os1 . value [ 2 ..]
113- raise "os1: len=#{ len } #{ os1 . value . inspect } " unless string && string . size == len
215+ string = os1 . value . byteslice ( 2 ..)
216+ raise "os1: len=#{ len } #{ os1 . value . inspect } " unless string && string . bytesize == len
114217
115218 len = string . unpack1 ( "n" )
116- string = string [ 2 ..]
117- raise "os1: len=#{ len } #{ string . inspect } " unless string && string . size == len
118-
119- sct_version , sct_log_id , sct_timestamp , sct_extensions_len , sct_signature_alg_hash ,
120- sct_signature_alg_sign , sct_signature_len , sct_signature_bytes = string . unpack ( "Ca32QnCCna*" )
121- raise "sct extensions not supported" unless sct_extensions_len . zero?
122- unless sct_signature_bytes . bytesize == sct_signature_len
123- raise "sct_signature_bytes: #{ sct_signature_bytes . inspect } sct_signature_len: #{ sct_signature_len } "
219+ string = string . byteslice ( 2 ..)
220+ raise "os2: len=#{ len } #{ string . inspect } " unless string && string . bytesize == len
221+
222+ list = unpack_sct_list ( string )
223+
224+ list . map! do |sct |
225+ hash = {
226+ 0 => "none" ,
227+ 1 => "md5" ,
228+ 2 => "sha1" ,
229+ 3 => "sha224" ,
230+ 4 => "sha256" ,
231+ 5 => "sha384" ,
232+ 6 => "sha512" ,
233+ 255 => "unknown"
234+ } . fetch ( sct [ :sct_signature_alg_hash ] , "unknown" )
235+
236+ signature_algorithm = {
237+ 0 => "anonymous" ,
238+ 1 => "rsa" ,
239+ 2 => "dsa" ,
240+ 3 => "ecdsa" ,
241+ 255 => "unknown"
242+ } . fetch ( sct [ :sct_signature_alg_sign ] , "unknown" )
243+
244+ {
245+ version : sct [ :sct_version ] ,
246+ log_id : sct [ :sct_log_id ] . unpack1 ( "H*" ) ,
247+ timestamp : sct [ :sct_timestamp ] ,
248+ signature : sct [ :sct_signature_bytes ] ,
249+ hash : hash ,
250+ signature_algorithm : signature_algorithm ,
251+ entry_type : 1 # precert_entry
252+ }
253+ end
254+ end
255+
256+ def unpack_sct_list ( string )
257+ offset = 0
258+ len = string . bytesize
259+ list = [ ]
260+ while offset < len
261+ sct_version , sct_log_id , sct_timestamp , sct_extensions_len = string . unpack ( "Ca32Q>n" , offset : offset )
262+ offset += 1 + 32 + 8 + 2 + sct_extensions_len
263+ raise "expect sct version to be 0, got #{ sct_version } " unless sct_version . zero?
264+ raise "sct_extensions_len=#{ sct_extensions_len } not supported" unless sct_extensions_len . zero?
265+
266+ sct_signature_alg_hash , sct_signature_alg_sign , sct_signature_len = string . unpack ( "CCn" , offset : offset )
267+ offset += 1 + 1 + 2
268+ sct_signature_bytes = string . unpack1 ( "a#{ sct_signature_len } " , offset : offset ) . b
269+ offset += sct_signature_len
270+ list << {
271+ sct_version : sct_version ,
272+ sct_log_id : sct_log_id ,
273+ sct_timestamp : sct_timestamp ,
274+ sct_extensions_len : sct_extensions_len ,
275+ sct_signature_alg_hash : sct_signature_alg_hash ,
276+ sct_signature_alg_sign : sct_signature_alg_sign ,
277+ sct_signature_len : sct_signature_len ,
278+ sct_signature_bytes : sct_signature_bytes
279+ }
124280 end
281+ raise "offset=#{ offset } len=#{ len } " unless offset == len
282+
283+ list
284+ end
285+
286+ def find_issuer_cert ( chain )
287+ issuer = chain [ 0 ]
288+ issuer = chain [ 1 ] if preissuer? ( issuer )
289+ raise "issuer not found" unless issuer
125290
126- # TODO: parse the SCT properly
127- [ nil ]
291+ issuer
292+ end
293+
294+ def preissuer? ( cert )
295+ return false unless ( eku = cert . find_extension ( "extendedKeyUsage" ) )
296+
297+ values = OpenSSL ::ASN1 . decode ( eku . value_der ) . value
298+ raise values . inspect unless values . is_a? ( Array )
299+
300+ values . any? do
301+ _1 . oid == "1.3.6.1.4.1.11129.2.4.4"
302+ end
128303 end
129304 end
130305end
0 commit comments