1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
|
require 'rubygems/user_interaction'
##
# 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 in
# Gem::Security::Policies.
class Gem::Security::Policy
include Gem::UserInteraction
attr_reader :name
attr_accessor :only_signed
attr_accessor :only_trusted
attr_accessor :verify_chain
attr_accessor :verify_data
attr_accessor :verify_root
attr_accessor :verify_signer
##
# Create a new Gem::Security::Policy object with the given mode and
# options.
def initialize name, policy = {}, opt = {}
require 'openssl'
@name = name
@opt = opt
# Default to security
@only_signed = true
@only_trusted = true
@verify_chain = true
@verify_data = true
@verify_root = true
@verify_signer = true
policy.each_pair do |key, val|
case key
when :verify_data then @verify_data = val
when :verify_signer then @verify_signer = val
when :verify_chain then @verify_chain = val
when :verify_root then @verify_root = val
when :only_trusted then @only_trusted = val
when :only_signed then @only_signed = val
end
end
end
##
# Verifies each certificate in +chain+ has signed the following certificate
# and is valid for the given +time+.
def check_chain chain, time
raise Gem::Security::Exception, 'missing signing chain' unless chain
raise Gem::Security::Exception, 'empty signing chain' if chain.empty?
begin
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
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.digest
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
raise Gem::Security::Exception, 'missing signing certificate' unless
signer
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 public key of +key+ matches the public key in +signer+
def check_key signer, key
unless signer and key then
return true unless @only_signed
raise Gem::Security::Exception, 'missing key or signature'
end
raise Gem::Security::Exception,
"certificate #{signer.subject} does not match the signing key" unless
signer.public_key.to_pem == key.public_key.to_pem
true
end
##
# Ensures the root certificate in +chain+ is self-signed and valid for
# +time+.
def check_root chain, time
raise Gem::Security::Exception, 'missing signing chain' unless chain
root = chain.first
raise Gem::Security::Exception, 'missing root certificate' unless root
raise Gem::Security::Exception,
"root certificate #{root.subject} is not self-signed " +
"(issuer #{root.issuer})" if
root.issuer.to_s != root.subject.to_s # HACK to_s is for ruby 1.8
check_cert root, root, time
end
##
# Ensures the root of +chain+ has a trusted certificate in +trust_dir+ and
# the digests of the two certificates match according to +digester+
def check_trust chain, digester, trust_dir
raise Gem::Security::Exception, 'missing signing chain' unless chain
root = chain.first
raise Gem::Security::Exception, 'missing root certificate' unless root
path = Gem::Security.trust_dir.cert_path root
unless File.exist? path then
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
save_cert = OpenSSL::X509::Certificate.new File.read path
save_dgst = digester.digest save_cert.public_key.to_s
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
##
# Extracts the email or subject from +certificate+
def subject certificate # :nodoc:
certificate.extensions.each do |extension|
next unless extension.oid == 'subjectAltName'
return extension.value
end
certificate.subject.to_s
end
def inspect # :nodoc:
("[Policy: %s - data: %p signer: %p chain: %p root: %p " +
"signed-only: %p trusted-only: %p]") % [
@name, @verify_chain, @verify_data, @verify_root, @verify_signer,
@only_signed, @only_trusted,
]
end
##
# For +full_name+, verifies the certificate +chain+ is valid, the +digests+
# match the signatures +signatures+ created by the signer depending on the
# +policy+ settings.
#
# If +key+ is given it is used to validate the signing certificate.
def verify chain, key = nil, digests = {}, signatures = {},
full_name = '(unknown)'
if signatures.empty? then
if @only_signed then
raise Gem::Security::Exception,
"unsigned gems are not allowed by the #{name} policy"
elsif digests.empty? then
# lack of signatures is irrelevant if there is nothing to check
# against
else
alert_warning "#{full_name} is not signed"
return
end
end
opt = @opt
digester = Gem::Security::DIGEST_ALGORITHM
trust_dir = opt[:trust_dir]
time = Time.now
_, signer_digests = digests.find do |algorithm, file_digests|
file_digests.values.first.name == Gem::Security::DIGEST_NAME
end
if @verify_data then
raise Gem::Security::Exception, 'no digests provided (probable bug)' if
signer_digests.nil? or signer_digests.empty?
else
signer_digests = {}
end
signer = chain.last
check_key signer, key if key
check_cert signer, nil, time if @verify_signer
check_chain chain, time if @verify_chain
check_root chain, time if @verify_root
if @only_trusted then
check_trust chain, digester, trust_dir
elsif signatures.empty? and digests.empty? then
# trust is irrelevant if there's no signatures to verify
else
alert_warning "#{subject signer} is not trusted for #{full_name}"
end
signatures.each do |file, _|
digest = signer_digests[file]
raise Gem::Security::Exception, "missing digest for #{file}" unless
digest
end
signer_digests.each do |file, digest|
signature = signatures[file]
raise Gem::Security::Exception, "missing signature for #{file}" unless
signature
check_data signer.public_key, digester, signature, digest if @verify_data
end
true
end
##
# Extracts the certificate chain from the +spec+ and calls #verify to ensure
# the signatures and certificate chain is valid according to the policy..
def verify_signatures spec, digests, signatures
chain = spec.cert_chain.map do |cert_pem|
OpenSSL::X509::Certificate.new cert_pem
end
verify chain, nil, digests, signatures, spec.full_name
true
end
alias to_s name # :nodoc:
end
|