Skip to content

Commit

Permalink
Re-implement missing jruby functionality atop java.security
Browse files Browse the repository at this point in the history
Signed-off-by: Samuel Giddins <[email protected]>
  • Loading branch information
segiddins committed Jan 27, 2025
1 parent 409ac01 commit 882a525
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 68 deletions.
2 changes: 1 addition & 1 deletion .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 Down
109 changes: 83 additions & 26 deletions lib/sigstore/internal/x509.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,71 @@
module Sigstore
module Internal
module X509
if RUBY_ENGINE == "jruby"
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)
t = time || Time.now
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: #{t.inspect} #{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 +214,9 @@ 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.purposes.any? do |purpose|
purpose.oid == "1.3.6.1.4.1.11129.2.4.4"
end
end
end

Expand Down Expand Up @@ -248,10 +315,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 +381,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 +460,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
37 changes: 18 additions & 19 deletions lib/sigstore/verifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ class Verifier

attr_reader :rekor_client

def initialize(rekor_client:, fulcio_cert_chain:, timestamp_authorities:, rekor_keyring:, ct_keyring:)
def initialize(rekor_client:, fulcio_cert_chains:, timestamp_authorities:, rekor_keyring:, ct_keyring:)
@rekor_client = rekor_client
@fulcio_cert_chain = fulcio_cert_chain
@fulcio_cert_chains = fulcio_cert_chains
@timestamp_authorities = timestamp_authorities
@rekor_keyring = rekor_keyring
@ct_keyring = ct_keyring
Expand All @@ -39,7 +39,7 @@ def initialize(rekor_client:, fulcio_cert_chain:, timestamp_authorities:, rekor_
def self.for_trust_root(trust_root:)
new(
rekor_client: Rekor::Client.new(url: trust_root.tlog_for_signing.base_url),
fulcio_cert_chain: trust_root.fulcio_cert_chain,
fulcio_cert_chains: trust_root.fulcio_cert_chains,
timestamp_authorities: trust_root.timestamp_authorities,
rekor_keyring: Internal::Keyring.new(keys: trust_root.rekor_keys),
ct_keyring: Internal::Keyring.new(keys: trust_root.ctfe_keys)
Expand Down Expand Up @@ -101,8 +101,10 @@ def verify(input:, policy:, offline:)

store = OpenSSL::X509::Store.new

@fulcio_cert_chain.each do |cert|
store.add_cert(cert.openssl)
@fulcio_cert_chains.each do |chain|
chain.each do |cert|
store.add_cert(cert.openssl)
end
end

# 3)
Expand All @@ -112,19 +114,12 @@ def verify(input:, policy:, offline:)
# timestamp from the Timestamping Service. If a timestamp from the Transparency Service is available, the Verifier
# MUST perform path validation using the timestamp from the Transparency Service. If both are available, the
# Verifier performs path validation twice. If either fails, verification fails.
chains = timestamps.map do |ts|
store_ctx = OpenSSL::X509::StoreContext.new(store, bundle.leaf_certificate.openssl)
store_ctx.time = ts

unless store_ctx.verify
return VerificationFailure.new(
"failed to validate certificate from fulcio cert chain: #{store_ctx.error_string}"
)
end
chains = timestamps.map do |ts|
chain, err = Internal::X509.validate_chain(@fulcio_cert_chains, bundle.leaf_certificate, ts)
return err if err

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

chains.uniq! { |chain| chain.map(&:to_der) }
Expand Down Expand Up @@ -280,13 +275,17 @@ def verify_sct(sct, certificate, chain, ct_keyring)
issuer_cert = find_issuer_cert(chain)
issuer_pubkey = issuer_cert.public_key
unless issuer_cert.ca?
raise Error::InvalidCertificate, "Invalid issuer pubkey basicConstraint (not a CA): #{issuer_cert.to_text}"
raise Error::InvalidCertificate, "Invalid issuer pubkey basicConstraint (not a CA): #{issuer_cert.to_pem}"
end

issuer_key_id = OpenSSL::Digest::SHA256.digest(issuer_pubkey.public_to_der)
# TODO: use public_to_der when available
issuer_key_id = OpenSSL::Digest::SHA256.digest(issuer_pubkey.to_der)
end

digitally_signed = pack_digitally_signed(sct, certificate, issuer_key_id).b

return true if RUBY_ENGINE == "jruby" # TODO: https://github.com/jruby/jruby-openssl/issues/323

ct_keyring.verify(key_id: sct.log_id, signature: sct.signature, data: digitally_signed)
end

Expand Down Expand Up @@ -328,7 +327,7 @@ def pack_digitally_signed(sct, certificate, issuer_key_id = nil)

[issuer_key_id, len1, len2, len3, tbs_cert].pack("a32 CCC a#{tbs_cert_len}")
else
raise Error::Unimplemented, "only x509_entry and precert_entry supported, given #{sct[:entry_type].inspect}"
raise Error::Unimplemented, "only x509_entry and precert_entry supported, given #{sct.entry_type.inspect}"
end

[sct.version, 0, sct.timestamp, sct.entry_type, signed_entry, 0].pack(<<~PACK)
Expand Down
8 changes: 6 additions & 2 deletions test/sigstore/trusted_root_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def test_production
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEc\n" \
"YXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9F\n" \
"yw==\n"], production.ctfe_keys.map { [_1.to_der].pack("m") }
assert_equal "-----BEGIN CERTIFICATE-----\n" \
assert_equal "chain 0\n" \
"-----BEGIN CERTIFICATE-----\n" \
"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAq\n" \
"MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx\n" \
"MDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUu\n" \
Expand All @@ -31,6 +32,7 @@ def test_production
"Ve/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uup\n" \
"Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==\n" \
"-----END CERTIFICATE-----\n" \
"chain 1\n" \
"-----BEGIN CERTIFICATE-----\n" \
"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw\n" \
"KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\n" \
Expand All @@ -57,7 +59,9 @@ def test_production
"KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM\n" \
"WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9\n" \
"TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\n" \
"-----END CERTIFICATE-----\n", production.fulcio_cert_chain.map(&:to_pem).join
"-----END CERTIFICATE-----\n", production.fulcio_cert_chains.map.with_index { |chain, i|
"chain #{i}\n" + chain.map(&:to_pem).join
}.join
end
end
end
Expand Down

0 comments on commit 882a525

Please sign in to comment.