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

Adding some X509 tooling - draft for CI #89

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
25 changes: 25 additions & 0 deletions assets/ponylang.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIESTCCAzGgAwIBAgISAyiN9Tg8FyZYAt5ifETG04U1MA0GCSqGSIb3DQEBCwUA
MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
EwJSMzAeFw0yMzA4MTUwMDMwMjdaFw0yMzExMTMwMDMwMjZaMBYxFDASBgNVBAMT
C3BvbnlsYW5nLmlvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEznT8wpdDL2ry
ck92h8gKHAzw+RsqmnmFy+UmMjpNfEreps/Brw0kJy4u6qTD2y7Mo5Z27lm6yLmr
Q1oH2zSZ46OCAj4wggI6MA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEF
BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUtU2t7EpXuiF3
WwoJiYyRUdjzNgswHwYDVR0jBBgwFoAUFC6zF7dYVsuuUAlA5h+vnYsUwsYwVQYI
KwYBBQUHAQEESTBHMCEGCCsGAQUFBzABhhVodHRwOi8vcjMuby5sZW5jci5vcmcw
IgYIKwYBBQUHMAKGFmh0dHA6Ly9yMy5pLmxlbmNyLm9yZy8wRwYDVR0RBEAwPoIL
cG9ueWxhbmcuaW+CDHBvbnlsYW5nLm9yZ4IPd3d3LnBvbnlsYW5nLmlvghB3d3cu
cG9ueWxhbmcub3JnMBMGA1UdIAQMMAowCAYGZ4EMAQIBMIIBBAYKKwYBBAHWeQIE
AgSB9QSB8gDwAHYAtz77JN+cTbp18jnFulj0bF38Qs96nzXEnh0JgSXttJkAAAGJ
9tDSVwAABAMARzBFAiA9JskK//tV8DKBjGG4r82v7RP7be7nZaWOGZCT7DFVhwIh
AO/PZjQsLfrKn3I8cWkaud7GawC+BPepg+b4noISjMIrAHYAejKMVNi3LbYg6jjg
Uh7phBZwMhOFTTvSK8E6V6NS61IAAAGJ9tDSaQAABAMARzBFAiB09LtPj5dVRlpZ
bu2aM4fEbmEVetZNRa73EtvyFx6A0AIhAK2a4UTXOceheNUTvVe3EH184/mPJK97
AT3u8yPwkAq5MA0GCSqGSIb3DQEBCwUAA4IBAQCHFrS1D9yhRD+ci2Om6Y/W/++1
OHH+xJ3+rGNC4YtTnv1im/SsXeN5TJBDHFtvXIwuwFotsShBUIbUahpUgwG3pTkz
vtIB48MG0r1UJoEfzxVc3Vc0fnAoLezO8mBVJjg50JaiUGSoW0s6pSE113OPC8gM
l1g5o9GwbUImtks/9oqT4nWpNWAjT6tzpC2U2BZ11zVT6CpkEdPClpyclZp/rwbE
nrN/pEiH251LvkER8eCQRXSAW6t+cWjpWjy9g2kcuj99TQbh9ocmaX10/uWJisIg
LtgRjmQzkQC7gWl5QMXSiZhXG31+4MsQGjpQiYFsDZry+7KxYniJ4qcsV7Sn
-----END CERTIFICATE-----
40 changes: 40 additions & 0 deletions net_ssl/_asn1_string.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use @ASN1_TIME_to_tm[I32](asn1str: Pointer[ASN1String] tag, tm: Tm)
use @ASN1_STRING_get0_data[Pointer[U8] ref](asn1s: Pointer[ASN1String] tag)
use @ASN1_STRING_length[I32](asn1s: Pointer[ASN1String] tag)
use @ASN1_STRING_type[I32](asn1s: Pointer[ASN1String] tag)
use @printf[I32](fmt: Pointer[U8] tag, ...)

primitive ASN1String
"""
FIXME
Functions used to do transformations on ASN1Strings.
(Note - this will almost certainly transition to a class when it comes
time to wrap this in an API to be able to create these objects)

NOTE: OpenSSL stores almost ALL data in ASN1Strings and almost all of
the other ASN1 types are typedef'd to this under the hood (hence the
time_to_posix function here)
"""
fun array(asn1str: Pointer[ASN1String] tag): Array[U8] val ? =>
"""
Returns the underlying data as an Array[U8]
"""
if (asn1str.is_null()) then error end
recover val
let len: I32 = @ASN1_STRING_length(asn1str)
let osslptr: Pointer[U8] ref = @ASN1_STRING_get0_data(asn1str)

(Array[U8].from_cpointer(osslptr, len.usize())).clone()
end

fun time_to_posix(asn1str: Pointer[ASN1String] tag): I64 =>
"""
Assuming this is a valid entry for time, will return the unix epoch time.
FIXME -- add check for this
"""
let tm: Tm = Tm
@ASN1_TIME_to_tm(asn1str, tm)
tm.timegm()
// let t: I64 = tm.timegm()
// @printf("%d : %d : %d\n".cstring(), tm.tm_isdst, tm.tm_gmtoff, t)
// t + tm.tm_gmtoff
65 changes: 65 additions & 0 deletions net_ssl/_bio.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use @BIO_new[Pointer[_BIO]](typ: Pointer[U8])
use @BIO_s_mem[Pointer[U8]]()
use @BIO_free[I32](a: Pointer[_BIO] tag)
use @BIO_free_all[None](a: Pointer[_BIO] tag)
use @BIO_write[I32](bio: Pointer[_BIO] tag, buf: Pointer[U8] tag, len: U32)
use @BIO_read[I32](b: Pointer[_BIO] tag, data: Pointer[U8] tag, dlen: U32)
use @BIO_ctrl_pending[USize](bio: Pointer[_BIO] tag)
use @BIO_gets[U32](bio: Pointer[_BIO] tag, buffer: Pointer[U8] tag, size: U32)
use @PEM_write_bio_X509[U32](bio: Pointer[_BIO] tag, cert: Pointer[X509] tag)

primitive _BIO
"""
BIO is OpenSSL's generic datastructure that allows the rest of the library
to write "contiguous" binary data without having to worry about memory
reällocation.

In this API it is mainly used for reading data in and out

// Notate why this isn't used in the SSL setup. (double-free)
"""

fun new_ptr(): Pointer[_BIO] tag =>
@BIO_new(@BIO_s_mem())

fun array(bio: Pointer[_BIO] tag): Array[U8] val =>
"""
Returns the contents of the BIO as an Array[U8] val
/// use "buffered"
"""
recover val
var tarr: Array[U8] ref = Array[U8]
let buffer: Array[U8] ref = Array[U8].init(0, 1024) // Look for the 95% percentile
var len: I32 = 0
while ((len = @BIO_read(bio, buffer.cpointer(), buffer.size().u32())); len > 0) do
buffer.copy_to(tarr, 0, tarr.size(), len.usize())
end
tarr
end

fun write(bio: Pointer[_BIO] tag, data: (Array[U8] val | String val)): Bool =>
"""
Writes the contents of the provided Array[U8] or String to the
BIO
"""
let readdata: I32 = @BIO_write(bio, data.cpointer(), data.size().u32())
(readdata == data.size().i32())

fun string(bio: Pointer[_BIO] tag): String val =>
"""
Returns the contents of the BIO as a String val
"""
String.from_array(array(bio))


fun write_pem_x509(bio: Pointer[_BIO] tag, cert: X509Certificate box): U32 =>
"""
Writes the certificate in pem form into this Bio object
"""
@PEM_write_bio_X509(bio, cert._ptr())


fun free(bio: Pointer[_BIO] tag) =>
@BIO_free_all(bio)


54 changes: 54 additions & 0 deletions net_ssl/_test.pony
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ actor \nodoc\ Main is TestList
test(_TestTCPSSLExpect)
test(_TestTCPSSLMute)
test(_TestTCPSSLUnmute)
test(_TestX509Certificate)
ifdef windows then
test(_TestWindowsLoadRootCertificates)
else
Expand Down Expand Up @@ -684,3 +685,56 @@ primitive \nodoc\ _TestSSLContext
end

(consume ssl_client, consume ssl_server)

// test(_TestX509Certificates)

class \nodoc\ iso _TestX509Certificate is UnitTest
"""
Test loading a certificate and analyze it's metadata.
"""
fun name(): String => "x509/_TestX509Certificate"

fun ref apply(h: TestHelper) =>
try
let auth = FileAuth(h.env.root)
let fp: FilePath = FilePath(auth, "assets/ponylang.pem")
let f: File = File.open(fp)
let pemdata: String val = f.read_string(f.size())

let certificate: X509Certificate val = X509Certificate.from_pem(pemdata)?
h.assert_false(certificate.is_null())
h.assert_eq[String](certificate.get_pem(), pemdata.clone().>remove("\r"))
h.assert_eq[String](certificate.issuer_name()?, "R3")
h.assert_eq[String](certificate.subject_name()?, "ponylang.io")
h.assert_eq[String](certificate.fingerprint_sha1(), "6F6E4D79A77B44D524F8C2FB62E46559B3FF7EB4")
h.assert_eq[String](certificate.fingerprint_sha256(), "35E5ACDA20A72168E05E93813A4B62F91E9E2594A243FE73213F4323BA264BFF")
h.assert_eq[String](certificate.fingerprint_md5(), "62AE902FC8BB8A66856CCC3A1066D663")
h.assert_eq[I64](certificate.not_before_posix(), 1692059427)
h.assert_eq[I64](certificate.not_after_posix(), 1699835426)
h.assert_eq[String](certificate.serial()?, "03:28:8D:F5:38:3C:17:26:58:02:DE:62:7C:44:C6:D3:85:35")
h.assert_false(certificate.is_ca())

ifdef "openssl_1.1.x" or "openssl_3.0.x" then
h.assert_eq[String](certificate.authority_key_id()?, "14:2E:B3:17:B7:58:56:CB:AE:50:09:40:E6:1F:AF:9D:8B:14:C2:C6")
h.assert_eq[String](certificate.key_id()?, "B5:4D:AD:EC:4A:57:BA:21:77:5B:0A:09:89:8C:91:51:D8:F3:36:0B")
else
h.assert_error({(): None ? => certificate.authority_key_id()?})
h.assert_error({(): None ? => certificate.key_id()?})
end

ifdef "openssl_3.0.x" then
h.assert_false(certificate.is_self_signed()?)
else
h.assert_error({(): None ? => certificate.is_self_signed()?})
end

let mcert: X509Certificate iso = certificate.clone()
h.assert_true(true)
h.assert_false(mcert.is_null())
h.assert_eq[String](mcert.get_pem(), pemdata.clone().>remove("\r"))


else
h.fail("Certificate Tests failed")
end

25 changes: 25 additions & 0 deletions net_ssl/_tm.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use @timegm[I64](tm: Tm)

struct Tm
"""
Pony mapping of the C struct tm
"""
var tm_sec: I32 = I32(0) // FundamentalType
var tm_min: I32 = I32(0) // FundamentalType
var tm_hour: I32 = I32(0) // FundamentalType
var tm_mday: I32 = I32(0) // FundamentalType
var tm_mon: I32 = I32(0) // FundamentalType
var tm_year: I32 = I32(0) // FundamentalType
var tm_wday: I32 = I32(0) // FundamentalType
var tm_yday: I32 = I32(0) // FundamentalType
var tm_isdst: I32 = I32(0) // FundamentalType
var tm_gmtoff: I64 = I64(0) // FundamentalType
var tm_zone: Pointer[U8] = Pointer[U8] // PointerType

fun ref timegm(): I64 =>
"""
Mapping to the C function call which takes a Tm struct and returns
UTC UNIX epoch time.
"""
@timegm(this)

20 changes: 20 additions & 0 deletions net_ssl/_x509_name.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use @X509_NAME_get_text_by_NID[I32](x509name: Pointer[_X509Name] tag, nid: I32, buf: Pointer[U8] tag, len: I32)

primitive _X509Name
"""
Functions to convert X509Name to Strings for display
(Will likely become a class in the future to support generating them
"""
fun string(x509name: Pointer[_X509Name] tag): String val ? =>
"""
Converts X509Names into String val
"""
if (x509name.is_null()) then error end
let len: I32 = @X509_NAME_get_text_by_NID(x509name, I32(13), Pointer[U8], I32(0))
if (len == -1) then error end
let str: String ref = recover String(len.usize()) end
@X509_NAME_get_text_by_NID(x509name, I32(13), str.cstring(), len+1)
str.recalc()
if (str.size() != len.usize()) then error end
str.clone()

21 changes: 13 additions & 8 deletions net_ssl/ssl.pony
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ use @SSL_ctrl[ILong](
use @SSL_new[Pointer[_SSL]](ctx: Pointer[_SSLContext] tag)
use @SSL_free[None](ssl: Pointer[_SSL] tag)
use @SSL_set_verify[None](ssl: Pointer[_SSL], mode: I32, cb: Pointer[U8])
use @BIO_s_mem[Pointer[U8]]()
use @BIO_new[Pointer[_BIO]](typ: Pointer[U8])
use @SSL_set_bio[None](ssl: Pointer[_SSL], rbio: Pointer[_BIO] tag, wbio: Pointer[_BIO] tag)
use @SSL_set_accept_state[None](ssl: Pointer[_SSL])
use @SSL_set_connect_state[None](ssl: Pointer[_SSL])
Expand All @@ -19,16 +17,12 @@ use @SSL_get0_alpn_selected[None](ssl: Pointer[_SSL] tag, data: Pointer[Pointer[
use @SSL_pending[I32](ssl: Pointer[_SSL])
use @SSL_read[I32](ssl: Pointer[_SSL], buf: Pointer[U8] tag, len: U32)
use @SSL_write[I32](ssl: Pointer[_SSL], buf: Pointer[U8] tag, len: U32)
use @BIO_read[I32](bio: Pointer[_BIO] tag, buf: Pointer[U8] tag, len: U32)
use @BIO_write[I32](bio: Pointer[_BIO] tag, buf: Pointer[U8] tag, len: U32)
use @SSL_get_error[I32](ssl: Pointer[_SSL], ret: I32)
use @BIO_ctrl_pending[USize](bio: Pointer[_BIO] tag)
use @SSL_has_pending[I32](ssl: Pointer[_SSL]) if "openssl_1.1.x" or "openssl_3.0.x"
use @SSL_get_peer_certificate[Pointer[X509]](ssl: Pointer[_SSL]) if "openssl_1.1.x" or "openssl_0.9.0"
use @SSL_get1_peer_certificate[Pointer[X509]](ssl: Pointer[_SSL]) if "openssl_3.0.x"
use @SSL_get_peer_certificate[Pointer[X509]](ssl: Pointer[_SSL] tag) if "openssl_1.1.x" or "openssl_0.9.0"
use @SSL_get1_peer_certificate[Pointer[X509]](ssl: Pointer[_SSL] tag) if "openssl_3.0.x"

primitive _SSL
primitive _BIO

primitive SSLHandshake
primitive SSLAuthFail
Expand Down Expand Up @@ -92,6 +86,17 @@ class SSL
@SSL_do_handshake(_ssl)
end

fun get_peer_certificate(): X509Certificate val =>
"""
Returns the X509Certificate from this SSL connection
"""
ifdef "openssl_3.0.x" then
return X509Certificate._from_cert_ptr(
@SSL_get1_peer_certificate[Pointer[X509] tag](_ssl)
)
end
recover val X509Certificate end

fun box alpn_selected(): (ALPNProtocolName | None) =>
"""
Get the protocol identifier negotiated via ALPN
Expand Down
1 change: 0 additions & 1 deletion net_ssl/ssl_context.pony
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ use @CertEnumCertificatesInStore[NullablePointer[_CertContext]](cert_store: Poin
use @d2i_X509[Pointer[X509] tag](val_out: Pointer[U8] tag, der_in: Pointer[Pointer[U8]],
length: U32)
use @X509_STORE_add_cert[U32](store: Pointer[U8] tag, x509: Pointer[X509] tag)
use @X509_free[None](x509: Pointer[X509] tag)
use @SSL_CTX_set_cert_store[None](ctx: Pointer[_SSLContext] tag, store: Pointer[U8] tag)
use @X509_STORE_free[None](store: Pointer[U8] tag)
use @CertCloseStore[Bool](store: Pointer[U8] tag, flags: U32) if windows
Expand Down
9 changes: 1 addition & 8 deletions net_ssl/x509.pony
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ use "collections"
use "net"

use @pony_os_ip_string[Pointer[U8]](src: Pointer[U8], len: I32)
use @X509_get_subject_name[Pointer[_X509Name]](cert: Pointer[X509])
use @X509_NAME_get_text_by_NID[I32](name: Pointer[_X509Name], nid: I32,
buf: Pointer[U8] tag, len: I32)
use @X509_get_ext_d2i[Pointer[_GeneralNameStack]](cert: Pointer[X509],
nid: I32, crit: Pointer[U8], idx: Pointer[U8])
use @OPENSSL_sk_pop[Pointer[_GeneralName]](stack: Pointer[_GeneralNameStack])
Expand All @@ -13,16 +10,12 @@ use @sk_pop[Pointer[_GeneralName]](stack: Pointer[_GeneralNameStack])
if "openssl_0.9.0"
use @GENERAL_NAME_get0_value[Pointer[U8] tag](name: Pointer[_GeneralName],
ptype: Pointer[I32])
use @ASN1_STRING_type[I32](value: Pointer[U8] tag)
use @ASN1_STRING_get0_data[Pointer[U8]](value: Pointer[U8] tag)
use @ASN1_STRING_length[I32](value: Pointer[U8] tag)
use @GENERAL_NAME_free[None](name: Pointer[_GeneralName])
use @OPENSSL_sk_free[None](stack: Pointer[_GeneralNameStack])
if "openssl_1.1.x" or "openssl_3.0.x"
use @sk_free[None](stack: Pointer[_GeneralNameStack])
if "openssl_0.9.0"

primitive _X509Name
primitive _GeneralName
primitive _GeneralNameStack

Expand Down Expand Up @@ -95,7 +88,7 @@ primitive X509
while not name.is_null() do
var ptype = I32(0)
let value =
@GENERAL_NAME_get0_value(name, addressof ptype)
@GENERAL_NAME_get0_value[Pointer[ASN1String] tag](name, addressof ptype)

match ptype
| 2 => // GEN_DNS
Expand Down
Loading