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

ssl_peer_fingerprint_verification for self-signed certs #150

Closed
wants to merge 5 commits into from
Closed
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
36 changes: 33 additions & 3 deletions lib/winrm/http/transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ def initialize(endpoint)
# @param [String] The XML SOAP message
# @returns [REXML::Document] The parsed response body
def send_request(message)
ssl_peer_fingerprint_verification!
log_soap_message(message)
hdr = {
'Content-Type' => 'application/soap+xml;charset=UTF-8',
'Content-Length' => message.length }
hdr = { 'Content-Type' => 'application/soap+xml;charset=UTF-8',
'Content-Length' => message.length }
resp = @httpcli.post(@endpoint, message, hdr)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the time of this call to POST our first message, we don't know if the fingerprint will match, but we go ahead and send the http request.

We have already connected once in ssl_peer_fingerprint_verification! but if we had a complex malicious-in-the-middle scenario, they could wait for the initial verify then redirect all subsequent connections.

I'm not sure of all the initial http soap requests winrm makes, but I'd like to verify that we could fail after that first request and still be safe. It's obviously fine for negotiate and kerberos, but I'm unsure if the first request for plaintext would send anything.

log_soap_message(resp.http_body.content)
verify_ssl_fingerprint(resp.peer_cert)
Copy link
Member

Choose a reason for hiding this comment

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

You need a if @ssl_peer_fingerprint here so it is not called for normal requests. Or perhaps even better, return from that method if its nil.

handler = WinRM::ResponseHandler.new(resp.http_body.content, resp.status)
handler.parse_to_xml
end
Expand All @@ -67,6 +68,34 @@ def no_ssl_peer_verification!
@httpcli.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
end

# SSL Peer Fingerprint Verification prior to connecting
def ssl_peer_fingerprint_verification!
return unless @ssl_peer_fingerprint && ! @ssl_peer_fingerprint_verified
connection_cert = shady_ssl_connection.peer_cert_chain.last
Copy link
Member

Choose a reason for hiding this comment

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

should sysclose be called on the SSL socket when done?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wouldn't hurt.

verify_ssl_fingerprint(connection_cert)
@logger.info("initial ssl fingerprint #{@ssl_peer_fingerprint} verified\n")
@ssl_peer_fingerprint_verified = true
no_ssl_peer_verification!
end

# Connect without verification to retrieve untrusted ssl context
def shady_ssl_connection
noverify_peer_context = OpenSSL::SSL::SSLContext.new
noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
tcp_connection = TCPSocket.new(@endpoint.host, @endpoint.port)
shady_ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection,
noverify_peer_context)
shady_ssl_connection.connect
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ssl_fingerprint_verification! will make an outbound tcp connection before sending any traffic to verify our cert fingerprint. See #151

shady_ssl_connection
end

# compare @ssl_peer_fingerprint to current ssl context
def verify_ssl_fingerprint(cert)
conn_fingerprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s
return unless @ssl_peer_fingerprint.casecmp(conn_fingerprint) != 0
fail "ssl fingerprint mismatch!!!!\n"
end

# HTTP Client receive timeout. How long should a remote call wait for a
# for a response from WinRM?
def receive_timeout=(sec)
Expand Down Expand Up @@ -112,6 +141,7 @@ def initialize(endpoint, user, pass, ca_trust_path = nil, opts)
no_sspi_auth! if opts[:disable_sspi]
basic_auth_only! if opts[:basic_auth_only]
no_ssl_peer_verification! if opts[:no_ssl_peer_verification]
@ssl_peer_fingerprint = opts[:ssl_peer_fingerprint]
end
end

Expand Down
37 changes: 37 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'bundler/setup'
require 'winrm'
require 'json'
require 'openssl'
require_relative 'matchers'

# Creates a WinRM connection for integration tests
Expand Down Expand Up @@ -41,6 +42,42 @@ def symbolize_keys(hash)
end
end
# rubocop:enable Metrics/MethodLength

# create a simple cert for a public_key
def test_cert(public_key)
subject = OpenSSL::X509::Name.parse('/C=BE/O=Test/OU=Test/CN=Test')
cert = OpenSSL::X509::Certificate.new
cert.subject = cert.issuer = subject
cert.not_before = Time.now
cert.not_after = Time.now + 365 * 24 * 3600
cert.public_key = public_key
cert.serial = 0x0
cert.version = 2
cert
end

# add CA and subject extensions to cert
def add_cert_extensions(cert)
ef = OpenSSL::X509::ExtensionFactory.new
ef.subject_certificate = cert
ef.issuer_certificate = cert
cert.extensions = [
ef.create_extension('basicConstraints', 'CA:TRUE', true),
ef.create_extension('subjectKeyIdentifier', 'hash'),
# ef.create_extension('keyUsage', 'cRLSign,keyCertSign', true),
]
cert.add_extension ef.create_extension('authorityKeyIdentifier',
'keyid:always,issuer:always')
cert
end

# create a self-signed-cert from private key
def gen_self_signed_cert(key)
plain_cert = test_cert(key.public_key)
cert = add_cert_extensions(plain_cert)
cert.sign key, OpenSSL::Digest::SHA1.new
cert
end
end

RSpec.configure do |config|
Expand Down