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

get peer certificates and signatures #8005

Merged
merged 5 commits into from
Jun 17, 2021
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
65 changes: 42 additions & 23 deletions spec/std/openssl/ssl/socket_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -41,37 +41,56 @@ describe OpenSSL::SSL::Socket do
end
end
end
end

it "returns the cipher that is currently in use" do
tcp_server = TCPServer.new("127.0.0.1", 0)
server_context, client_context = ssl_context_pair
private alias Server = OpenSSL::SSL::Socket::Server
private alias Client = OpenSSL::SSL::Socket::Client

OpenSSL::SSL::Server.open(tcp_server, server_context) do |server|
spawn do
OpenSSL::SSL::Socket::Client.open(TCPSocket.new(tcp_server.local_address.address, tcp_server.local_address.port), client_context, hostname: "example.com") do |socket|
end
end
private def socket_test(server_tests, client_tests)
tcp_server = TCPServer.new("127.0.0.1", 0)
server_context, client_context = ssl_context_pair

client = server.accept
client.cipher.should_not be_empty
client.close
OpenSSL::SSL::Server.open(tcp_server, server_context) do |server|
spawn do
Client.open(TCPSocket.new(tcp_server.local_address.address, tcp_server.local_address.port), client_context, hostname: "example.com") do |socket|
client_tests.call(socket)
end
end

client = server.accept
server_tests.call(client)
client.close
end
end

it "returns the TLS version" do
tcp_server = TCPServer.new("127.0.0.1", 0)
server_context, client_context = ssl_context_pair
describe OpenSSL::SSL::Socket do
it "returns the cipher that is currently in use" do
socket_test(
server_tests: ->(client : Server) {
client.cipher.should_not be_empty
},
client_tests: ->(client : Client) {}
)
end

OpenSSL::SSL::Server.open(tcp_server, server_context) do |server|
spawn do
OpenSSL::SSL::Socket::Client.open(TCPSocket.new(tcp_server.local_address.address, tcp_server.local_address.port), client_context, hostname: "example.com") do |socket|
end
end
it "returns the TLS version" do
socket_test(
server_tests: ->(client : Server) {
client.tls_version.should contain "TLS"
},
client_tests: ->(client : Client) {}
)
end

client = server.accept
client.tls_version.should contain "TLS"
client.close
end
it "returns the peer certificate" do
socket_test(
server_tests: ->(client : Server) {
client.peer_certificate.should be_nil
},
client_tests: ->(client : Client) {
client.peer_certificate.should_not be_nil
}
)
end

it "accepts clients that only write then close the connection" do
Expand Down
14 changes: 14 additions & 0 deletions spec/std/openssl/x509/certificate_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,18 @@ describe OpenSSL::X509::Certificate do
cert.extensions.map(&.oid).should eq ["subjectAltName", "subjectAltName"]
cert.extensions.map(&.value).should eq ["IP Address:127.0.0.1", "DNS:localhost.localdomain"]
end

it "#signature_algorithm" do
cert = OpenSSL::X509::Certificate.new

expect_raises(Exception, "Could not determine certificate signature algorithm") do
cert.signature_algorithm
end
end

it "#digest" do
cert = OpenSSL::X509::Certificate.new
expect_raises(ArgumentError) { cert.digest("not a real algo") }
cert.digest("SHA256").should_not be_nil
end
end
7 changes: 7 additions & 0 deletions src/openssl/lib_crypto.cr
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ lib LibCrypto
fun obj_obj2nid = OBJ_obj2nid(obj : ASN1_OBJECT) : Int
fun obj_ln2nid = OBJ_ln2nid(ln : Char*) : Int
fun obj_sn2nid = OBJ_sn2nid(sn : Char*) : Int
{% if compare_versions(OPENSSL_VERSION, "1.0.2") >= 0 %}
fun obj_find_sigid_algs = OBJ_find_sigid_algs(sigid : Int32, pdig_nid : Int32*, ppkey_nid : Int32*) : Int32
{% end %}

fun asn1_object_free = ASN1_OBJECT_free(obj : ASN1_OBJECT)
fun asn1_string_data = ASN1_STRING_data(x : ASN1_STRING) : Char*
Expand Down Expand Up @@ -165,6 +168,7 @@ lib LibCrypto
fun hmac_final = HMAC_Final(ctx : HMAC_CTX, md : UInt8*, len : UInt32*) : Int32
fun hmac_ctx_copy = HMAC_CTX_copy(dst : HMAC_CTX, src : HMAC_CTX) : Int32

fun x509_digest = X509_digest(x509 : X509, evp_md : EVP_MD, hash : UInt8*, len : Int32*) : Int32
fun evp_get_digestbyname = EVP_get_digestbyname(name : UInt8*) : EVP_MD
fun evp_digestinit_ex = EVP_DigestInit_ex(ctx : EVP_MD_CTX, type : EVP_MD, engine : Void*) : Int32
fun evp_digestupdate = EVP_DigestUpdate(ctx : EVP_MD_CTX, data : UInt8*, count : LibC::SizeT) : Int32
Expand Down Expand Up @@ -282,6 +286,9 @@ lib LibCrypto
fun x509_get_ext = X509_get_ext(x : X509, idx : Int) : X509_EXTENSION
fun x509_get_ext_count = X509_get_ext_count(x : X509) : Int
fun x509_get_ext_d2i = X509_get_ext_d2i(x : X509, nid : Int, crit : Int*, idx : Int*) : Void*
{% if compare_versions(OPENSSL_VERSION, "1.0.2") >= 0 %}
fun x509_get_signature_nid = X509_get_signature_nid(x509 : X509) : Int32
{% end %}

MBSTRING_UTF8 = 0x1000

Expand Down
2 changes: 2 additions & 0 deletions src/openssl/lib_ssl.cr
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ lib LibSSL
fun ssl_ctx_set_default_verify_paths = SSL_CTX_set_default_verify_paths(ctx : SSLContext) : Int
fun ssl_ctx_ctrl = SSL_CTX_ctrl(ctx : SSLContext, cmd : Int, larg : ULong, parg : Void*) : ULong

fun ssl_get_peer_certificate = SSL_get_peer_certificate(handle : SSL) : LibCrypto::X509

{% if compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 %}
fun ssl_ctx_get_options = SSL_CTX_get_options(ctx : SSLContext) : ULong
fun ssl_ctx_set_options = SSL_CTX_set_options(ctx : SSLContext, larg : ULong) : ULong
Expand Down
18 changes: 18 additions & 0 deletions src/openssl/ssl/socket.cr
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ abstract class OpenSSL::SSL::Socket < IO
socket.close
end
end

# Returns the `OpenSSL::X509::Certificate` the peer presented.
def peer_certificate : OpenSSL::X509::Certificate
super.not_nil!
end
end

class Server < Socket
Expand Down Expand Up @@ -257,4 +262,17 @@ abstract class OpenSSL::SSL::Socket < IO
raise NotImplementedError.new("#{io.class}#write_timeout=")
end
end

# Returns the `OpenSSL::X509::Certificate` the peer presented, if a
# connection was esablished.
#
# NOTE: Due to the protocol definition, a TLS/SSL server will always send a
# certificate, if present. A client will only send a certificate when
# explicitly requested to do so by the server (see `SSL_CTX_set_verify(3)`). If
# an anonymous cipher is used, no certificates are sent. That a certificate
# is returned does not indicate information about the verification state.
def peer_certificate : OpenSSL::X509::Certificate?
cert = LibSSL.ssl_get_peer_certificate(@ssl)
OpenSSL::X509::Certificate.new cert if cert
end
end
31 changes: 31 additions & 0 deletions src/openssl/x509/certificate.cr
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,36 @@ module OpenSSL::X509
raise Error.new("X509_add_ext") if ret.null?
extension
end

# Returns the name of the signature algorithm.
def signature_algorithm : String
{% if compare_versions(LibSSL::OPENSSL_VERSION, "1.0.2") >= 0 %}
sigid = LibCrypto.x509_get_signature_nid(@cert)
result = LibCrypto.obj_find_sigid_algs(sigid, out algo_nid, nil)
raise "Could not determine certificate signature algorithm" if result == 0

sn = LibCrypto.obj_nid2sn(algo_nid)
raise "Unknown algo NID #{algo_nid.inspect}" if sn.null?
String.new sn
{% else %}
raise "Missing OpenSSL function for certificate signature algorithm (requires OpenSSL 1.0.2)"
{% end %}
end

# Returns the digest of the certificate using *algorithm_name*
#
# ```
# cert.digest("SHA1").hexstring # => "6f608752059150c9b3450a9fe0a0716b4f3fa0ca"
# cert.digest("SHA256").hexstring # => "51d80c865cc717f181cd949f0b23b5e1e82c93e01db53f0836443ec908b83748"
# ```
def digest(algorithm_name : String) : Bytes
algo_type = LibCrypto.evp_get_digestbyname algorithm_name
raise ArgumentError.new "Could not find digest for #{algorithm_name.inspect}" if algo_type.null?
hash = Bytes.new(64) # EVP_MAX_MD_SIZE for SHA512
result = LibCrypto.x509_digest(@cert, algo_type, hash, out size)
raise "Could not generate certificate hash" unless result == 1

hash[0, size]
end
end
end