模块 Gem::Security

签名 gem 包

Gem::Security 为 gem 包实现加密签名。以下部分是使用签名 gem 包并生成您自己的 gem 包的分步指南。

演练

构建您的证书

为了开始签名您的 gem 包,您需要构建一个私钥和一个自签名证书。步骤如下:

# build a private key and certificate for yourself:
$ gem cert --build [email protected]

这可能需要几秒到一两分钟的时间,具体取决于您的计算机速度(公钥算法并不是世界上速度最快的加密算法)。完成后,您将在当前目录中看到“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_keycert_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 - 比 LowSecurityNoSecurity 更好,但仍然会出错。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 概念来构建信任的逻辑层次结构。以下是基于(大致)地理位置的假设信任层次结构的示例。

                    --------------------------
                    | [email protected] |
                    --------------------------
                                |
              -----------------------------------
              |                                 |
  ----------------------------    -----------------------------
  |  [email protected] |    | [email protected] |
  ----------------------------    -----------------------------
       |                |                 |             |
---------------   ----------------   -----------   --------------
|   drbrain   |   |   zenspider  |   | pabs@dc |   | tomcope@dc |
---------------   ----------------   -----------   --------------

现在,用户不必拥有 4 个受信任的证书(分别针对 drbrain、zenspider、pabs@dc 和 tomecope@dc),而实际上可以使用一个证书,即“[email protected]”证书。

工作原理如下:

我安装了 “rdoc-3.12.gem”,这是一个由 “drbrain” 签名的软件包。我从未听说过“drbrain”,但是他的证书具有来自“[email protected]”证书的有效签名,而该证书又具有来自“[email protected]”证书的有效签名。瞧!在这一点上,我信任由 “drbrain” 签名的软件包要合理得多,因为我可以建立一条指向我信任的 “[email protected]” 的链。

签名证书

--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 包签名。

  1. 获取并解压 gem 包。

    gem fetch some_signed_gem
    tar -xf some_signed_gem-1.0.gem
  2. 从 gemspec 中获取公钥。

    gem spec some_signed_gem-1.0.gem cert_chain | \
      ruby -rpsych -e 'puts Psych.load($stdin)' > public_key.crt
  3. 生成 data.tar.gz 的 SHA1 哈希值。

    openssl dgst -sha1 < data.tar.gz > my.hash
    
  4. 验证签名。

    openssl rsautl -verify -inkey public_key.crt -certin \
      -in data.tar.gz.sig > verified.hash
  5. 将您的哈希值与验证后的哈希值进行比较。

    diff -s verified.hash my.hash
  6. 对 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(对于 MediumSecurityHighSecurity 策略,它为 true),则证书链的根必须是自签名的。

  • 更好地解释 X509 命名(即,我们不必使用电子邮件地址)

  • 遵守 AIA 字段(请参阅上面有关 OCSP 的注释)

  • 遵守扩展限制

  • 最好将证书链存储为 PKCS#7 或 PKCS#12 文件,而不是嵌入在元数据中的数组。

原始作者

Paul Duncan <[email protected]> pablotron.org/

常量

几乎无安全

几乎无安全策略:仅验证签名证书是否是实际签名数据的证书。不尝试验证签名证书链。

此策略基本上毫无用处。比没有强,但仍然很容易被欺骗,不建议使用。

DEFAULT_KEY_ALGORITHM

构建密钥对时使用的默认算法

EC_NAME

用于椭圆曲线的命名曲线

EXTENSIONS

默认的扩展集是

  • 该证书不是证书颁发机构

  • 该证书的密钥可用于密钥和数据加密以及数字签名

  • 该证书包含使用者密钥标识符

高安全

高安全策略:仅允许安装已签名的 gem,验证签名证书,验证一直到根证书的签名证书链,并且仅信任我们已明确允许信任的根证书。

此安全策略更难以绕过,并提供了合理的保证,即 gem 的内容没有被更改。

KEY_CIPHER

用于加密用于签名 gem 的密钥对的密码。必须在 OpenSSL::Cipher.ciphers 返回的列表中。

低安全

低安全策略:仅验证签名证书是否实际上是 gem 签名者,并且签名证书有效。

此策略比没有强,但仍然很容易被欺骗,不建议使用。

中安全

中安全策略:验证签名证书,验证一直到根证书的签名证书链,并且仅信任我们已明确允许信任的根证书。

此安全策略是合理的,但它允许未签名的软件包,因此恶意人员可以简单地删除软件包签名,并将 gem 作为未签名的 gem 传递。

无安全

无安全策略:禁用所有软件包签名检查。

ONE_DAY

一天有多少秒

ONE_YEAR

一年有多少秒

策略

已配置安全策略的哈希

RSA_DSA_KEY_LENGTH

由 RSA 和 DSA 密钥创建的密钥长度

SigningPolicy

Policy 用于在签名 gem 时验证证书和密钥

公共类方法

alt_name_or_x509_entry(certificate, x509_entry) 单击以切换源
# File 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
create_cert(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) 单击以切换源

subjectkey 创建未签名的证书。密钥的有效期从当前时间到 age,默认为一年。

extensions 将密钥的使用限制在指定的用途中。

# File 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
create_cert_email(email, key, age = ONE_YEAR, extensions = EXTENSIONS) 单击以切换源

使用来自 email 的颁发者和使用者,email 的使用者备用名称以及 key 的给定 extensions 创建自签名证书。

# File 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
create_cert_self_signed(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) 单击以切换源

使用 subject 的颁发者和使用者以及 key 的给定 extensions 创建自签名证书。

# File 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
create_digest(algorithm = DIGEST_NAME) 单击以切换源

使用指定的 algorithm 创建新的摘要实例。默认值为 SHA256。

# File rubygems/security.rb, line 462
def self.create_digest(algorithm = DIGEST_NAME)
  OpenSSL::Digest.new(algorithm)
end
create_key(algorithm) 单击以切换源

创建指定 algorithm 的新密钥对。支持 RSA、DSA 和 EC。

# File 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
email_to_name(email_address) 单击以切换源

email_address 转换为 OpenSSL::X509::Name

# File rubygems/security.rb, line 489
def self.email_to_name(email_address)
  email_address = email_address.gsub(/[^\[email protected]]+/i, "_")

  cn, dcs = email_address.split "@"

  dcs = dcs.split "."

  OpenSSL::X509::Name.new([
    ["CN", cn],
    *dcs.map {|dc| ["DC", dc] },
  ])
end
get_public_key(key) 单击以切换源

从 PKey 实例获取正确的公钥

# File 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
re_sign(expired_certificate, private_key, age = ONE_YEAR, extensions = EXTENSIONS) 单击以切换源

如果密钥匹配并且过期的证书是自签名的,则使用 private_key 签署 expired_certificate

# File 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
reset() 单击以切换源

重置用于验证 gem 的信任目录。

# File rubygems/security.rb, line 533
def self.reset
  @trust_dir = nil
end
sign(certificate, signing_key, signing_cert, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) 单击以切换源

使用 Gem::Security::DIGEST_NAME,使用 signing_keysigning_cert 签署来自 certificate 的公钥。使用默认证书有效期范围和扩展。

返回新签名的证书。

# File 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
trust_dir() 单击以切换源

返回一个 Gem::Security::TrustDir,它包装了存储受信任证书的目录。

# File 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
trusted_certificates(&block) 单击以切换源

通过 Gem::Security::TrustDir 枚举受信任的证书。

# File rubygems/security.rb, line 583
def self.trusted_certificates(&block)
  trust_dir.each_certificate(&block)
end
write(pemmable, path, permissions = 0o600, passphrase = nil, cipher = KEY_CIPHER) 单击以切换源

将必须响应 to_pempemmable 写入到具有给定 permissionspath。如果传递了 cipherpassphrase,则这些参数将传递给 to_pem

# File 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