diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..940cc26 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,152 @@ +require: rubocop-performance + +AllCops: + DisabledByDefault: true + Exclude: + - 'bundler/**/*' + - 'lib/rubygems/resolver/molinillo/**/*' + - 'pkg/**/*' + - 'tmp/**/*' + TargetRubyVersion: 2.3 + +Layout/AccessModifierIndentation: + Enabled: true + +Layout/ArrayAlignment: + Enabled: true + +Layout/BlockAlignment: + Enabled: true + +Layout/CaseIndentation: + Enabled: true + +Layout/ClosingParenthesisIndentation: + Enabled: true + +Layout/CommentIndentation: + Enabled: true + +Layout/ElseAlignment: + Enabled: true + +Layout/EmptyLinesAroundAccessModifier: + Enabled: true + +# Force Unix line endings. +Layout/EndOfLine: + Enabled: true + EnforcedStyle: lf + +Layout/EmptyLines: + Enabled: true + +Layout/EmptyLinesAroundClassBody: + Enabled: true + +Layout/EmptyLinesAroundMethodBody: + Enabled: true + +Layout/ExtraSpacing: + Enabled: true + +Layout/FirstHashElementIndentation: + Enabled: true + EnforcedStyle: consistent + +Layout/FirstArrayElementIndentation: + Enabled: true + EnforcedStyle: consistent + +Layout/IndentationConsistency: + Enabled: true + +Layout/IndentationWidth: + Enabled: true + +Layout/LeadingEmptyLines: + Enabled: true + +Layout/SpaceAroundOperators: + Enabled: true + +Layout/SpaceInsideBlockBraces: + Enabled: true + SpaceBeforeBlockParameters: false + +Layout/SpaceInsideParens: + Enabled: true + +Layout/TrailingEmptyLines: + Enabled: true + +Layout/TrailingWhitespace: + Enabled: true + +Lint/DuplicateMethods: + Enabled: true + +Lint/ParenthesesAsGroupedExpression: + Enabled: true + +Layout/EndAlignment: + Enabled: true + +Naming/HeredocDelimiterCase: + Enabled: true + +Naming/HeredocDelimiterNaming: + Enabled: true + ForbiddenDelimiters: + - ^RB$ + +Performance/StartWith: + Enabled: true + +Performance/StringReplacement: + Enabled: true + +Security/Open: + Enabled: true + +Style/Encoding: + Enabled: true + Exclude: + - test/rubygems/specifications/foo-0.0.1-x86-mswin32.gemspec + +Style/EvalWithLocation: + Enabled: true + +Style/IfInsideElse: + Enabled: false + +Style/MethodCallWithoutArgsParentheses: + Enabled: true + +Style/MethodDefParentheses: + Enabled: true + +Style/MultilineIfThen: + Enabled: true + +Style/MutableConstant: + Enabled: true + +Style/NilComparison: + Enabled: true + +Style/BlockDelimiters: + Enabled: true + +Style/PercentLiteralDelimiters: + Enabled: true + +# Having these make it easier to *not* forget to add one when adding a new +# value and you can simply copy the previous line. +Style/TrailingCommaInArrayLiteral: + Enabled: true + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInHashLiteral: + Enabled: true + EnforcedStyleForMultiline: comma diff --git a/Gemfile b/Gemfile index b20f148..9a5b0c0 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,11 @@ gem "faraday_middleware", "~> 1.0.0" gem "oa-openid", "~> 0.0.2" gem "omniauth-openid", "~> 2.0.1" gem "ruby-openid-apps-discovery", "~> 1.2.0" -gem "rake", "~> 12.0" -gem "rspec", "~> 3.0" -gem "json-jwt", "~> 1.13.0" \ No newline at end of file +gem "json-jwt", "~> 1.13.0" + +group :development do + gem "rubocop", "~> 0.80.1" + gem "rubocop-performance", "~> 1.5.2" + gem "rake", "~> 12.0" + gem "rspec", "~> 3.0" +end diff --git a/Gemfile.lock b/Gemfile.lock index 8806f3e..357208f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -25,6 +25,7 @@ GEM addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) aes_key_wrap (1.1.0) + ast (2.4.2) attr_required (1.0.1) bindata (2.4.8) concurrent-ruby (1.1.8) @@ -82,6 +83,7 @@ GEM httpclient (2.8.3) i18n (1.8.9) concurrent-ruby (~> 1.0) + jaro_winkler (1.5.4) json-jwt (1.13.0) activesupport (>= 4.2) aes_key_wrap @@ -115,6 +117,9 @@ GEM validate_email validate_url webfinger (>= 1.0.1) + parallel (1.21.0) + parser (3.0.2.0) + ast (~> 2.4.1) pp (0.2.0) prettyprint prettyprint (0.1.0) @@ -131,7 +136,9 @@ GEM ruby-openid (>= 2.1.8) rack-protection (2.1.0) rack + rainbow (3.0.0) rake (12.3.3) + rexml (3.2.5) rspec (3.10.0) rspec-core (~> 3.10.0) rspec-expectations (~> 3.10.0) @@ -145,9 +152,20 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.10.0) rspec-support (3.10.2) + rubocop (0.80.1) + jaro_winkler (~> 1.5.1) + parallel (~> 1.10) + parser (>= 2.7.0.1) + rainbow (>= 2.2.2, < 4.0) + rexml + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 1.7) + rubocop-performance (1.5.2) + rubocop (>= 0.71.0) ruby-openid (2.9.2) ruby-openid-apps-discovery (1.2.0) ruby-openid (>= 2.1.7) + ruby-progressbar (1.11.0) ruby2_keywords (0.0.4) swd (1.2.0) activesupport (>= 3) @@ -155,6 +173,7 @@ GEM httpclient (>= 2.4) tzinfo (2.0.4) concurrent-ruby (~> 1.0) + unicode-display_width (1.6.1) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) @@ -179,6 +198,8 @@ DEPENDENCIES pp (= 0.2.0) rake (~> 12.0) rspec (~> 3.0) + rubocop (~> 0.80.1) + rubocop-performance (~> 1.5.2) ruby-openid-apps-discovery (~> 1.2.0) ruby-sigstore! diff --git a/lib/rubygems/commands/sign_command.rb b/lib/rubygems/commands/sign_command.rb index 81d2424..cf2644d 100644 --- a/lib/rubygems/commands/sign_command.rb +++ b/lib/rubygems/commands/sign_command.rb @@ -41,10 +41,10 @@ def usage # :nodoc: end def execute - config = SigStoreConfig.new().config - priv_key, pub_key = Crypto.new().generate_keys + config = SigStoreConfig.new.config + priv_key, pub_key = Crypto.new.generate_keys proof, access_token = OpenIDHandler.new(priv_key).get_token - cert_response = HttpClient.new().get_cert(access_token, proof, pub_key, config.fulcio_host) + cert_response = HttpClient.new.get_cert(access_token, proof, pub_key, config.fulcio_host) puts cert_response end end diff --git a/lib/rubygems/commands/verify_command.rb b/lib/rubygems/commands/verify_command.rb index 82af112..52a9c29 100644 --- a/lib/rubygems/commands/verify_command.rb +++ b/lib/rubygems/commands/verify_command.rb @@ -23,4 +23,4 @@ def initialize def execute puts "verify" end -end \ No newline at end of file +end diff --git a/lib/rubygems/sigstore/config.rb b/lib/rubygems/sigstore/config.rb index 9e5698d..eed9e66 100644 --- a/lib/rubygems/sigstore/config.rb +++ b/lib/rubygems/sigstore/config.rb @@ -15,17 +15,17 @@ require 'config' class SigStoreConfig - def initialize; end - def config - Config.setup do |config| - config.use_env = true - config.env_prefix = 'sigstore' - config.env_separator = '_' - end + def initialize; end + def config + Config.setup do |config| + config.use_env = true + config.env_prefix = 'sigstore' + config.env_separator = '_' + end settings_file = Config.setting_files( File.expand_path('../../../../', __FILE__), 'development' # TODO: Get this from gemspec - ) + ) return Config.load_and_set_settings(settings_file) - end + end end diff --git a/lib/rubygems/sigstore/crypto.rb b/lib/rubygems/sigstore/crypto.rb index a1efb2b..e47b4f0 100644 --- a/lib/rubygems/sigstore/crypto.rb +++ b/lib/rubygems/sigstore/crypto.rb @@ -15,22 +15,22 @@ require 'base64' require 'openssl' -class Crypto - def initialize; end - - def generate_keys - key = OpenSSL::PKey::RSA.generate(2048) - pkey = key.public_key - return [key, pkey, Base64.encode64(pkey.to_der)] - end - - def sign_proof(priv_key, email) - proof = priv_key.sign(OpenSSL::Digest::SHA256.new, email) - return Base64.encode64(proof) - end +class Crypto + def initialize; end + + def generate_keys + key = OpenSSL::PKey::RSA.generate(2048) + pkey = key.public_key + return [key, pkey, Base64.encode64(pkey.to_der)] + end + + def sign_proof(priv_key, email) + proof = priv_key.sign(OpenSSL::Digest::SHA256.new, email) + return Base64.encode64(proof) + end end -# class Crypto +# class Crypto # def initialize; end # def generate_keys @@ -45,4 +45,3 @@ def sign_proof(priv_key, email) # return Base64.encode64(proof) # end # end - diff --git a/lib/rubygems/sigstore/http_client.rb b/lib/rubygems/sigstore/http_client.rb index 26b3b0d..18fdaa1 100644 --- a/lib/rubygems/sigstore/http_client.rb +++ b/lib/rubygems/sigstore/http_client.rb @@ -16,50 +16,50 @@ require "openssl" class HttpClient - def initialize; end - def get_cert(id_token, proof, pub_key, fulcio_host) - # rekor uses a self signed certificate which failes the ssl check - connection = Faraday.new(ssl: { verify: false }) do |request| - request.authorization :Bearer, id_token.to_s - request.url_prefix = fulcio_host - request.request :json - request.response :json, content_type: /json/ - request.adapter :net_http - end - fulcio_response = connection.post("/api/v1/signingCert", { publicKey: { content: pub_key, algorithm: "ecdsa" }, signedEmailAddress: proof}) - return fulcio_response.body + def initialize; end + def get_cert(id_token, proof, pub_key, fulcio_host) + # rekor uses a self signed certificate which failes the ssl check + connection = Faraday.new(ssl: { verify: false }) do |request| + request.authorization :Bearer, id_token.to_s + request.url_prefix = fulcio_host + request.request :json + request.response :json, content_type: /json/ + request.adapter :net_http + end + fulcio_response = connection.post("/api/v1/signingCert", { publicKey: { content: pub_key, algorithm: "ecdsa" }, signedEmailAddress: proof}) + return fulcio_response.body + end + def submit_rekor(pub_key, data_digest, data_signature, certPEM, data_raw, rekor_host) + # rekor uses a self signed certificate which failes the ssl check + connection = Faraday.new(ssl: { verify: false }) do |request| + # request.authorization :Bearer, id_token.to_s + request.url_prefix = rekor_host + request.request :json + request.response :json, content_type: /json/ + request.adapter :net_http end - def submit_rekor(pub_key, data_digest, data_signature, certPEM, data_raw, rekor_host) - # rekor uses a self signed certificate which failes the ssl check - connection = Faraday.new(ssl: { verify: false }) do |request| - # request.authorization :Bearer, id_token.to_s - request.url_prefix = rekor_host - request.request :json - request.response :json, content_type: /json/ - request.adapter :net_http - end - rekor_response = connection.post("/api/v1/log/entries", - { - kind: "rekord", - apiVersion: "0.0.1", - spec: { - signature: { - format: "x509", - content: Base64.encode64(data_signature), - publicKey: { - content: Base64.encode64(pub_key.to_pem) - } + rekor_response = connection.post("/api/v1/log/entries", + { + kind: "rekord", + apiVersion: "0.0.1", + spec: { + signature: { + format: "x509", + content: Base64.encode64(data_signature), + publicKey: { + content: Base64.encode64(pub_key.to_pem), }, - data: { - content: data_raw, - hash: { - algorithm: "sha256", - value: data_digest - } - } - } - }) - return rekor_response.body - end + }, + data: { + content: data_raw, + hash: { + algorithm: "sha256", + value: data_digest, + }, + }, + }, + }) + return rekor_response.body + end end diff --git a/lib/rubygems/sigstore/openid.rb b/lib/rubygems/sigstore/openid.rb index 2c3da9a..6dd8155 100644 --- a/lib/rubygems/sigstore/openid.rb +++ b/lib/rubygems/sigstore/openid.rb @@ -22,161 +22,163 @@ require "launchy" require "openid_connect" -class OpenIDHandler - def initialize(priv_key) - @priv_key = priv_key +class OpenIDHandler + def initialize(priv_key) + @priv_key = priv_key + end + + def get_token() + config = SigStoreConfig.new.config + session = {} + session[:state] = SecureRandom.hex(16) + session[:nonce] = SecureRandom.hex(16) + oidc_discovery = OpenIDConnect::Discovery::Provider::Config.discover! config.oidc_issuer + + # oidc_discovery gem doesn't support code_challenge_methods yet, so we will just blindly include + pkce = generate_pkce + + # If development env, used a fixed port + if config.development == true + server = TCPServer.new 5678 + server_addr = "5678" + else + server = TCPServer.new 0 + server_addr = server.addr[1].to_s end - def get_token() - config = SigStoreConfig.new().config - session = {} - session[:state] = SecureRandom.hex(16) - session[:nonce] = SecureRandom.hex(16) - oidc_discovery = OpenIDConnect::Discovery::Provider::Config.discover! config.oidc_issuer - - # oidc_discovery gem doesn't support code_challenge_methods yet, so we will just blindly include - pkce = generate_pkce - - # If development env, used a fixed port - if config.development == true - server = TCPServer.new 5678 - server_addr = "5678" - else - server = TCPServer.new 0 - server_addr = server.addr[1].to_s - end - - webserv = Thread.new do - response = "You may close this browser" - response_code = "200 OK" - connection = server.accept - while (input = connection.gets) - begin - # VERB PATH HTTP/1.1 - http_req = input.split(' ') - if http_req.length() != 3 - raise "invalid HTTP request received on callback" - end - params = CGI.parse(URI.parse(http_req[1]).query) - if params["code"].length() != 1 or params["state"].length() != 1 - raise "multiple values for code or state returned in callback; unable to process" - end - Thread.current[:code] = params["code"][0] - Thread.current[:state] = params["state"][0] - rescue StandardError => e - response = "Error processing request: #{e.message}" - response_code = "400 Bad Request" - end - connection.print "HTTP/1.1 #{response_code}\r\n" + - "Content-Type: text/plain\r\n" + - "Content-Length: #{response.bytesize}\r\n" + - "Connection: close\r\n" - connection.print "\r\n" - connection.print response - connection.close - if response_code != "200 OK" - raise response - end - break + webserv = Thread.new do + begin + response = "You may close this browser" + response_code = "200 OK" + connection = server.accept + while (input = connection.gets) + begin + # VERB PATH HTTP/1.1 + http_req = input.split(' ') + if http_req.length != 3 + raise "invalid HTTP request received on callback" end - ensure - server.close - end - - webserv.abort_on_exception = true - - client = OpenIDConnect::Client.new( - authorization_endpoint: oidc_discovery.authorization_endpoint, - identifier: config.oidc_client, - redirect_uri: "http://localhost:" + server_addr, - secret: config.oidc_secret, - token_endpoint: oidc_discovery.token_endpoint, - ) - - authorization_uri = client.authorization_uri( - scope: ["openid", :email], - state: session[:state], - nonce: session[:nonce], - code_challenge_method: pkce[:method], - code_challenge: pkce[:challenge], - ) - - begin - Launchy.open(authorization_uri) - rescue - # NOTE: ignore any exception, as the URL is printed above and may be - # opened manually - puts "Cannot open browser automatically, please click on the link below:" - puts "" - puts authorization_uri + params = CGI.parse(URI.parse(http_req[1]).query) + if params["code"].length != 1 or params["state"].length != 1 + raise "multiple values for code or state returned in callback; unable to process" + end + Thread.current[:code] = params["code"][0] + Thread.current[:state] = params["state"][0] + rescue StandardError => e + response = "Error processing request: #{e.message}" + response_code = "400 Bad Request" + end + connection.print "HTTP/1.1 #{response_code}\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Length: #{response.bytesize}\r\n" + + "Connection: close\r\n" + connection.print "\r\n" + connection.print response + connection.close + if response_code != "200 OK" + raise response + end + break end + ensure + server.close + end + end - webserv.join + webserv.abort_on_exception = true + + client = OpenIDConnect::Client.new( + authorization_endpoint: oidc_discovery.authorization_endpoint, + identifier: config.oidc_client, + redirect_uri: "http://localhost:" + server_addr, + secret: config.oidc_secret, + token_endpoint: oidc_discovery.token_endpoint, + ) + + authorization_uri = client.authorization_uri( + scope: ["openid", :email], + state: session[:state], + nonce: session[:nonce], + code_challenge_method: pkce[:method], + code_challenge: pkce[:challenge], + ) + + begin + Launchy.open(authorization_uri) + rescue + # NOTE: ignore any exception, as the URL is printed above and may be + # opened manually + puts "Cannot open browser automatically, please click on the link below:" + puts "" + puts authorization_uri + end - # check state == webserv[:state] - if webserv[:state] != session[:state] - abort 'Invalid state value received from OIDC Provider' - end + webserv.join - client.authorization_code = webserv[:code] - access_token = client.access_token!({code_verifier: pkce[:value]}) + # check state == webserv[:state] + if webserv[:state] != session[:state] + abort 'Invalid state value received from OIDC Provider' + end - provider_public_keys = oidc_discovery.jwks + client.authorization_code = webserv[:code] + access_token = client.access_token!({code_verifier: pkce[:value]}) - token = verify_token(access_token, provider_public_keys, config, session[:nonce]) + provider_public_keys = oidc_discovery.jwks - proof = Crypto.new().sign_proof(@priv_key, token["email"]) - return proof, access_token - end + token = verify_token(access_token, provider_public_keys, config, session[:nonce]) - private + proof = Crypto.new.sign_proof(@priv_key, token["email"]) + return proof, access_token + end - def generate_pkce() - pkce = {} - pkce[:method] = "S256" - # generate 43 <= x <= 128 character random string; the length below will generate a 2x hex length string - pkce[:value] = SecureRandom.hex(24) - # compute SHA256 hash and base64-urlencode hash - pkce[:challenge] = Base64.urlsafe_encode64(Digest::SHA256.digest(pkce[:value]), padding:false) - return pkce - end + private - def verify_token(access_token, public_keys, config, nonce) - begin - decoded_access_token = JSON::JWT.decode(access_token.to_s,public_keys) - rescue JSON::JWS::VerificationFailed => e - abort 'JWT Verification Failed: ' + e.to_s - else #success - token = JSON.parse(decoded_access_token.to_json) - end + def generate_pkce() + pkce = {} + pkce[:method] = "S256" + # generate 43 <= x <= 128 character random string; the length below will generate a 2x hex length string + pkce[:value] = SecureRandom.hex(24) + # compute SHA256 hash and base64-urlencode hash + pkce[:challenge] = Base64.urlsafe_encode64(Digest::SHA256.digest(pkce[:value]), padding:false) + return pkce + end - # verify issuer matches - if token["iss"] != config.oidc_issuer - abort 'Mismatched issuer in OIDC ID Token' - end + def verify_token(access_token, public_keys, config, nonce) + begin + decoded_access_token = JSON::JWT.decode(access_token.to_s,public_keys) + rescue JSON::JWS::VerificationFailed => e + abort 'JWT Verification Failed: ' + e.to_s + else #success + token = JSON.parse(decoded_access_token.to_json) + end - # verify it was intended for me - if token["aud"] != config.oidc_client - abort 'OIDC ID Token was not intended for this use' - end + # verify issuer matches + if token["iss"] != config.oidc_issuer + abort 'Mismatched issuer in OIDC ID Token' + end - # verify token has not expired (iat < now <= exp) - now = Time.now.to_i - if token["iat"] > now or now > token["exp"] - abort 'OIDC ID Token is expired' - end + # verify it was intended for me + if token["aud"] != config.oidc_client + abort 'OIDC ID Token was not intended for this use' + end - # verify nonce if present in token - if token.key?("nonce") and token["nonce"] != nonce - abort 'OIDC ID Token has incorrect nonce value' - end + # verify token has not expired (iat < now <= exp) + now = Time.now.to_i + if token["iat"] > now or now > token["exp"] + abort 'OIDC ID Token is expired' + end - # ensure that the OIDC provider has verified the email address - # note: this may have happened some time in the past - if token["email_verified"] != true - abort 'Email address in OIDC token has not been verified by provider' - end + # verify nonce if present in token + if token.key?("nonce") and token["nonce"] != nonce + abort 'OIDC ID Token has incorrect nonce value' + end - return token + # ensure that the OIDC provider has verified the email address + # note: this may have happened some time in the past + if token["email_verified"] != true + abort 'Email address in OIDC token has not been verified by provider' end -end \ No newline at end of file + + return token + end +end diff --git a/lib/rubygems/sigstore/options.rb b/lib/rubygems/sigstore/options.rb index 8083f49..1fa055c 100644 --- a/lib/rubygems/sigstore/options.rb +++ b/lib/rubygems/sigstore/options.rb @@ -13,9 +13,10 @@ # limitations under the License. module Gem::Sigstore - private - def self.options - @options ||= {} - @options - end + private + + def self.options + @options ||= {} + @options + end end diff --git a/lib/rubygems/sigstore/sign_extend.rb b/lib/rubygems/sigstore/sign_extend.rb index c32869c..c677565 100644 --- a/lib/rubygems/sigstore/sign_extend.rb +++ b/lib/rubygems/sigstore/sign_extend.rb @@ -27,14 +27,14 @@ Gem::CommandManager.instance.register_command :sign def find_gemspec(glob = "*.gemspec") - gemspecs = Dir.glob(glob).sort + gemspecs = Dir.glob(glob).sort - if gemspecs.size > 1 - alert_error "Multiple gemspecs found: #{gemspecs}, please specify one" - terminate_interaction(1) - end + if gemspecs.size > 1 + alert_error "Multiple gemspecs found: #{gemspecs}, please specify one" + terminate_interaction(1) + end - gemspecs.first + gemspecs.first end # overde the generic gem build command to lay are own --sign option on top @@ -46,94 +46,93 @@ def find_gemspec(glob = "*.gemspec") class Gem::Commands::BuildCommand alias_method :original_execute, :execute def execute - - config = SigStoreConfig.new().config + config = SigStoreConfig.new.config if Gem::Sigstore.options[:sign] - config = SigStoreConfig.new().config - priv_key, pub_key, enc_pub_key = Crypto.new().generate_keys - proof, access_token = OpenIDHandler.new(priv_key).get_token - puts "" - cert_response = HttpClient.new().get_cert(access_token, proof, enc_pub_key, config.fulcio_host) - certPEM, rootPem = cert_response.split(/\n{2,}/) - - Dir.mkdir("certs") unless File.exists?("certs") - File.write('certs/sigstore.pem', "#{certPEM}\n", nil , mode: 'w+') - - puts "Received fulcio signing certicate: certs/sigstore.pem" - puts "" - - # Run the gem build process (original_execute) - original_execute - - # Find the gemspec file for the project - gemspec_file = find_gemspec() - spec = Gem::Specification::load(gemspec_file) - - # Unwrap files for signing - File.open("#{spec.full_name}.gem", "rb") do |file| - Gem::Package::TarReader.new(file) do |tar| - tar.each do |entry| - if entry.file? - FileUtils.mkdir_p(File.dirname(entry.full_name)) - File.open(entry.full_name, "wb") do |f| - f.write(entry.read) - end - File.chmod(entry.header.mode, entry.full_name) - end - end + config = SigStoreConfig.new.config + priv_key, pub_key, enc_pub_key = Crypto.new.generate_keys + proof, access_token = OpenIDHandler.new(priv_key).get_token + puts "" + cert_response = HttpClient.new.get_cert(access_token, proof, enc_pub_key, config.fulcio_host) + certPEM, rootPem = cert_response.split(/\n{2,}/) + + Dir.mkdir("certs") unless File.exists?("certs") + File.write('certs/sigstore.pem', "#{certPEM}\n", nil , mode: 'w+') + + puts "Received fulcio signing certicate: certs/sigstore.pem" + puts "" + + # Run the gem build process (original_execute) + original_execute + + # Find the gemspec file for the project + gemspec_file = find_gemspec + spec = Gem::Specification::load(gemspec_file) + + # Unwrap files for signing + File.open("#{spec.full_name}.gem", "rb") do |file| + Gem::Package::TarReader.new(file) do |tar| + tar.each do |entry| + if entry.file? + FileUtils.mkdir_p(File.dirname(entry.full_name)) + File.open(entry.full_name, "wb") do |f| + f.write(entry.read) + end + File.chmod(entry.header.mode, entry.full_name) end + end end - - puts "" - puts " Updating #{spec.full_name}.gem with signed materials" - - checksums_file = File.read('checksums.yaml.gz') - checksums_digest = OpenSSL::Digest::SHA256.new(checksums_file) - checksums_signature = priv_key.sign checksums_digest, checksums_file - File.open('checksums.yaml.gz.sig', 'wb') do |f| - f.write(checksums_signature) - end - - metadata_file = File.read('metadata.gz') - metadata_digest = OpenSSL::Digest::SHA256.new(metadata_file) - metadata_signature = priv_key.sign metadata_digest, metadata_file - File.open('metadata.gz.sig', 'wb') do |f| - f.write(metadata_signature) - end - - data_file = File.read('data.tar.gz') - data_digest = OpenSSL::Digest::SHA256.new(data_file) - data_signature = priv_key.sign data_digest, data_file - File.open('data.tar.gz.sig', 'wb') do |f| - f.write(data_signature) - end - - gem_files = ["data.tar.gz", "data.tar.gz.sig", "metadata.gz", "metadata.gz.sig", "checksums.yaml.gz", "checksums.yaml.gz.sig"] - - File.open("#{spec.full_name}_signed.gem", 'wb') do |file| - Gem::Package::TarWriter.new(file) do |tar| - gem_files.each { |file| - tar.add_file_simple(File.basename(file), 0o666, File.size(file)) do |io| - File.open(file, 'rb') { |f| io.write(f.read) } - end - } + end + + puts "" + puts " Updating #{spec.full_name}.gem with signed materials" + + checksums_file = File.read('checksums.yaml.gz') + checksums_digest = OpenSSL::Digest::SHA256.new(checksums_file) + checksums_signature = priv_key.sign checksums_digest, checksums_file + File.open('checksums.yaml.gz.sig', 'wb') do |f| + f.write(checksums_signature) + end + + metadata_file = File.read('metadata.gz') + metadata_digest = OpenSSL::Digest::SHA256.new(metadata_file) + metadata_signature = priv_key.sign metadata_digest, metadata_file + File.open('metadata.gz.sig', 'wb') do |f| + f.write(metadata_signature) + end + + data_file = File.read('data.tar.gz') + data_digest = OpenSSL::Digest::SHA256.new(data_file) + data_signature = priv_key.sign data_digest, data_file + File.open('data.tar.gz.sig', 'wb') do |f| + f.write(data_signature) + end + + gem_files = ["data.tar.gz", "data.tar.gz.sig", "metadata.gz", "metadata.gz.sig", "checksums.yaml.gz", "checksums.yaml.gz.sig"] + + File.open("#{spec.full_name}_signed.gem", 'wb') do |file| + Gem::Package::TarWriter.new(file) do |tar| + gem_files.each do|file| + tar.add_file_simple(File.basename(file), 0o666, File.size(file)) do |io| + File.open(file, 'rb') {|f| io.write(f.read) } end + end end - - puts "" - puts " sigstore signing operation complete" - puts "" - puts " sending signing manifests to rekor.." - puts "" - rekor_response = HttpClient.new().submit_rekor(pub_key, data_digest, data_signature, certPEM, Base64.encode64(data_file), config.rekor_host) - print " rekor response: " - puts rekor_response - #clean up - Open3.popen3("rm data.tar.gz data.tar.gz.sig metadata.gz metadata.gz.sig checksums.yaml.gz checksums.yaml.gz.sig") do |stdin, stdout, stderr, thread| - puts stdout.read.chomp - end - puts "signed file: #{spec.full_name}_signed.gem" + end + + puts "" + puts " sigstore signing operation complete" + puts "" + puts " sending signing manifests to rekor.." + puts "" + rekor_response = HttpClient.new.submit_rekor(pub_key, data_digest, data_signature, certPEM, Base64.encode64(data_file), config.rekor_host) + print " rekor response: " + puts rekor_response + #clean up + Open3.popen3("rm data.tar.gz data.tar.gz.sig metadata.gz metadata.gz.sig checksums.yaml.gz checksums.yaml.gz.sig") do |stdin, stdout, stderr, thread| + puts stdout.read.chomp + end + puts "signed file: #{spec.full_name}_signed.gem" end end end diff --git a/lib/rubygems/sigstore/version.rb b/lib/rubygems/sigstore/version.rb index 5150ddf..24b2ab6 100644 --- a/lib/rubygems/sigstore/version.rb +++ b/lib/rubygems/sigstore/version.rb @@ -14,6 +14,6 @@ module Ruby module Sigstore - VERSION = "0.1.0" + VERSION = "0.1.0".freeze end end diff --git a/lib/rubygems_plugin.rb b/lib/rubygems_plugin.rb index 18e681c..1356b99 100644 --- a/lib/rubygems_plugin.rb +++ b/lib/rubygems_plugin.rb @@ -20,5 +20,5 @@ Gem::CommandManager.instance.register_command :verify [:sign, :verify, :build, :install].each do |cmd_name| - cmd = Gem::CommandManager.instance[cmd_name] + cmd = Gem::CommandManager.instance[cmd_name] end diff --git a/ruby-sigstore.gemspec b/ruby-sigstore.gemspec index da5bd29..44c5b33 100644 --- a/ruby-sigstore.gemspec +++ b/ruby-sigstore.gemspec @@ -20,8 +20,8 @@ Gem::Specification.new do |spec| spec.authors = ["Sigstore Community"] spec.email = ["lhinds@redhat.com"] - spec.summary = %q{Sigstore signing client.} - spec.description = %q{Sigstore} + spec.summary = %q(Sigstore signing client.) + spec.description = %q(Sigstore) spec.homepage = "https://github.com/sigstore/ruby-sigstore" spec.license = "MIT" spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") @@ -31,16 +31,15 @@ Gem::Specification.new do |spec| spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = "https://github.com/sigstore/ruby-sigstore" spec.metadata["changelog_uri"] = "https://github.com/sigstore/ruby-sigstore/CHANGELOG.md" - spec.cert_chain = ['certs/sigstore.pem'] - + spec.cert_chain = ['certs/sigstore.pem'] # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do - `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do + `git ls-files -z`.split("\x0").reject {|f| f.match(%r{^(test|spec|features)/}) } end spec.bindir = "exe" - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f) } spec.require_paths = ["lib"] spec.add_development_dependency "pp", "0.2.0"