Skip to content

Commit

Permalink
Re-implement missing jruby functionality atop java.security (#192)
Browse files Browse the repository at this point in the history
Signed-off-by: Samuel Giddins <[email protected]>
  • Loading branch information
segiddins authored Feb 19, 2025
1 parent 958d8ee commit c8ad4cf
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 86 deletions.
15 changes: 9 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
ruby-versions:
uses: ruby/actions/.github/workflows/ruby_versions.yml@3fbf038d6f0d8043b914f923764c61bc2a114a77
with:
engine: cruby-truffleruby
engine: all
min_version: 3.1

test:
Expand All @@ -39,7 +39,7 @@ jobs:

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Ruby
uses: ruby/setup-ruby@1287d2b408066abada82d5ad1c63652e758428d9 # v1.214.0
uses: ruby/setup-ruby@f2f42b7848feff522ffa488a5236ba0a73bccbdd # v1.219.0
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
Expand Down Expand Up @@ -76,7 +76,7 @@ jobs:

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Ruby
uses: ruby/setup-ruby@1287d2b408066abada82d5ad1c63652e758428d9 # v1.214.0
uses: ruby/setup-ruby@f2f42b7848feff522ffa488a5236ba0a73bccbdd # v1.219.0
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
Expand Down Expand Up @@ -123,14 +123,17 @@ jobs:

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Ruby
uses: ruby/setup-ruby@1287d2b408066abada82d5ad1c63652e758428d9 # v1.214.0
uses: ruby/setup-ruby@f2f42b7848feff522ffa488a5236ba0a73bccbdd # v1.219.0
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true

- name: Touch requirements.txt
run: touch requirements.txt

- name: Write xfails
run: bin/rake bin/tuf-conformance-entrypoint.xfails

- name: Run the TUF conformance tests
uses: theupdateframework/tuf-conformance@dee4e23533d7a12a6394d96b59b3ea0aa940f9bf
with:
Expand Down Expand Up @@ -167,7 +170,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Ruby
uses: ruby/setup-ruby@1287d2b408066abada82d5ad1c63652e758428d9 # v1.214.0
uses: ruby/setup-ruby@f2f42b7848feff522ffa488a5236ba0a73bccbdd # v1.219.0
with:
ruby-version: ${{ fromJson(needs.ruby-versions.outputs.latest) }}
bundler-cache: true
Expand Down Expand Up @@ -217,7 +220,7 @@ jobs:

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Ruby
uses: ruby/setup-ruby@1287d2b408066abada82d5ad1c63652e758428d9 # v1.214.0
uses: ruby/setup-ruby@f2f42b7848feff522ffa488a5236ba0a73bccbdd # v1.219.0
with:
ruby-version: ${{ fromJson(needs.ruby-versions.outputs.latest) }}
bundler-cache: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
with:
persist-credentials: false

- uses: ruby/setup-ruby@1287d2b408066abada82d5ad1c63652e758428d9 # v1.214.0
- uses: ruby/setup-ruby@f2f42b7848feff522ffa488a5236ba0a73bccbdd # v1.219.0
with:
# NOTE: We intentionally don't use a cache in the release step,
# to reduce the risk of cache poisoning.
Expand Down Expand Up @@ -126,7 +126,7 @@ jobs:
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8

- name: Set up Ruby
uses: ruby/setup-ruby@1287d2b408066abada82d5ad1c63652e758428d9 # v1.214.0
uses: ruby/setup-ruby@f2f42b7848feff522ffa488a5236ba0a73bccbdd # v1.219.0
with:
ruby-version: "3.3"
bundler-cache: false
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
/pkg/
/spec/reports/
/tmp/
/bin/tuf-conformance-entrypoint.xfails
12 changes: 11 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,18 @@ GitRepo.define_task(tuf_conformance: %w[find_action_versions]).tap do |task|
end

namespace :tuf_conformance do
file "bin/tuf-conformance-entrypoint.xfails" do |t|
if RUBY_ENGINE == "jruby"
File.write(t.name, <<~TXT)
test_keytype_and_scheme[rsa/rsassa-pss-sha256]
test_keytype_and_scheme[ed25519/ed25519]
TXT
else
File.write(t.name, "")
end
end
file "test/tuf-conformance/env/pyvenv.cfg" => :tuf_conformance do
sh "make", "dev", chdir: "test/tuf-conformance"
end
task setup: "test/tuf-conformance/env/pyvenv.cfg" # rubocop:disable Rake/Desc
task setup: %w[test/tuf-conformance/env/pyvenv.cfg bin/tuf-conformance-entrypoint.xfails]
end
4 changes: 0 additions & 4 deletions lib/sigstore/internal/key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ def self.read(key_type, schema, key_bytes, key_id: nil)
RSA.new(key_type, schema, pkey, key_id:)
else
raise ArgumentError, "Unsupported key type #{key_type}"
end.tap do |key|
if RUBY_ENGINE == "jruby" && key.to_pem != key_bytes && key.to_der != key_bytes
raise Error::UnsupportedPlatform, "Key mismatch: #{key.to_pem.inspect} != #{key_bytes.inspect}"
end
end
rescue OpenSSL::PKey::PKeyError => e
raise OpenSSL::PKey::PKeyError, "Invalid key: #{e} for #{key_type} #{schema} #{key_id}"
Expand Down
110 changes: 84 additions & 26 deletions lib/sigstore/internal/x509.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,74 @@
module Sigstore
module Internal
module X509
if RUBY_ENGINE == "jruby"
unless JOpenSSL::VERSION >= "0.15.3"
raise Error::UnsupportedPlatform, "JRuby support requires jruby-openssl >= 0.15.3, is #{JOpenSSL::VERSION}"
end

def self.validate_chain(trust_roots, leaf, time)
cert_factory = java.security.cert.CertificateFactory.getInstance("X.509")
cert_factory.generateCertificate(java.io.ByteArrayInputStream.new(leaf.to_der.to_java_bytes))
target = leaf.openssl.to_java

trust_anchors = Set.new
intermediate_certs = []
trust_roots.each do |chain|
root = chain.last

trust_anchors << java.security.cert.TrustAnchor.new(root.openssl.to_java, nil)
chain[..-2].each do |cert|
intermediate_certs << cert.openssl.to_java
end
end

cert_store_parameters = java.security.cert.CollectionCertStoreParameters.new(intermediate_certs)
cert_store = java.security.cert.CertStore.getInstance("Collection", cert_store_parameters)

cert_selector = java.security.cert.X509CertSelector.new
cert_selector.setCertificate(target)

pkix_builder_parameters = java.security.cert.PKIXBuilderParameters.new(trust_anchors, cert_selector)
pkix_builder_parameters.setDate(time) if time
pkix_builder_parameters.setRevocationEnabled(false)
pkix_builder_parameters.addCertStore(cert_store)

cert_path_builder = java.security.cert.CertPathBuilder.getInstance("PKIX")
cert_path_result = cert_path_builder.build(pkix_builder_parameters)
chain = cert_path_result.cert_path.getCertificates.map do |cert|
der = String.from_java_bytes(cert.getEncoded).b
Certificate.read(der)
end
chain.shift # remove the cert itself
chain << Certificate.read(
String.from_java_bytes(cert_path_result.get_trust_anchor.getTrustedCert.getEncoded).b
)
[chain, nil]
end
else
def self.validate_chain(trust_roots, leaf, time)
store = OpenSSL::X509::Store.new
intermediate_certs = []
trust_roots.each do |chain|
store.add_cert(chain.last.openssl)
chain[..-2].each do |cert|
intermediate_certs << cert.openssl
end
end
store_ctx = OpenSSL::X509::StoreContext.new(store, leaf.openssl, intermediate_certs)
store_ctx.time = time if time
unless store_ctx.verify
return nil, VerificationFailure.new(
"failed to validate certificate from fulcio cert chain: #{store_ctx.error_string}"
)
end

chain = store_ctx.chain || raise(Error::InvalidCertificate, "no valid cert chain found")
chain.shift # remove the cert itself
[chain.map! { Certificate.new(_1) }, nil]
end
end

class Certificate
extend Forwardable

Expand Down Expand Up @@ -149,7 +217,7 @@ def preissuer?
extended_key_usage = extension(Extension::ExtendedKeyUsage)
return false unless extended_key_usage

extended_key_usage.purposes.include?(OpenSSL::ASN1::ObjectId.new("1.3.6.1.4.1.11129.2.4.4"))
extended_key_usage.precert?
end
end

Expand Down Expand Up @@ -248,10 +316,15 @@ def parse_value(value)
end

CODE_SIGNING = OpenSSL::ASN1::ObjectId.new("1.3.6.1.5.5.7.3.3")
PRECERT_PURPOSE = OpenSSL::ASN1::ObjectId.new("1.3.6.1.4.1.11129.2.4.4")

def code_signing?
purposes.any? { |purpose| purpose.oid == CODE_SIGNING.oid }
end

def precert?
purposes.any? { |purpose| purpose.oid == PRECERT_PURPOSE.oid }
end
end

class BasicConstraints < Extension
Expand Down Expand Up @@ -309,11 +382,15 @@ def parse_value(value)
@general_names = value.map do |general_name|
tag = general_name.tag

value = general_name.value
value = value.first if value.is_a?(Array) && value.size == 1
value = value.value if value.is_a?(OpenSSL::ASN1::OctetString)

case tag
when 1
[:otherName, general_name.value]
[:otherName, value]
when 6
[:uniformResourceIdentifier, general_name.value]
[:uniformResourceIdentifier, value]
else
raise Error::Unimplemented,
"Unhandled general name tag: #{tag}"
Expand Down Expand Up @@ -384,46 +461,27 @@ def parse_value(value)

private

if RUBY_VERSION >= "3.1"
def unpack_at(string, format, offset:)
string.unpack(format, offset:)
end

def unpack1_at(string, format, offset:)
string.unpack1(format, offset:)
end
else
def unpack_at(string, format, offset:)
string[offset..].unpack(format)
end

def unpack1_at(string, format, offset:)
string[offset..].unpack1(format)
end
end

# https://letsencrypt.org/2018/04/04/sct-encoding.html
def unpack_sct_list(string)
offset = 0
len = string.bytesize
list = []
while offset < len
sct_version, sct_log_id, sct_timestamp, sct_extensions_len = unpack_at(string, "Ca32Q>n", offset:)
sct_version, sct_log_id, sct_timestamp, sct_extensions_len = string.unpack("Ca32Q>n", offset:)
offset += 1 + 32 + 8 + 2
raise Error::Unimplemented, "expect sct version to be 0, got #{sct_version}" unless sct_version.zero?

sct_extensions_bytes = unpack1_at(string, "a#{sct_extensions_len}", offset:).b
sct_extensions_bytes = string.unpack1("a#{sct_extensions_len}", offset:).b
offset += sct_extensions_len

unless sct_extensions_len.zero?
raise Error::Unimplemented,
"sct_extensions_len=#{sct_extensions_len} not supported"
end

sct_signature_alg_hash, sct_signature_alg_sign, sct_signature_len = unpack_at(string, "CCn",
offset:)
sct_signature_alg_hash, sct_signature_alg_sign, sct_signature_len = string.unpack("CCn", offset:)
offset += 1 + 1 + 2
sct_signature_bytes = unpack1_at(string, "a#{sct_signature_len}", offset:).b
sct_signature_bytes = string.unpack1("a#{sct_signature_len}", offset:).b
offset += sct_signature_len
list << Timestamp.new(
version: sct_version,
Expand Down
12 changes: 10 additions & 2 deletions lib/sigstore/policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,16 @@ def verify(cert)
VerificationSuccess.new
end

def ext_value(ext)
ext.value
if RUBY_ENGINE == "jruby"
def ext_value(ext)
der = ext.to_der
seq = OpenSSL::ASN1.decode(der)
seq.value.last.value
end
else
def ext_value(ext)
ext.value
end
end

def oid
Expand Down
23 changes: 13 additions & 10 deletions lib/sigstore/signer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,17 +133,20 @@ def verify_chain(leaf)
# Perform certification path validation (RFC 5280 §6) of the returned certificate chain with the pre-distributed
# Fulcio root certificate(s) as a trust anchor.

x509_store = OpenSSL::X509::Store.new
expected_chain = @trusted_root.fulcio_cert_chain

x509_store.add_cert expected_chain.last.openssl
unless x509_store.verify(leaf.openssl, expected_chain[..-2].map(&:openssl))
raise Error::Signing, "returned certificate does not validate: #{x509_store.error_string}"
now = Time.now
if leaf.not_before > now
unless leaf.not_before - now < 60
raise Error::Signing, "leaf certificate not yet valid: #{leaf.not_before.inspect} vs #{now.inspect}"
end

logger.warn do
"leaf certificate not yet valid: #{leaf.not_before.inspect} vs #{now.inspect}, sleeping until valid"
end
sleep(leaf.not_before - now)
end

chain = x509_store.chain
chain.shift # remove the leaf cert
chain.map! { |cert| Internal::X509::Certificate.new(cert) }
chain, err = Internal::X509.validate_chain(@trusted_root.fulcio_cert_chains, leaf, nil)
raise Error::Signing, "failed to validate returned certificate chain: #{err.reason}" if err

logger.debug { "verified chain" }

Expand Down Expand Up @@ -175,7 +178,7 @@ def verify_chain(leaf)
"certificate does not contain expected SAN #{expected_san}, got #{general_names}"
end

[leaf, x509_store.chain]
[leaf, chain.unshift(leaf)]
end

def sign_payload(payload, key)
Expand Down
14 changes: 6 additions & 8 deletions lib/sigstore/trusted_root.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ def ctfe_keys
keys
end

def fulcio_cert_chain
certs = ca_keys(certificate_authorities, allow_expired: true).flat_map do |raw_bytes|
Internal::X509::Certificate.read(raw_bytes)
def fulcio_cert_chains
chains = ca_keys(certificate_authorities, allow_expired: true).map do |certs|
certs.map { |raw_bytes| Internal::X509::Certificate.read(raw_bytes) }
end
raise Error::InvalidBundle, "Fulcio certificates not found in trusted root" if certs.empty?
raise Error::InvalidBundle, "Fulcio certificates not found in trusted root" if chains.none?(&:any?)

certs
chains
end

def tlog_for_signing
Expand Down Expand Up @@ -97,9 +97,7 @@ def ca_keys(certificate_authorities, allow_expired:)
certificate_authorities.each do |ca|
next unless timerange_valid?(ca.valid_for, allow_expired:)

ca.cert_chain.certificates.each do |cert|
yield cert.raw_bytes
end
yield ca.cert_chain.certificates.map(&:raw_bytes)
end
end

Expand Down
Loading

0 comments on commit c8ad4cf

Please sign in to comment.