Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-implement missing jruby functionality atop java.security #192

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is a jruby discrepancy here -- file issue for it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Loading