Skip to content

Commit

Permalink
Separated Gem::Security::Policy#verify_signature into individually te…
Browse files Browse the repository at this point in the history
…stable methods.
  • Loading branch information
drbrain committed Jan 16, 2012
1 parent 6a8fa74 commit 2514bd1
Show file tree
Hide file tree
Showing 25 changed files with 593 additions and 102 deletions.
204 changes: 119 additions & 85 deletions lib/rubygems/security/policy.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
##
# A Gem::Security::Policy object encapsulates the settings for verifying
# signed gem files. This is the base class. You can either declare an
# instance of this or use one of the preset security policies below.
# instance of this or use one of the preset security policies in
# Gem::Security::Policies.

class Gem::Security::Policy

Expand Down Expand Up @@ -37,111 +38,144 @@ def initialize name, policy = {}, opt = {}

##
# Get the path to the file for this cert.
#--
# TODO move to Gem::Security

def self.trusted_cert_path cert, opt = {}
opt = Gem::Security::OPT.merge opt

algo = opt[:dgst_algo]
dgst = algo.hexdigest cert.subject.to_s
digester = opt[:dgst_algo]
digest = digester.hexdigest cert.subject.to_s

name = "cert-#{dgst}.pem"
name = "cert-#{digest}.pem"

File.join opt[:trust_dir], name
end

##
# Verifies each certificate in +chain+ has signed the following certificate
# and is valid for the given +time+.

def check_chain chain, time
chain.each_cons 2 do |issuer, cert|
check_cert cert, issuer, time
end

true
rescue Gem::Security::Exception => e
raise Gem::Security::Exception, "invalid signing chain: #{e.message}"
end

##
# Verifies that +data+ matches the +signature+ created by +public_key+ and
# the +digest+ algorithm.

def check_data public_key, digest, signature, data
raise Gem::Security::Exception, "invalid signature" unless
public_key.verify digest.new, signature, data

true
end

##
# Ensures that +signer+ is valid for +time+ and was signed by the +issuer+.
# If the +issuer+ is +nil+ no verification is performed.

def check_cert signer, issuer, time
message = "certificate #{signer.subject}"

if not_before = signer.not_before and not_before > time then
raise Gem::Security::Exception,
"#{message} not valid before #{not_before}"
end

if not_after = signer.not_after and not_after < time then
raise Gem::Security::Exception, "#{message} not valid after #{not_after}"
end

if issuer and not signer.verify issuer.public_key then
raise Gem::Security::Exception,
"#{message} was not issued by #{issuer.subject}"
end

true
end

##
# Ensures the root certificate in +chain+ is self-signed and valid for
# +time+.

def check_root chain, time
root = chain.first

raise Gem::Security::Exception,
"root certificate #{root.subject} is not self-signed " \
"(issuer #{root.issuer})" if
root.issuer != root.subject

check_cert root, root, time
end

def check_trust chain, digester, trust_dir
root = chain.first

# get digest algorithm, calculate checksum of root.subject
path = Gem::Security::Policy.trusted_cert_path(root,
:trust_dir => trust_dir,
:digester => digester)

# check to make sure trusted path exists
unless File.exist? path
message = "root cert #{root.subject} is not trusted"

message << " (root of signing cert #{chain.last.subject})" if
chain.length > 1

raise Gem::Security::Exception, message
end

# load calculate digest from saved cert file
save_cert = OpenSSL::X509::Certificate.new File.read path
save_dgst = digester.digest save_cert.public_key.to_s

# create digest of public key
pkey_str = root.public_key.to_s
cert_dgst = digester.digest pkey_str

raise Gem::Security::Exception,
"trusted root certificate #{root.subject} checksum " \
"does not match signing root certificate checksum" unless
save_dgst == cert_dgst

true
end

##
# Verify that the gem data with the given signature and signing chain
# matched this security policy at the specified time.

def verify_signature signature, data, chain, time = Time.now
Gem.ensure_ssl_available
exc = Gem::Security::Exception
chain ||= []

chain = chain.map { |cert| OpenSSL::X509::Certificate.new cert }
signer, ch_len = chain[-1], chain.size
opt = Gem::Security::OPT.merge @opt

# make sure signature is valid
if @verify_data then
dgst = opt[:dgst_algo]

# verify the data signature (this is the most important part, so don't
# screw it up :D)
v = signer.public_key.verify dgst.new, signature, data
raise exc, "Invalid Gem Signature" unless v

# make sure the signer is valid
if @verify_signer
# make sure the signing cert is valid right now
v = signer.check_validity nil, time
raise exc, "Invalid Signature: #{v[:desc]}" unless v[:is_valid]
end
end
signer = chain.last

# make sure the certificate chain is valid
if @verify_chain
# iterate down over the chain and verify each certificate against it's
# issuer
(ch_len - 1).downto 1 do |i|
issuer, cert = chain[i - 1, 2]
v = cert.check_validity issuer, time
raise exc, "%s: cert = '%s', error = '%s'" % [
'Invalid Signing Chain', cert.subject, v[:desc]
] unless v[:is_valid]
end
opt = Gem::Security::OPT.merge @opt
digester = opt[:dgst_algo]
trust_dir = opt[:trust_dir]

# verify root of chain
if @verify_root
# make sure root is self-signed
root = chain[0]
raise exc, "%s: %s (subject = '%s', issuer = '%s')" % [
'Invalid Signing Chain Root',
'Subject does not match Issuer for Gem Signing Chain',
root.subject.to_s,
root.issuer.to_s,
] unless root.issuer.to_s == root.subject.to_s

# make sure root is valid
v = root.check_validity root, time
raise exc, "%s: cert = '%s', error = '%s'" % [
'Invalid Signing Chain Root', root.subject, v[:desc]
] unless v[:is_valid]

# verify that the chain root is trusted
if @only_trusted
# get digest algorithm, calculate checksum of root.subject
algo = opt[:dgst_algo]
path = Gem::Security::Policy.trusted_cert_path root, opt

# check to make sure trusted path exists
raise exc, "%s: cert = '%s', error = '%s'" % [
'Untrusted Signing Chain Root',
root.subject.to_s,
"path \"#{path}\" does not exist",
] unless File.exist? path

# load calculate digest from saved cert file
save_cert = OpenSSL::X509::Certificate.new File.read path
save_dgst = algo.digest save_cert.public_key.to_s

# create digest of public key
pkey_str = root.public_key.to_s
cert_dgst = algo.digest pkey_str

# now compare the two digests, raise exception
# if they don't match
raise exc, "%s: %s (saved = '%s', root = '%s')" % [
'Invalid Signing Chain Root',
"Saved checksum doesn't match root checksum",
save_dgst, cert_dgst,
] unless save_dgst == cert_dgst
end
end
check_data signer.public_key, digester, signature, data if @verify_data

# return the signing chain
chain.map { |cert| cert.subject }
end
check_cert signer, nil, time if @verify_signer

check_chain chain, time if @verify_chain

check_root chain, time if @verify_root

check_trust chain, digester, trust_dir if @only_trusted

true
end

##
Expand Down
54 changes: 38 additions & 16 deletions lib/rubygems/test_case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,22 +87,6 @@ module DefaultUserInteraction

class Gem::TestCase < MiniTest::Unit::TestCase

private_key = File.expand_path('../../../test/rubygems/private_key.pem',
__FILE__)
private_key = File.read private_key
PRIVATE_KEY = OpenSSL::PKey::RSA.new private_key

public_cert = if 32 == (Time.at(2**32) rescue 32) then
'public_cert_32.pem'
else
'public_cert.pem'
end

public_cert = File.expand_path("../../../test/rubygems/#{public_cert}",
__FILE__)
public_cert = File.read public_cert
PUBLIC_CERT = OpenSSL::X509::Certificate.new public_cert

# TODO: move to minitest
def assert_path_exists path, msg = nil
msg = message(msg) { "Expected path '#{path}' to exist" }
Expand Down Expand Up @@ -942,5 +926,43 @@ def prefetch(reqs)
end
end

##
# Loads certificate name +cert_name+ from <tt>test/rubygems/</tt>. If
# +thirty_two+ is true and this system does not support a 64 bit time_t a
# certificate expiring in 2038 when time_t rolls over is returned.

def self.load_cert cert_name
if 32 == (Time.at(2**32) rescue 32) then
cert_file =
File.expand_path "../../../test/rubygems/#{cert_name}_32.pem", __FILE__

cert_file = nil unless File.exist? cert_file
end

cert_file ||=
File.expand_path "../../../test/rubygems/#{cert_name}_cert.pem", __FILE__

cert = File.read cert_file

OpenSSL::X509::Certificate.new cert
end

##
# Loads an RSA private key named +key_name+ in <tt>test/rubygems/</tt>

def self.load_key key_name
key_file =
File.expand_path "../../../test/rubygems/#{key_name}_key.pem", __FILE__

key = File.read key_file

OpenSSL::PKey::RSA.new key
end

PRIVATE_KEY = load_key 'private'
PUBLIC_KEY = PRIVATE_KEY.public_key

PUBLIC_CERT = load_cert 'public'

end

9 changes: 9 additions & 0 deletions test/rubygems/alternate_cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE-----
MIIBSjCB9aADAgECAgEBMA0GCSqGSIb3DQEBBQUAMC0xEjAQBgNVBAMMCWFsdGVy
bmF0ZTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUwIBcNMTIwMTEyMjMwNzU4WhgP
OTk5OTEyMzEyMzU5NTlaMC0xEjAQBgNVBAMMCWFsdGVybmF0ZTEXMBUGCgmSJomT
8ixkARkWB2V4YW1wbGUwXDANBgkqhkiG9w0BAQEFAANLADBIAkEA0lN/jShlFg0t
+h1L2gkdi2UXFnfLnXNh+J3Jolh4/Pd8rdzKTdnCep2nqDIRimnlEjO1+I3u7aur
Uc4b1AhX5QIDAQABMA0GCSqGSIb3DQEBBQUAA0EADQjHAq3zfeBW+TuWhb8eZmlE
/9mExNGisVY3lRHBvkEWY28ZIEB3R2pXsQMKsb8eWwa5Tv/l1vwkPhoyd7/OLQ==
-----END CERTIFICATE-----
9 changes: 9 additions & 0 deletions test/rubygems/alternate_cert_32.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE-----
MIIBSDCB86ADAgECAgEBMA0GCSqGSIb3DQEBBQUAMC0xEjAQBgNVBAMMCWFsdGVy
bmF0ZTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUwHhcNMTIwMTEyMjMwODI5WhcN
MzgwMTE5MDMxNDA3WjAtMRIwEAYDVQQDDAlhbHRlcm5hdGUxFzAVBgoJkiaJk/Is
ZAEZFgdleGFtcGxlMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANJTf40oZRYNLfod
S9oJHYtlFxZ3y51zYfidyaJYePz3fK3cyk3Zwnqdp6gyEYpp5RIztfiN7u2rq1HO
G9QIV+UCAwEAATANBgkqhkiG9w0BAQUFAANBAH9YxSERfWdqipZbQGDmx2F8e5O2
6zbDialEy+bD8juhzWa6nIPaoQzeSkD0lw98OXpVyE0vhnGS1rlVH3725Xw=
-----END CERTIFICATE-----
9 changes: 9 additions & 0 deletions test/rubygems/alternate_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBANJTf40oZRYNLfodS9oJHYtlFxZ3y51zYfidyaJYePz3fK3cyk3Z
wnqdp6gyEYpp5RIztfiN7u2rq1HOG9QIV+UCAwEAAQJAAM1kvSscR5tBQ6T89xX1
0ORP1DwcwLsB8qrXbBEeVQiy4aAcfUgKbMxiSEL3iogoHnKK6VFmu4bH4v360dJl
OQIhAO97aiKxP6u+Y1oNyScF2lViu81xR4jlL/DjD8I+oBr/AiEA4NVFey13ks3U
2DTHU3qhU6y+MmYv0aCInf49hMt1gRsCIQCvSiR71EWIjkjmh6Su3Yfca/KPA9Wa
jDc1GN5WDcTfPQIgaYzWLKjAvQOyi7njZdZpTG4JcIG2q1QRLxL/cItP48cCIFvI
/R7PnPrVdF5G2QT7DZyrHjjtqnyW76zwU3MSxZMJ
-----END RSA PRIVATE KEY-----
9 changes: 9 additions & 0 deletions test/rubygems/child_cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE-----
MIIBQzCB7qADAgECAgEBMA0GCSqGSIb3DQEBBQUAMCoxDzANBgNVBAMMBm5vYm9k
eTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUwIBcNMTIwMTEzMDAxNTE4WhgPOTk5
OTEyMzEyMzU5NTlaMCkxDjAMBgNVBAMMBWNoaWxkMRcwFQYKCZImiZPyLGQBGRYH
ZXhhbXBsZTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC93Mml1yQmsUDalFT1PH4z
z8Al8ki969L/NHUWdkG6DuM0krWiWPBkO+Moj+6NMH2TrGWyHPfYv5zQ3zn9ai+d
AgMBAAEwDQYJKoZIhvcNAQEFBQADQQCKb9V9Wl6hQNuDsn38Ceg6gCh9hXqvHrCz
/pzoFjTxZfsYCNapXp1hzb8cUijzi2YalXzUpg7zgEowLZpZD94s
-----END CERTIFICATE-----
9 changes: 9 additions & 0 deletions test/rubygems/child_cert_32.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE-----
MIIBQzCB7qADAgECAgEBMA0GCSqGSIb3DQEBBQUAMCoxDzANBgNVBAMMBm5vYm9k
eTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUwIBcNMTIwMTEzMDAxNTE4WhgPMjAz
ODAxMTkwMzE0MDdaMCkxDjAMBgNVBAMMBWNoaWxkMRcwFQYKCZImiZPyLGQBGRYH
ZXhhbXBsZTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC93Mml1yQmsUDalFT1PH4z
z8Al8ki969L/NHUWdkG6DuM0krWiWPBkO+Moj+6NMH2TrGWyHPfYv5zQ3zn9ai+d
AgMBAAEwDQYJKoZIhvcNAQEFBQADQQB7JhifNXK4PTYVZv5NYmqyi4ze7h2sxxNl
hqjzIj+GFsf/5uFt0Oz/n8VCE/LEWVKhejiyiaLxN7nLjszW7EGq
-----END CERTIFICATE-----
9 changes: 9 additions & 0 deletions test/rubygems/child_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAL3cyaXXJCaxQNqUVPU8fjPPwCXySL3r0v80dRZ2QboO4zSStaJY
8GQ74yiP7o0wfZOsZbIc99i/nNDfOf1qL50CAwEAAQJAEBEhF2Gvc37IiDbJQb+O
xn+sOQnZ0gik4GfmCLKVOVNn4mxJI2fJe/6qUyt5Bp0nTuY+jfx9XFIlx3A3QZY6
mQIhAN0E92rtcTvTy5lPO+hAeUQu59q6+HzC7HJJOevPWlqrAiEA2+lvvNOyZXUD
VmlyexQ1VdNLQtqRWtQd2PBtg0W4HtcCIAU7C9v0+JwQ3B2puWI6vGP3tDcB+8WA
G3Zs8zj4dEv3AiBdSLB1UPejwfcgeKpvOytEtNSAD2cJOGFQ3OwXe0OXEQIhAJXR
I7JTU0DJF+dqpR7jr1hQSXibdEXP7MIJUSK4uLUs
-----END RSA PRIVATE KEY-----
9 changes: 9 additions & 0 deletions test/rubygems/expired_cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE-----
MIIBQjCB7aADAgECAgEBMA0GCSqGSIb3DQEBBQUAMCoxDzANBgNVBAMMBm5vYm9k
eTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUwHhcNNzAwMTAxMDAwMDAwWhcNNzAw
MTAxMDAwMDAwWjAqMQ8wDQYDVQQDDAZub2JvZHkxFzAVBgoJkiaJk/IsZAEZFgdl
eGFtcGxlMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALHKBD9e+mrCnQ7Xs2fdEhxa
+0mpObjlK7dL6hoNF190aoLwQ6xwiCAMT7WPVs4/XW81Nssq2RkHGiWGxIXnMlsC
AwEAATANBgkqhkiG9w0BAQUFAANBAE3nKONBLbE97pTsIpM6v8bzCVhtVPfS+6F7
6WnIDuVUyN0pmVlWqRuEm8WZtrwnu1D6CGNTWwescrIEbQV6dvc=
-----END CERTIFICATE-----
9 changes: 9 additions & 0 deletions test/rubygems/future_cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE-----
MIIBRjCB8aADAgECAgEBMA0GCSqGSIb3DQEBBQUAMCoxDzANBgNVBAMMBm5vYm9k
eTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUwIhgPOTk5OTEyMzEyMzU5NThaGA85
OTk5MTIzMTIzNTk1OVowKjEPMA0GA1UEAwwGbm9ib2R5MRcwFQYKCZImiZPyLGQB
GRYHZXhhbXBsZTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCxygQ/Xvpqwp0O17Nn
3RIcWvtJqTm45Su3S+oaDRdfdGqC8EOscIggDE+1j1bOP11vNTbLKtkZBxolhsSF
5zJbAgMBAAEwDQYJKoZIhvcNAQEFBQADQQB0blKxIt6IfzaHHBgMfJwkwtEoDEey
OEtjX5oz9L0P+l/KjssEFk4fzPiVFHmx9y3KCXhapVvHcgeY7IKGHK7z
-----END CERTIFICATE-----
9 changes: 9 additions & 0 deletions test/rubygems/future_cert_32.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE-----
MIIBSDCB86ADAgECAgEBMA0GCSqGSIb3DQEBBQUAMC0xEjAQBgNVBAMMCWFsdGVy
bmF0ZTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUwHhcNMzgwMTE5MDMxNDA2WhcN
MzgwMTE5MDMxNDA3WjAtMRIwEAYDVQQDDAlhbHRlcm5hdGUxFzAVBgoJkiaJk/Is
ZAEZFgdleGFtcGxlMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALHKBD9e+mrCnQ7X
s2fdEhxa+0mpObjlK7dL6hoNF190aoLwQ6xwiCAMT7WPVs4/XW81Nssq2RkHGiWG
xIXnMlsCAwEAATANBgkqhkiG9w0BAQUFAANBAG7xn/s1GqtjdNA92OOwLP84N7hQ
Xa4tBtYaP84rC4LJhkYWdhTIM4YM3UpG9obxL1Xh1+92kLTN3/KpLK4EcQQ=
-----END CERTIFICATE-----
Loading

0 comments on commit 2514bd1

Please sign in to comment.