模块 Gem::Security
签名 gem 包¶ ↑
Gem::Security
实现了 gem 包的加密签名。下面的部分是使用签名 gem 包和生成你自己的签名 gem 包的分步指南。
演练¶ ↑
构建你的证书¶ ↑
为了开始签名你的 gem 包,你需要构建一个私钥和一个自签名证书。以下是如何操作:
# build a private key and certificate for yourself: $ gem cert --build you@example.com
这可能需要几秒钟到一两分钟的时间,具体取决于你的计算机速度(公钥算法并不是世界上最快的加密算法)。完成后,你将在当前目录中看到文件 “gem-private_key.pem” 和 “gem-public_cert.pem”。
首先:如果你在该目录中还没有密钥和证书,请将这两个文件都移动到 ~/.gem。确保文件权限使其他人无法读取密钥(默认情况下,文件会安全保存)。
隐藏你的私钥;如果它被泄露,其他人可以以你的身份签署软件包(注意:PKI 有办法减轻密钥被盗的风险;稍后会详细介绍)。
签名 Gem 包¶ ↑
在 RubyGems 2 和更新的版本中,签名 gem 包无需额外操作。RubyGems 将自动在你的主目录中找到你的密钥和证书,并使用它们来签名新打包的 gem 包。
如果你的证书不是自签名的(由第三方签名),RubyGems 将尝试从受信任的证书加载证书链。使用 gem cert --add signing_cert.pem
将你的签名者添加为受信任的证书。有关证书链的更多信息,请参见下文。
如果你构建你的 gem 包,它将自动被签名。如果你查看你的 gem 文件内部,你会看到添加了几个新文件:
$ tar tf your-gem-1.0.gem metadata.gz metadata.gz.sig # metadata signature data.tar.gz data.tar.gz.sig # data signature checksums.yaml.gz checksums.yaml.gz.sig # checksums signature
手动签名 gem 包¶ ↑
如果你希望将密钥存储在单独的安全位置,你需要手动设置你的 gem 包进行签名。为此,在打包你的 gem 包之前,在 gemspec 中设置 signing_key
和 cert_chain
:
s.signing_key = '/secure/path/to/gem-private_key.pem' s.cert_chain = %w[/secure/path/to/gem-public_cert.pem]
当你使用这些选项设置打包你的 gem 包时,RubyGems 将自动从安全路径加载你的密钥和证书。
签名 gem 包和安全策略¶ ↑
现在让我们验证签名。继续并安装 gem 包,但添加以下选项:-P HighSecurity
,像这样:
# install the gem with using the security policy "HighSecurity" $ sudo gem install your.gem -P HighSecurity
-P
选项设置你的安全策略——我们稍后会讨论这个问题。嗯,这是怎么回事?
$ gem install -P HighSecurity your-gem-1.0.gem ERROR: While executing gem ... (Gem::Security::Exception) root cert /CN=you/DC=example is not trusted
罪魁祸首是安全策略。RubyGems 有几种不同的安全策略。让我们稍作休息,回顾一下安全策略。以下是可用的安全策略列表,以及每个策略的简要说明:
-
NoSecurity
- 嗯,根本没有安全措施。签名包被视为未签名包。 -
LowSecurity
- 几乎没有安全措施。如果一个包被签名,那么 RubyGems 将确保签名与签名证书匹配,并且签名证书没有过期,仅此而已。恶意用户可以很容易地规避这种安全措施。 -
MediumSecurity
- 比LowSecurity
和NoSecurity
好,但仍然容易出错。Package
的内容会针对签名证书进行验证,并且签名证书会检查有效性,并针对证书链的其余部分进行检查(如果你不知道什么是证书链,请继续关注,我们稍后会讲到)。相比LowSecurity
,最大的改进是MediumSecurity
不会安装来自不受信任来源签名的包。不幸的是,MediumSecurity
仍然不是完全安全的——恶意用户仍然可以解压 gem 包,剥离签名,并分发未签名的 gem 包。 -
HighSecurity
- 这就是让我们陷入困境的罪魁祸首。HighSecurity
策略与MediumSecurity
策略相同,只是它不允许使用未签名的 gem 包。恶意用户在这里没有太多选择;他们无法修改软件包内容而不使签名失效,并且他们无法修改或删除签名或签名证书链,否则 RubyGems 将拒绝安装该软件包。好吧,也许他们可以更好地为 CPAN 用户制造麻烦:)。
RubyGems 拒绝安装你闪亮的新签名 gem 包的原因是它来自不受信任的来源。好吧,你的代码是无懈可击的(自然),所以你需要将自己添加为受信任的来源:
# add trusted certificate gem cert --add ~/.gem/gem-public_cert.pem
你现在已将你的公共证书添加为受信任的来源。现在,你可以安装由你的私钥签名的软件包,而没有任何麻烦。让我们再次尝试上面的安装命令:
# install the gem with using the HighSecurity policy (and this time # without any shenanigans) $ gem install -P HighSecurity your-gem-1.0.gem Successfully installed your-gem-1.0 1 gem installed
这次 RubyGems 将接受你的签名包并开始安装。
在你等待 RubyGems 发挥魔力的同时,请通过运行 gem help cert
查看其他一些安全命令:
Options: -a, --add CERT Add a trusted certificate. -l, --list [FILTER] List trusted certificates where the subject contains FILTER -r, --remove FILTER Remove trusted certificates where the subject contains FILTER -b, --build EMAIL_ADDR Build private key and self-signed certificate for EMAIL_ADDR -C, --certificate CERT Signing certificate for --sign -K, --private-key KEY Key for --sign or --build -A, --key-algorithm ALGORITHM Select key algorithm for --build from RSA, DSA, or EC. Defaults to RSA. -s, --sign CERT Signs CERT with the key from -K and the certificate from -C -d, --days NUMBER_OF_DAYS Days before the certificate expires -R, --re-sign Re-signs the certificate from -C with the key from -K
我们已经介绍了 --build
选项,而 --add
、--list
和 --remove
命令似乎相当简单;它们允许你添加、列出和删除受信任证书列表中的证书。但是这个 --sign
选项是什么意思?
证书链¶ ↑
为了回答这个问题,让我们看一下“证书链”,这是我之前提到过的一个概念。自签名证书存在一些问题:首先,自签名证书没有提供太多安全保障。当然,证书上写着 Yukihiro Matsumoto,但是我怎么知道它实际上是由 matz 本人生成和签名的,除非他亲自把证书给我?
第二个问题是可伸缩性。当然,如果有 50 个 gem 包作者,那么我有 50 个受信任的证书,没问题。如果有 500 个 gem 包作者呢?1000 个呢?必须不断添加新的受信任证书很麻烦,而且实际上会通过鼓励 RubyGems 用户盲目信任新证书而使信任系统变得不那么安全。
这就是证书链的用武之地。证书链在颁发证书和子证书之间建立任意长的信任链。因此,我们不按每个开发人员信任证书,而是使用 PKI 证书链的概念来构建逻辑信任层次结构。以下是基于(大致)地理位置的假设信任层次结构的示例:
-------------------------- | rubygems@rubygems.org | -------------------------- | ----------------------------------- | | ---------------------------- ----------------------------- | seattlerb@seattlerb.org | | dcrubyists@richkilmer.com | ---------------------------- ----------------------------- | | | | --------------- ---------------- ----------- -------------- | drbrain | | zenspider | | pabs@dc | | tomcope@dc | --------------- ---------------- ----------- --------------
现在,用户不需要 4 个受信任的证书(drbrain、zenspider、pabs@dc 和 tomecope@dc 各一个),实际上可以使用一个证书,即 “rubygems@rubygems.org” 证书。
它是这样工作的:
我安装了 “rdoc-3.12.gem”,这是一个由 “drbrain” 签名的软件包。我从未听说过 “drbrain”,但他的证书具有 “seattle.rb@seattlerb.org” 证书的有效签名,而该证书又具有 “rubygems@rubygems.org” 证书的有效签名。瞧!此时,我信任由 “drbrain” 签名的软件包要合理得多,因为我可以建立一条到我确实信任的 “rubygems@rubygems.org” 的链。
签名证书¶ ↑
--sign
选项允许所有这一切发生。开发人员使用 --build
选项创建他们的构建证书,然后将其带到他们下一个区域 Ruby 会议(在我们的假设示例中)并签署证书,该证书由持有区域 RubyGems 签名证书的人员在那里签名,该签名证书在下一个 RubyConf 上由顶级 RubyGems 证书的持有者签名。在每个点,颁发者都会运行相同的命令:
# sign a certificate with the specified key and certificate # (note that this modifies client_cert.pem!) $ gem cert -K /mnt/floppy/issuer-priv_key.pem -C issuer-pub_cert.pem --sign client_cert.pem
然后,已颁发证书的持有者(在本例中为你的朋友 “drbrain”)可以开始使用此签名证书来签名 RubyGems。顺便说一句,为了让其他所有人了解他的新签名证书,“drbrain” 会将他的新签名证书保存为 ~/.gem/gem-public_cert.pem
:
显然,这种 RubyGems 信任基础设施尚不存在。此外,在“现实世界”中,颁发者实际上是从证书请求生成子证书,而不是对现有证书进行签名。而且,我们假设的基础设施缺少证书撤销系统。这些可以在将来修复……
此时,你应该知道如何执行所有这些新的有趣的事情:
-
构建 gem 签名密钥和证书
-
调整你的安全策略
-
修改你的受信任证书列表
-
签名证书
手动验证签名¶ ↑
如果你不信任 RubyGems,可以手动验证 gem 签名:
-
获取并解压 gem 包
gem fetch some_signed_gem tar -xf some_signed_gem-1.0.gem
-
从 gemspec 获取公钥
gem spec some_signed_gem-1.0.gem cert_chain | \ ruby -rpsych -e 'puts Psych.load($stdin)' > public_key.crt
-
生成 data.tar.gz 的 SHA1 哈希
openssl dgst -sha1 < data.tar.gz > my.hash
-
验证签名
openssl rsautl -verify -inkey public_key.crt -certin \ -in data.tar.gz.sig > verified.hash
-
将您的哈希值与已验证的哈希值进行比较
diff -s verified.hash my.hash
-
使用 metadata.gz 重复第 5 步和第 6 步
OpenSSL
参考¶ ↑
由 –build 和 –sign 生成的 .pem 文件是 PEM 文件。以下是一些有用的 OpenSSL
命令,用于操作它们
# convert a PEM format X509 certificate into DER format: # (note: Windows .cer files are X509 certificates in DER format) $ openssl x509 -in input.pem -outform der -out output.der # print out the certificate in a human-readable format: $ openssl x509 -in input.pem -noout -text
您也可以对私钥文件执行相同的操作
# convert a PEM format RSA key into DER format: $ openssl rsa -in input_key.pem -outform der -out output_key.der # print out the key in a human readable format: $ openssl rsa -in input_key.pem -noout -text
Bug/待办事项¶ ↑
-
无法定义系统范围的信任列表。
-
自定义安全策略(来自
YAML
文件等) -
生成签名证书请求的简单方法
-
支持 OCSP、SCVP、CRL 或其他某种形式的证书状态检查(列表按偏好顺序排列)
-
支持加密的私钥
-
某种半正式的信任层级结构(请参阅上面的冗长解释)
-
路径发现(对于没有自签名根证书的 gem 证书链)– 顺便说一句,由于我们没有这个功能,如果
Policy#verify_root
为 true(对于MediumSecurity
和HighSecurity
策略,它确实为 true),则证书链的根必须是自签名的 -
更好地解释 X509 命名(即,我们不必使用电子邮件地址)
-
遵守 AIA 字段(请参阅上面关于 OCSP 的说明)
-
遵守扩展限制
-
最好将证书链存储为 PKCS#7 或 PKCS#12 文件,而不是嵌入在元数据中的数组。
原始作者¶ ↑
Paul Duncan <pabs@pablotron.org> pablotron.org/
常量
- AlmostNoSecurity
-
几乎没有安全策略:仅验证签名证书是否是实际签署数据的一个。不尝试验证签名证书链。
此策略基本上没有用。比没有好,但仍然可以很容易地被欺骗,因此不建议使用。
- DEFAULT_KEY_ALGORITHM
-
构建密钥对时使用的默认算法
- EC_NAME
-
用于椭圆曲线的命名曲线
- EXTENSIONS
-
默认扩展集为
-
该证书不是证书颁发机构
-
证书的密钥可用于密钥和数据加密以及数字签名
-
证书包含主体密钥标识符
-
- HighSecurity
-
高安全策略:仅允许安装已签名的 gem,验证签名证书,验证签名证书链一直到根证书,并且仅信任我们已明确允许信任的根证书。
此安全策略更难以绕过,并提供合理的保证,即 gem 的内容没有被更改。
- KEY_CIPHER
-
用于加密用于签署 gem 的密钥对的密码。必须在
OpenSSL::Cipher.ciphers
返回的列表中。 - LowSecurity
-
低安全策略:仅验证签名证书实际上是 gem 签名者,并且签名证书有效。
此策略比没有好,但仍然可以很容易地被欺骗,因此不建议使用。
- MediumSecurity
-
中安全策略:验证签名证书,验证签名证书链一直到根证书,并且仅信任我们已明确允许信任的根证书。
此安全策略是合理的,但它允许未签名的软件包,因此恶意人员可以简单地删除软件包签名并将 gem 作为未签名的 gem 传递。
- NoSecurity
-
无安全策略:禁用所有软件包签名检查。
- ONE_DAY
-
一天以秒为单位
- ONE_YEAR
-
一年以秒为单位
- Policies
-
已配置安全策略的
Hash
- RSA_DSA_KEY_LENGTH
-
由 RSA 和 DSA 密钥创建的密钥长度
- SigningPolicy
-
用于在签署 gem 时验证证书和密钥的
Policy
公共类方法
来源
# File lib/rubygems/security.rb, line 385 def self.alt_name_or_x509_entry(certificate, x509_entry) alt_name = certificate.extensions.find do |extension| extension.oid == "#{x509_entry}AltName" end return alt_name.value if alt_name certificate.send x509_entry end
来源
# File lib/rubygems/security.rb, line 401 def self.create_cert(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) cert = OpenSSL::X509::Certificate.new cert.public_key = get_public_key(key) cert.version = 2 cert.serial = serial cert.not_before = Time.now cert.not_after = Time.now + age cert.subject = subject ef = OpenSSL::X509::ExtensionFactory.new nil, cert cert.extensions = extensions.map do |ext_name, value| ef.create_extension ext_name, value end cert end
为 subject
和 key
创建一个未签名的证书。密钥的生命周期从当前时间到 age
,默认为一年。
extensions
将密钥限制为指示的用途。
来源
# File lib/rubygems/security.rb, line 440 def self.create_cert_email(email, key, age = ONE_YEAR, extensions = EXTENSIONS) subject = email_to_name email extensions = extensions.merge "subjectAltName" => "email:#{email}" create_cert_self_signed subject, key, age, extensions end
使用来自 email
的颁发者和主体,email
的主体备用名称以及给定 key
的 extensions
创建自签名证书。
来源
# File lib/rubygems/security.rb, line 452 def self.create_cert_self_signed(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) certificate = create_cert subject, key, age, extensions sign certificate, key, certificate, age, extensions, serial end
使用 subject
的颁发者和主体以及给定 key
的 extensions
创建自签名证书。
来源
# File lib/rubygems/security.rb, line 462 def self.create_digest(algorithm = DIGEST_NAME) OpenSSL::Digest.new(algorithm) end
使用指定的 algorithm
创建新的摘要实例。默认为 SHA256。
来源
# File lib/rubygems/security.rb, line 470 def self.create_key(algorithm) if defined?(OpenSSL::PKey) case algorithm.downcase when "dsa" OpenSSL::PKey::DSA.new(RSA_DSA_KEY_LENGTH) when "rsa" OpenSSL::PKey::RSA.new(RSA_DSA_KEY_LENGTH) when "ec" OpenSSL::PKey::EC.generate(EC_NAME) else raise Gem::Security::Exception, "#{algorithm} algorithm not found. RSA, DSA, and EC algorithms are supported." end end end
创建指定 algorithm
的新密钥对。支持 RSA、DSA 和 EC。
来源
# File lib/rubygems/security.rb, line 489 def self.email_to_name(email_address) email_address = email_address.gsub(/[^\w@.-]+/i, "_") cn, dcs = email_address.split "@" dcs = dcs.split "." OpenSSL::X509::Name.new([ ["CN", cn], *dcs.map {|dc| ["DC", dc] }, ]) end
将 email_address
转换为 OpenSSL::X509::Name
来源
# File lib/rubygems/security.rb, line 425 def self.get_public_key(key) # Ruby 3.0 (Ruby/OpenSSL 2.2) or later return OpenSSL::PKey.read(key.public_to_der) if key.respond_to?(:public_to_der) return key.public_key unless key.is_a?(OpenSSL::PKey::EC) ec_key = OpenSSL::PKey::EC.new(key.group.curve_name) ec_key.public_key = key.public_key ec_key end
从 PKey 实例获取正确的公钥
来源
# File lib/rubygems/security.rb, line 508 def self.re_sign(expired_certificate, private_key, age = ONE_YEAR, extensions = EXTENSIONS) raise Gem::Security::Exception, "incorrect signing key for re-signing " + expired_certificate.subject.to_s unless expired_certificate.check_private_key(private_key) unless expired_certificate.subject.to_s == expired_certificate.issuer.to_s subject = alt_name_or_x509_entry expired_certificate, :subject issuer = alt_name_or_x509_entry expired_certificate, :issuer raise Gem::Security::Exception, "#{subject} is not self-signed, contact #{issuer} " \ "to obtain a valid certificate" end serial = expired_certificate.serial + 1 create_cert_self_signed(expired_certificate.subject, private_key, age, extensions, serial) end
如果密钥匹配并且过期的证书是自签名的,则使用 private_key
签署 expired_certificate
。
来源
# File lib/rubygems/security.rb, line 533 def self.reset @trust_dir = nil end
重置用于验证 gem 的信任目录。
来源
# File lib/rubygems/security.rb, line 544 def self.sign(certificate, signing_key, signing_cert, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) signee_subject = certificate.subject signee_key = certificate.public_key alt_name = certificate.extensions.find do |extension| extension.oid == "subjectAltName" end extensions = extensions.merge "subjectAltName" => alt_name.value if alt_name issuer_alt_name = signing_cert.extensions.find do |extension| extension.oid == "subjectAltName" end extensions = extensions.merge "issuerAltName" => issuer_alt_name.value if issuer_alt_name signed = create_cert signee_subject, signee_key, age, extensions, serial signed.issuer = signing_cert.subject signed.sign signing_key, Gem::Security::DIGEST_NAME end
使用 signing_key
和 signing_cert
以及 Gem::Security::DIGEST_NAME 对来自 certificate
的公钥进行签名。使用默认的证书有效期范围和扩展。
返回新签名的证书。
来源
# File lib/rubygems/security.rb, line 572 def self.trust_dir return @trust_dir if @trust_dir dir = File.join Gem.user_home, ".gem", "trust" @trust_dir ||= Gem::Security::TrustDir.new dir end
返回一个 Gem::Security::TrustDir
,它包装了受信任证书所在的目录。
来源
# File lib/rubygems/security.rb, line 583 def self.trusted_certificates(&block) trust_dir.each_certificate(&block) end
通过 Gem::Security::TrustDir
枚举受信任的证书。
来源
# File lib/rubygems/security.rb, line 592 def self.write(pemmable, path, permissions = 0o600, passphrase = nil, cipher = KEY_CIPHER) path = File.expand_path path File.open path, "wb", permissions do |io| if passphrase && cipher io.write pemmable.to_pem cipher, passphrase else io.write pemmable.to_pem end end path end
使用给定的 permissions
将必须响应 to_pem
的 pemmable
写入 path
。如果传递了 cipher
和 passphrase
,这些参数将传递给 to_pem
。