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

OpenSSL: use Windows' system root certificate store #13187

Merged
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
57 changes: 57 additions & 0 deletions src/crystal/system/win32/crypto.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
require "c/wincrypt"
require "openssl"

module Crystal::System::Crypto
private ServerAuthOID = "1.3.6.1.5.5.7.3.1"

# heavily based on cURL's code for importing system certificates on Windows:
# https://github.com/curl/curl/blob/2f17a9b654121dd1ecf4fc043c6d08a9da3522db/lib/vtls/openssl.c#L3015-L3157
private def self.each_system_certificate(store_name : String, &)
now = ::Time.utc

return unless cert_store = LibC.CertOpenSystemStoreW(nil, System.to_wstr(store_name))

eku = Pointer(LibC::CERT_USAGE).null
cert_context = Pointer(LibC::CERT_CONTEXT).null
while cert_context = LibC.CertEnumCertificatesInStore(cert_store, cert_context)
next unless cert_context.value.dwCertEncodingType == LibC::X509_ASN_ENCODING

next if cert_context.value.pbCertEncoded.nil?

not_before = Crystal::System::Time.from_filetime(cert_context.value.pCertInfo.value.notBefore)
not_after = Crystal::System::Time.from_filetime(cert_context.value.pCertInfo.value.notAfter)
next unless not_before <= now <= not_after

# look for the serverAuth OID if extended key usage exists
if LibC.CertGetEnhancedKeyUsage(cert_context, 0, nil, out eku_size) != 0
eku = eku.as(UInt8*).realloc(eku_size).as(LibC::CERT_USAGE*)
next unless LibC.CertGetEnhancedKeyUsage(cert_context, 0, eku, pointerof(eku_size)) != 0
next unless (0...eku.value.cUsageIdentifier).any? do |i|
LibC.strcmp(eku.value.rgpszUsageIdentifier[i], ServerAuthOID) == 0
end
end

encoded = Slice.new(cert_context.value.pbCertEncoded, cert_context.value.cbCertEncoded)
until encoded.empty?
cert, encoded = OpenSSL::X509::Certificate.from_der?(encoded)
break unless cert
yield cert
end
end
ensure
LibC.CertCloseStore(cert_store, 0) if cert_store
end

private class_getter system_root_certificates : Array(OpenSSL::X509::Certificate) do
certs = [] of OpenSSL::X509::Certificate
each_system_certificate("ROOT") { |cert| certs << cert }
certs
end

def self.populate_system_root_certificates(ssl_context)
cert_store = LibSSL.ssl_ctx_get_cert_store(ssl_context)
system_root_certificates.each do |cert|
LibCrypto.x509_store_add_cert(cert_store, cert)
end
end
end
83 changes: 83 additions & 0 deletions src/lib_c/x86_64-windows-msvc/c/wincrypt.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
require "c/win_def"
require "c/minwinbase"
require "c/winnt"

@[Link("crypt32")]
lib LibC
alias HCERTSTORE = Void*
alias HCRYPTPROV_LEGACY = Void*

struct CERT_NAME_BLOB
cbData : DWORD
pbData : BYTE*
end

struct CRYPT_INTEGER_BLOB
cbData : DWORD
pbData : BYTE*
end

struct CRYPT_OBJID_BLOB
cbData : DWORD
pbData : BYTE*
end

struct CRYPT_BIT_BLOB
cbData : DWORD
pbData : BYTE*
cUnusedBits : DWORD
end

struct CRYPT_ALGORITHM_IDENTIFIER
pszObjId : LPSTR
parameters : CRYPT_OBJID_BLOB
end

struct CERT_PUBLIC_KEY_INFO
algorithm : CRYPT_ALGORITHM_IDENTIFIER
publicKey : CRYPT_BIT_BLOB
end

struct CERT_EXTENSION
pszObjId : LPSTR
fCritical : BOOL
value : CRYPT_OBJID_BLOB
end

struct CERT_INFO
dwVersion : DWORD
serialNumber : CRYPT_INTEGER_BLOB
signatureAlgorithm : CRYPT_ALGORITHM_IDENTIFIER
issuer : CERT_NAME_BLOB
notBefore : FILETIME
notAfter : FILETIME
subject : CERT_NAME_BLOB
subjectPublicKeyInfo : CERT_PUBLIC_KEY_INFO
issuerUniqueId : CRYPT_BIT_BLOB
subjectUniqueId : CRYPT_BIT_BLOB
cExtension : DWORD
rgExtension : CERT_EXTENSION*
end

struct CERT_USAGE
cUsageIdentifier : DWORD
rgpszUsageIdentifier : LPSTR*
end

X509_ASN_ENCODING = 0x00000001
PKCS_7_ASN_ENCODING = 0x00010000

struct CERT_CONTEXT
dwCertEncodingType : DWORD
pbCertEncoded : BYTE*
cbCertEncoded : DWORD
pCertInfo : CERT_INFO*
hCertStore : HCERTSTORE
end

fun CertOpenSystemStoreW(hProv : HCRYPTPROV_LEGACY, szSubsystemProtocol : LPWSTR) : HCERTSTORE
fun CertCloseStore(hCertStore : HCERTSTORE, dwFlags : DWORD) : BOOL

fun CertEnumCertificatesInStore(hCertStore : HCERTSTORE, pPrevCertContext : CERT_CONTEXT*) : CERT_CONTEXT*
fun CertGetEnhancedKeyUsage(pCertContext : CERT_CONTEXT*, dwFlags : DWORD, pUsage : CERT_USAGE*, pcbUsage : DWORD*) : BOOL
end
4 changes: 4 additions & 0 deletions src/openssl/lib_crypto.cr
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ lib LibCrypto
type X509_EXTENSION = Void*
type X509_NAME = Void*
type X509_NAME_ENTRY = Void*
type X509_STORE = Void*
type X509_STORE_CTX = Void*

struct Bio
Expand Down Expand Up @@ -316,6 +317,7 @@ lib LibCrypto
fun sk_value(x0 : Void*, x1 : Int) : Void*
{% end %}

fun d2i_X509(a : X509*, ppin : UInt8**, length : Long) : X509
fun x509_dup = X509_dup(a : X509) : X509
fun x509_free = X509_free(a : X509)
fun x509_get_subject_name = X509_get_subject_name(a : X509) : X509_NAME
Expand Down Expand Up @@ -352,6 +354,8 @@ lib LibCrypto
fun x509v3_ext_nconf_nid = X509V3_EXT_nconf_nid(conf : Void*, ctx : Void*, ext_nid : Int, value : Char*) : X509_EXTENSION
fun x509v3_ext_print = X509V3_EXT_print(out : Bio*, ext : X509_EXTENSION, flag : Int, indent : Int) : Int

fun x509_store_add_cert = X509_STORE_add_cert(ctx : X509_STORE, x : X509) : Int

{% unless compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 %}
fun err_load_crypto_strings = ERR_load_crypto_strings
fun openssl_add_all_algorithms = OPENSSL_add_all_algorithms_noconf
Expand Down
1 change: 1 addition & 0 deletions src/openssl/lib_ssl.cr
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ lib LibSSL
fun ssl_ctx_get_verify_mode = SSL_CTX_get_verify_mode(ctx : SSLContext) : VerifyMode
fun ssl_ctx_set_verify = SSL_CTX_set_verify(ctx : SSLContext, mode : VerifyMode, callback : VerifyCallback)
fun ssl_ctx_set_default_verify_paths = SSL_CTX_set_default_verify_paths(ctx : SSLContext) : Int
fun ssl_ctx_get_cert_store = SSL_CTX_get_cert_store(ctx : SSLContext) : LibCrypto::X509_STORE
fun ssl_ctx_ctrl = SSL_CTX_ctrl(ctx : SSLContext, cmd : Int, larg : ULong, parg : Void*) : ULong

{% if compare_versions(OPENSSL_VERSION, "3.0.0") >= 0 %}
Expand Down
15 changes: 15 additions & 0 deletions src/openssl/ssl/context.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
require "uri/punycode"
require "log"
{% if flag?(:win32) %}
require "crystal/system/win32/crypto"
{% end %}

# An `SSL::Context` represents a generic secure socket protocol configuration.
#
Expand Down Expand Up @@ -223,6 +226,12 @@ abstract class OpenSSL::SSL::Context
{% end %}

add_modes(OpenSSL::SSL::Modes.flags(AUTO_RETRY, RELEASE_BUFFERS))

# OpenSSL does not support reading from the system root certificate store on
# Windows, so we have to import them ourselves
{% if flag?(:win32) %}
Crystal::System::Crypto.populate_system_root_certificates(self)
{% end %}
end

# Overriding initialize or new in the child classes as public methods,
Expand All @@ -233,6 +242,12 @@ abstract class OpenSSL::SSL::Context
protected def _initialize_insecure(method : LibSSL::SSLMethod)
@handle = LibSSL.ssl_ctx_new(method)
raise OpenSSL::Error.new("SSL_CTX_new") if @handle.null?

# since an insecure context on non-Windows systems still has access to the
# system certificates, we do the same for Windows
{% if flag?(:win32) %}
Crystal::System::Crypto.populate_system_root_certificates(self)
{% end %}
end

protected def self.insecure(method : LibSSL::SSLMethod)
Expand Down
13 changes: 13 additions & 0 deletions src/openssl/x509/certificate.cr
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ module OpenSSL::X509
@cert
end

# Attempts to decode an ASN.1/DER-encoded certificate from *bytes*.
#
# Returns the decoded certificate and the remaining bytes on success.
# Returns `nil` and *bytes* unchanged on failure.
def self.from_der?(bytes : Bytes) : {self?, Bytes}
ptr = bytes.to_unsafe
if x509 = LibCrypto.d2i_X509(nil, pointerof(ptr), bytes.size)
{new(x509), bytes[ptr - bytes.to_unsafe..]}
else
{nil, bytes}
end
end

def subject : X509::Name
subject = LibCrypto.x509_get_subject_name(@cert)
raise Error.new("X509_get_subject_name") if subject.null?
Expand Down