Skip to content
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
8 changes: 6 additions & 2 deletions lib/rubygems/commands/cert_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ def initialize
end

add_option("-A", "--key-algorithm ALGORITHM",
"Select which key algorithm to use for --build") do |algorithm, options|
"Select key algorithm for --build from",
"RSA, DSA, EC, ML-DSA-44, ML-DSA-65,",
"or ML-DSA-87. Defaults to "\
"#{Gem::Security::DEFAULT_KEY_ALGORITHM}.") do |algorithm, options|
options[:key_algorithm] = algorithm
end

Expand Down Expand Up @@ -100,7 +103,8 @@ def open_private_key(key_file)
rescue Errno::ENOENT
raise Gem::OptionParser::InvalidArgument, "#{key_file}: does not exist"
rescue OpenSSL::PKey::PKeyError, ArgumentError
raise Gem::OptionParser::InvalidArgument, "#{key_file}: invalid RSA, DSA, or EC key"
raise Gem::OptionParser::InvalidArgument, "#{key_file}: invalid "\
"RSA, DSA, EC, ML-DSA-44, ML-DSA-65, or ML-DSA-87 key"
end

def execute
Expand Down
48 changes: 41 additions & 7 deletions lib/rubygems/security.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@
# 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.
# -A, --key-algorithm ALGORITHM Select key algorithm for --build from
# RSA, DSA, EC, ML-DSA-44, ML-DSA-65,
# or ML-DSA-87. 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
Expand Down Expand Up @@ -351,6 +353,20 @@ class Exception < Gem::Exception; end

EC_NAME = "secp384r1"

##
# ML-DSA algorithm names to use when building a key pair.
# ML-DSA-44: NIST security strength category 2, signature size 2420 bytes
# ML-DSA-65: NIST security strength category 3, signature size 3309 bytes
# ML-DSA-87: NIST security strength category 5, signature size 4627 bytes
# See NIST FIPS 204 Section 4 (Parameter Sets).
# https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf
# See Security (Evaluation Criteria) - 4.A.5 Security Strength Categories.
# https://csrc.nist.gov/projects/post-quantum-cryptography/post-quantum-cryptography-standardization/evaluation-criteria/security-(evaluation-criteria)

ML_DSA_44_NAME = "ML-DSA-44"
ML_DSA_65_NAME = "ML-DSA-65"
ML_DSA_87_NAME = "ML-DSA-87"

##
# Cipher used to encrypt the key pair used to sign gems.
# Must be in the list returned by OpenSSL::Cipher.ciphers
Expand Down Expand Up @@ -464,25 +480,41 @@ def self.create_digest(algorithm = DIGEST_NAME)
end

##
# Creates a new key pair of the specified +algorithm+. RSA, DSA, and EC
# are supported.
# Creates a new key pair of the specified +algorithm+. RSA, DSA, EC,
# ML-DSA-44, ML-DSA-65, and ML-DSA-87 are supported.

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 "dsa"
OpenSSL::PKey::DSA.new(RSA_DSA_KEY_LENGTH)
when "ec"
OpenSSL::PKey::EC.generate(EC_NAME)
when "ml-dsa-44"
OpenSSL::PKey.generate_key(ML_DSA_44_NAME)
when "ml-dsa-65"
OpenSSL::PKey.generate_key(ML_DSA_65_NAME)
when "ml-dsa-87"
OpenSSL::PKey.generate_key(ML_DSA_87_NAME)
else
raise Gem::Security::Exception,
"#{algorithm} algorithm not found. RSA, DSA, and EC algorithms are supported."
"#{algorithm} algorithm not found. RSA, DSA, EC, ML-DSA-44, "\
"ML-DSA-65, and ML-DSA-87 algorithms are supported."
end
end
end

##
# Returns whether the +key+ requires an explicit digest algorithm for signing
# and verification. ML-DSA has a built-in digest and does not accept one.

def self.digest_required?(key)
key.is_a?(OpenSSL::PKey::RSA) || key.is_a?(OpenSSL::PKey::DSA) ||
key.is_a?(OpenSSL::PKey::EC)
end

##
# Turns +email_address+ into an OpenSSL::X509::Name

Expand Down Expand Up @@ -562,8 +594,10 @@ def self.sign(certificate, signing_key, signing_cert, age = ONE_YEAR, extensions
signed = create_cert signee_subject, signee_key, age, extensions, serial
signed.issuer = signing_cert.subject

digest_name = Gem::Security::DIGEST_NAME if digest_required?(signing_key)

begin
signed.sign signing_key, Gem::Security::DIGEST_NAME
signed.sign signing_key, digest_name
rescue OpenSSL::PKey::PKeyError, ArgumentError
raise Gem::Security::Exception,
"incorrect signing key for signing"
Expand Down
4 changes: 3 additions & 1 deletion lib/rubygems/security/policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ def check_chain(chain, time)
# the +digest+ algorithm.

def check_data(public_key, digest, signature, data)
filtered_digest = digest if Gem::Security.digest_required?(public_key)

raise Gem::Security::Exception, "invalid signature" unless
public_key.verify digest, signature, data.digest
public_key.verify filtered_digest, signature, data.digest

true
end
Expand Down
3 changes: 2 additions & 1 deletion lib/rubygems/security/signer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ def sign(data)

Gem::Security::SigningPolicy.verify @cert_chain, @key, {}, {}, full_name

@key.sign @digest_algorithm.new, data
digest = @digest_algorithm.new if Gem::Security.digest_required?(@key)
@key.sign digest, data
end

##
Expand Down
12 changes: 6 additions & 6 deletions spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
it "connects" do
ssl_server = start_ssl_server
allow(Bundler.settings).to receive(:[]).and_call_original
allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PemUtilities::CA_CERT_FILE)
allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PEMUtilities::CA_CERT_FILE)
response = fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml")
expect(response.code).to eq("200")
end
Expand All @@ -30,8 +30,8 @@
verify_mode: OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
)
allow(Bundler.settings).to receive(:[]).and_call_original
allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PemUtilities::CA_CERT_FILE)
allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(Gem::PemUtilities::CLIENT_FILE)
allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PEMUtilities::CA_CERT_FILE)
allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(Gem::PEMUtilities::CLIENT_FILE)
response = fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml")
expect(response.code).to eq("200")
end
Expand All @@ -45,7 +45,7 @@
it "connects" do
ssl_server = start_ssl_server(mode: :pqc)
allow(Bundler.settings).to receive(:[]).and_call_original
allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PemUtilities::MLDSA65_CA_CERT_FILE)
allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PEMUtilities::MLDSA65_CA_CERT_FILE)
response = fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml")
expect(response.code).to eq("200")
end
Expand All @@ -56,8 +56,8 @@
verify_mode: OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
)
allow(Bundler.settings).to receive(:[]).and_call_original
allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PemUtilities::MLDSA65_CA_CERT_FILE)
allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(Gem::PemUtilities::MLDSA65_CLIENT_FILE)
allow(Bundler.settings).to receive(:[]).with(:ssl_ca_cert).and_return(Gem::PEMUtilities::MLDSA65_CA_CERT_FILE)
allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(Gem::PEMUtilities::MLDSA65_CLIENT_FILE)
response = fetch_path("https://localhost:#{ssl_server.addr[1]}/yaml")
expect(response.code).to eq("200")
end
Expand Down
11 changes: 10 additions & 1 deletion test/rubygems/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
require "zlib"
require_relative "mock_gem_ui"
require_relative "pem_utilities"
require_relative "pqc_utilities"

# JRuby on Windows raises TypeError inside File.symlink (the wincode helper
# trips on a nil path), so any test that exercises Gem::Installer's symlink
Expand Down Expand Up @@ -1659,7 +1660,15 @@ def prefetch(reqs) # :nodoc:
end
end

include Gem::PemUtilities
include Gem::PEMUtilities

include Gem::PQCUtilities

def omit_unless_support_pqc
without_pqc_support do |message|
omit message
end
end
end

# https://github.com/seattlerb/minitest/blob/13c48a03d84a2a87855a4de0c959f96800100357/lib/minitest/mock.rb#L192
Expand Down
70 changes: 4 additions & 66 deletions test/rubygems/local_ssl_server_utilities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
require "socket"
require "openssl"
require_relative "pem_utilities"
require_relative "pqc_utilities"

module Gem::LocalSSLServerUtilities
include Gem::PemUtilities
include Gem::PEMUtilities
include Gem::PQCUtilities

def initialize_ssl_server
@ssl_server_thread = nil
@ssl_server = nil
Expand Down Expand Up @@ -74,69 +77,4 @@ def handle_request(client)
client.print "HTTP/1.1 404 Not Found\r\n\r\n"
end
end

def without_pqc_support(&block)
# PQC algorithms ML-KEM and ML-DSA require OpenSSL >= 3.5.
# https://openssl-library.org/post/2025-04-08-openssl-35-final-release/
unless OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30500000
yield "PQC algorithms require OpenSSL >= 3.5"
return
end
# ctx.groups (OpenSSL::SSL::SSLContext#groups) used in start_ssl_server
# mode :pqc requires Ruby OpenSSL >= 4.0.
unless Gem::Version.new(OpenSSL::VERSION) >= Gem::Version.new("4.0")
yield "PQC test requires Ruby OpenSSL >= 4.0"
return
end
# Even with a new enough OpenSSL, the runtime may keep PQC groups and
# signature algorithms out of its default negotiation lists (for example
# RHEL's system-wide crypto policies). The PQC server forces both, while
# the gem fetcher connects with the default client configuration, so a
# real loopback handshake is the only reliable way to tell whether this
# environment can negotiate PQC at all.
unless Gem::LocalSSLServerUtilities.support_pqc_handshake?
yield "PQC handshake is not available in this OpenSSL configuration"
end
end

# Probe an actual PQC handshake between a forced-PQC server and a
# default-configured client, mirroring what the integration tests exercise.
# Memoized so the probe runs at most once per process.
def self.support_pqc_handshake?
return @support_pqc_handshake unless @support_pqc_handshake.nil?

@support_pqc_handshake = probe_pqc_handshake
end

def self.probe_pqc_handshake
server = TCPServer.new("127.0.0.1", 0)
ctx = OpenSSL::SSL::SSLContext.new
ctx.cert = Gem::PemUtilities::MLDSA65_SSL_CERT
ctx.key = Gem::PemUtilities::MLDSA65_SSL_KEY
ctx.groups = "X25519MLKEM768"
ssl_server = OpenSSL::SSL::SSLServer.new(server, ctx)

port = server.addr[1]
server_thread = Thread.new do
client = ssl_server.accept
client.close
rescue OpenSSL::OpenSSLError
nil
end

client_ctx = OpenSSL::SSL::SSLContext.new
client_ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
socket = TCPSocket.new("127.0.0.1", port)
ssl = OpenSSL::SSL::SSLSocket.new(socket, client_ctx)
ssl.connect
ssl.close
true
rescue OpenSSL::OpenSSLError, SystemCallError
false
ensure
server_thread&.join(5)
server_thread&.kill if server_thread&.alive?
ssl_server&.close
server&.close
end
end
90 changes: 90 additions & 0 deletions test/rubygems/mldsa65_encrypted_private_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIQdTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQB26p6NP6pNd/2Ouj
74A7mQICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEF007QVOyF/R6ldk
jAaUCzAEghAQg1iVCVmZWZwdIWn+2HZDBwPIcnQJAM8KU1E3S+447Zq6MOe+CWXr
lJ8uFS9uaNd8uvCv4FYtZCHp/z3px1RXrXxHA+P4fS9PTh+m4PPqC1BGHKduBDI9
5W/NUoXaGElMnrtKuadqF6ckvD/gjfi4ISz45UHeXL5oznzyDuNi0WYTGsqtcDmU
39UtuDis/99vJSSqfDUtIAGJzv5oeJq1/HsYB1U6DMvApQo8ppots9fXSQ/vovDM
I4XiW/+s60fmoEn4DHdM1RVnGHDSBrk7b23MDL8Q+x3Y9j7SHIrCoIrElOU2cKhf
jJtJrHPP59cmU7e+ccbYl6Nuj0qCeHEHX01amJOkdCj8ALMMfzohiOyOfLNSDwVc
ECTsg6XoLiHf3KsjNk2XzzMM3BZDuxjNOqfNAy2K/0cizuGKO6U098kwLZ9vDf8R
o9QE+ri9fStmtpB2hc1TuxGxIhWoW86iuLcSmSYieXWuEuEaPRwiT+G4O2gn5q/u
+IC5Mz5yLE4puEiF45jOTR1HIKUiNh+8fgyOrDVwe3AHQjMAKMoERf5vBlKS7IC6
4eR9tleuWjfhHszci/X74oBHRHcp1Ls3J9Hq3PR27cjdXqc6ia736peXeWpi+g/L
S7ZhTIRgOGIrRcZiQRycV8Y4euWezILuooO0ROvTOBRHFJDDvrWMQQXoYSPLQ/8r
0fW7CCC2UD0NFSGTFTSDSEvyF2kbsb48YsBpHU9anJiIHouffI4rEhXCFusOmWbC
Hao0QxSM7TI3WRnoUP5Xp4Hs/ZqHknAs2+vSPEzEFifqeW3jiq90YbKYvnfqdDVq
7tu3qzhBR5JxM28YfCHWPg9n0VKNxGFZPkE2+thTaW+rbUcZsLqrzYUA5JYYLax9
FYmVBr89u39UwRdI32eKd4eONu97ILPZAJQHEuE51qWTDRIB+H6OneQDH5Aw27f1
aWNN0OkBbcmN/ztF31M+MIHWwBF0jx1lzjGi7PTWm4QkbKVxk4j/rTBliAXLw//t
XmLxFykpywOoE/KdZpSOvF1A9rUT1VEN9Wsfp2mT8xS5Iojw67cYRz1Nakn/Hhij
k9eihxlj8UvLpMGuSkrEoC34C0mZPpGuq3PQ6jYtER43r+VsjFx9u3Yq1G4uWZqC
w7COV/o3Wfqse5tNnP8Ia5xRaERMLHTfGz3V4g9NfYR3NZY4tguTqX3Nz32TTpfX
MF1HSB0jCJ3Ru1FCDljCzL7X4qru7ivnZkFJ04NMQhGmeIgVz4QLwdWa9JRVO2pd
HXSyYgXUnJYs4j1ufs3e29oUCEuKF55LSN1jRHsNPFx0AHF7auP8Qdj4m8UieDcA
LDojE5kt6D3V9DpD55+Bv331pfkgL4IXgij38jzb60+k7QhEsLIGFHK9sbWfaJ/b
Iqlhfc8/7iaMDNLQgAh2Tw/UGmcdwn/mvrif0a40KrdrQPLwwXWLdsAE7IjMmGrP
PoX3go6qE6yFWBa5SNN9BqZSkTaWEit5K6hZMiB87Vq7iN4W5DDirF5tPd0bqXar
0rA/ctc3UaqdIVgDGn1QMXc959/+WDt2ZfvD2MI6bvrkxNrwnMEQiXv5it1WZp52
G1UYadVZJ6q/KMdHHPIXIPHN0w2LQ1GvkSuTf+qnY/YJuyzwzy4bpF+8Ja8TcmDt
wrZXvAVRpjbXQkhKe3eKCDBA5iSG0uDpNHH1qSYMzCvX1xINTFk4GzfM01OP0dm8
PWhvp+/dh8L479Bu9gRTqzg1Iwrn7LAjqRzp6fLzZHA/AzFhDiDTUXNlKu7dH/+0
0z4ehFcbBGYeHSt78GpwzgG0AtrbT0vrGx7LukpjxU+gjl1MwRO0J6gZvwhPkPut
19et5LWyngscXl0ZNjzEd3NbLS5n6nicHL2ubcN1XVPr2GP3ZMt+e7b/okqanpcw
rxG2CymIZTBVQklTTlh1S/wzPoIVc0fQV4ZJRG8RimL0HnlT1UgHnN/KkLcNqkxb
xO0paR5FQuX0S7D/4XoRpmwXJ8Mgc90jxAQTwPCsGj4ysO5B5dAQCPSrcI0EI02/
vyBIaQRkmd9Pa+BovY7KNlD/wgjgXJaddf2H4F/87hUIc0+kJATbjLjCzCPiaBT8
doDYZ/rEKz7lH6DGxI4OQIIHh5ndc5GU+JLYCzf5IXyeKge0HwMN6azu3+ssRHrw
bA9WcuVSBcaufi2AF7bnkVvDqp+55id+XtMHPmuAgHEXvJGO0ycHZ1dsstiao9tS
lrt5Ait1qICVCwMDLOEXNVPZsuO+V8RfRP1h+FGHpfojxSmyLOgBLL4hPDMxPspD
rpVNNcyfL7fAY9mmWSvs0GfWaHZ3VFJM/zwccjEXfGwCGp/Lj20XRdM3Lvk6MRMh
7MUsiKKKWHaLCEMj2qz/8jrF5fs5gBTOtDPY2MuNmjobcHVtDFLgnumUU8M+X01K
6ozhwX8DSaAf8DOQx/EI7XoJ1Ij3pxVG+VaefIYmnqRb+E/1xnzGrfKpV5qp+Ujm
CgS+i8EaJrkxKQOcs/UHpoMnOn5DrkTBsWMZDeJmb8mg4ujgpSXLK6ALLkvMgAWN
QATI9WhsOzuYQpDJGngU8pXjZ8tNN3R1LDhmYvHEITqFN8/tELkd4p0Dzk/0aVCW
truziRHZU8Mx+doQdoD8z30oTMYTPsQRCKXC8PNxVC8aWXSvJqOH7SUHGTlxeZ6X
78rxwfNpsnoieLlQsO2jVb+fcCYZGarqoCyroj0ZmPmeL6+iPYKHq1BvVFT1/h/Y
DuK5EK5Y8baJp1Lh2GRbr1PHHsrhlc/2r+v0+jWnrdHdT98A9l90/MCarcFAkicK
UDDny9wLIiTUMDARDKW6jTxig4e7KUYwUgcl1sOdBSGljIbn3/R227MULhiZwgGv
SuAJjzB24wdO/YHjmLJZLrACrbIA7DdbLnlv9JBq4+Sw2+M0quJUyURTTrDWdARm
CKzjpjj5EYkkkJDcDtNDwfVHVAWlYdbgM0GR+LIXP9BftolihRktfEUK/9BwTYsU
eNOzEAHLWl+KSWRWsj0qC2tNC3hcAMhiP99kqn6tyg9CVAnKuegwaCAz6Wn49kl5
LBmiCjWmLPuz0uSDMBQI+rsKslwT92/wuap33jG7O840HhV+rJ4rNeuuMTiHMLvZ
zDpeFpRZ9RgbRgHkzKFCQGhHT1kVaL2viTFwuTIejcR+Mb//l/TJqfL0/EoL2Z5Y
MSQXQDcZ0CcBWQSL+x169C5hzz55UU2/3OAnxnAENXVK1djHwYxQvVpYfZem+C55
yJwFlyR+5QULZvV6AlTqYBjnpf33tfvhlv9UoR87IGuORWAEBQSmyDPeCipz777a
0Ct09DsXnA18Z3nQDQYQNEdOM3iNfqteorpSHeWpiMKe3GVz7VeFceyx7T3OM9XQ
4xZQ4+Cz9IdD6cxfDHhMu5PbO7oA4kb61PvAKlhmQY1L05NTlWmPDKUHMvzWRCwZ
GXy8h9BAD8/nJO8liIvdu7Vil+r8FQHGT8S/BSUz/ghCJncyhcSyg5gm1l2x1KIO
knDY3ER5bp2jGM6blZZq0wQVlfyyL24c9NEtAiwS+Dcr4Mt/HHEKYKF3aomzL1SG
qu+zE4dLhAJWb18K1y8Wo0y5gZw+AiblDlftD6PDuqO1x3bJJB+cifYoD7EsxgbG
LZco4e8hnGRXNn0DjwBtNtD295A4DJfiPxBNvVqZDO0lp9Z5AzSDNyCdoYjO5qQZ
CYaNecA68fnKqgBqtEJVn723CiS0lifvvXsktwMWBOXAvwkZcaH6tQ5jED7SHJzE
LNIdk6GK4zhSRjFjAauu4CxSTUQSeAJsf0xV5vmal4Jt7MpTgbL2cOlz8x0NSQYQ
TmK+Av1xHbw8cshQExA5ubDgTFXjmHlEEWt0k/4eSSFpwB9Dr/yqJT8BV8kSZdNX
hQfIbaUO2au/eFB4b7Zq1CrS84FhkjPsbbSnJ1Da9bk0JKX2SJ3z3o9QBZfH+eev
fr6lDZ6g/pKNMESP3HR47vcKB/Y4AieVX0Lkb7s4Apj5WwS60Sdxt0+k4q/svPmg
TcTNNFgAkISEMGYKH6me83FTV62RSoxGjuJck6KrY18fGSVeSKH9syJDalHwp46T
nLP/BoBplA7H+jyLuJCnfwsmwAhtIrKeTmT6vK4LBQSyR0Rg6XCnZOI5w+ug2uPw
HHK0sEw2XTTlfaHJzNl1BOnYpryLSi9RMBwJ8osgRTmut1GL/yfd4odW7lAia+tI
JI1gZUB6u5NdN3SNP2KjT4WOz/TiYo+V5f7n8HBKCu2lhnG4sxmomHdH9Ye3EPCT
qBzU6o+Gld7og4GMFGTE+FQCjSeEu7UrpeR8MaTh5V8FWtYtpKasBgaixNMx1xp6
1lwM0aWphqqWNFXIvwQ6Ja0ajusyIGQZ8V0Fbi3WPIghwXEf4QFVG7MvBGwte8RE
4QJp2qlQo0wmbWHM8RCous4LhqIjgTbPx/HlnwkeMF2hqgxf4gEGNpWUMFl87K9i
c8G+8o2H4a72XEEBpDiITh9HQzP4YMNwfRGaugaymZbcXtgktCwwljgQo9cIkQf2
98AnzOSURaJAGfF/Pww5saMxP1+u29ytEW2RFv8P1kBuVp0dL5erSJvlf2Cb0FfD
KiIeuZWbjE3k72NbKQUco+xnYdMf9k8gdVEIS3eax1KNcbChS6d3ow4sqbWSqDho
LnQJUL6B8xFs+lvGZHcJu2mHBZRSoHXkGwzQjPtUTGqAWYRpOT0sOAvFtIEJ9UcS
6Tr4ZaF9TEldNjh1v2EaVoRKo6pJFUFZ/9DI5q4lPztKM+LBktBsBtgPMYOEA5rn
bddHk+Ohq0/mTlibMTZl56AIwIolmXLNGttoXNFlriITwSen51rMxxP8DqEF9Eo1
fd9Sz9BRefJgRTsmNUZcz1s7TqXOAIEDRV2Ka9Virj6IKehs9xZEBjSouFSZgSO5
HW+Oj+RVvo25MOGcRNzs7YO3lI5xmcYdTEuJCuRGFxDB7NW0inkj6X6DjNpdWPbz
YTAbVZbbzq4aN5Z8qo/7ql5GmkOI3rrzYWK2g5gLI6qjpN2WN1lvQcAzso11iaKB
U2u2CpElMxm8MN0SgoAv8X17hzHzw2FZ0v1Ujxcax4XJJTR8Wa8gRt7WzopQt8fT
dIhbutgbi4AQeXtULtr8x1A/mWJOxi7wb+qJTz4To5nvXkZJPvIgeNVlNM9KiQsh
xrFgfBsu8tw8C8kiXIS6hliNRrHNpD9yDB/kxtvTzeyIts5qswmcrmQSv8FgY4j0
H8ynxfan1lsW1HqYgL7DKRS+6ld/AWDNc2P8kvShBzslkLejmNszTTQO8MnjhjTg
6x1FNCtDq9Gof8hiX7qc4DsPTMl3Vxd8bH2ZC+WAh+Q8CN8PsXSw5QHJcR41Ao0L
5FAdRKs0OpaaMrgjB1aFWtKAZj26UK35ztIzwVfzgoX5NpQyC9WfH/iCc7DFADzg
TwG4SIFfzIw0lk5LX+5ontB+pImh1vA1REfD+3IG/tw6ZoC1Lk+NtUw=
-----END ENCRYPTED PRIVATE KEY-----
Loading