This commit is contained in:
2026-03-07 01:50:03 +01:00
parent 2ba9474002
commit 0985a85d5b
5 changed files with 569 additions and 0 deletions

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ compiled/
# Dependency tracking files
*.dep
/*.bak

20
info.rkt Normal file
View File

@@ -0,0 +1,20 @@
#lang info
(define pkg-authors '(hnmdijkema))
(define version "0.1.1")
(define license 'Apache-2.0)
(define pkg-desc "Generate self signed certificates based on the standard openssl libraries deployed with racket.")
(define scribblings
'(
("scribblings/self-signed-cert.scrbl" () (library) "racket-self-signed-cert")
)
)
(define deps
'("base"))
(define build-deps
'("racket-doc"
"rackunit-lib"
"scribble-lib"))

6
main.rkt Normal file
View File

@@ -0,0 +1,6 @@
#lang racket/base
(require "private/self-signed-cert.rkt")
(provide (all-from-out "private/self-signed-cert.rkt"))

View File

@@ -0,0 +1,339 @@
#lang racket/base
(require ffi/unsafe
ffi/unsafe/define
racket/match
openssl
openssl/libssl
(prefix-in c: racket/contract)
racket/string
)
(provide generate-self-signed-cert
self-signed-cert
self-signed-cert?
private-key
certificate
x509-cert
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Exported struct
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-struct self-signed-cert
(private-key certificate))
(c:define/contract (private-key ssc)
(c:-> self-signed-cert? string?)
(self-signed-cert-private-key ssc))
(c:define/contract (certificate ssc)
(c:-> self-signed-cert? string?)
(self-signed-cert-certificate ssc))
(define x509-cert certificate)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Supportive macros / functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-syntax version-ffi-define
(syntax-rules (openssl-major libopenssl libssl)
((_ version
(name definition) ...)
(begin
(define name #f)
...
(cond
([= openssl-major version]
(with-handlers
([exn:fail?
(λ (exn) (set! name (get-ffi-obj
(symbol->string 'name) libssl definition)))])
(set! name (get-ffi-obj (symbol->string 'name) libopenssl definition)))
...))
)
)
)
)
(define-syntax version-define
(syntax-rules (openssl-major)
((_ version
(name definition)
...)
(begin
(define name #f)
...
(cond
([= openssl-major version]
(set! name definition)
...
))
)
)
)
)
(define (is-ip? h)
(if (string? h)
(let ((re #px"^[0-9]+[.][0-9]+[.][0-9]+[.][0-9]+$"))
(not (eq? (regexp-match re (string-trim h)) #f)))
#f))
(define (is-dns? h)
(if (string? h)
(let ((re #px"[^ .]+([.][^ .]+)*"))
(not (eq? (regexp-match re (string-trim h)) #f)))
#f))
(define (make-alt-entry host)
(if (is-ip? host)
(format "IP:~a" host)
(format "DNS:~a" host)))
(define (list-of-hosts? h)
(letrec ((f (λ (l)
(if (null? l)
#t
(and (or (is-ip? (car l)) (is-dns? (car l)))
(f (cdr l)))))))
(if (list? h)
(if (null? h)
#f
(f h))
#f)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FFI Stuff needed
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define os (system-type 'os*))
(define libopenssl (cond
([eq? os 'windows] (ffi-lib "libeay32.dll"))
(else (error (format "Cannot load openssl library on platform ~a" os)))
))
(define-ffi-definer define-ssl libopenssl)
(define _EVP_PKEY-pointer (_cpointer/null 'EVP_PKEY*))
(define _RSA-pointer (_cpointer/null 'RSA*))
(define _X509-pointer (_cpointer/null 'X509*))
(define _ASN1_INTEGER-pointer (_cpointer/null 'ASN1_INTEGER*))
(define _ASN1_STRING-pointer (_cpointer/null 'ASN1_STRING*))
(define _ASN1_TIME-pointer (_cpointer/null 'ASN1_TIME*))
(define _X509_NAME-pointer (_cpointer/null 'X509_NAME*))
(define _EVP_MD-pointer (_cpointer/null 'EVP_MD*))
(define _BIO_METHOD-pointer (_cpointer/null 'BIO_METHOD*))
(define _BIO-pointer (_cpointer/null 'BIO*))
(define _EVP_CIPHER-pointer (_cpointer/null 'EVP_CYPHER*))
(define _X509_EXTENSION-pointer (_cpointer/null 'X509_EXTENSION*))
(define RSA_F4 #x10001)
(define MBSTRING_FLAG #x1000)
(define MBSTRING_UTF8 MBSTRING_FLAG)
(define MBSTRING_ASC (+ MBSTRING_FLAG 1))
(define BIO_CTRL_INFO 3)
(define NID_rsaEncryption 6)
(define EVP_PKEY_RSA NID_rsaEncryption)
(define V_ASN1_OCTET_STRING 4)
(define NID_subject_alt_name 85)
(define _string/utf-8-pointer (_ptr o _string/utf-8))
;typedef int pem_password_cb(char *buf, int size, int rwflag, void *userdata)
(define _pem_password_cb
(_fun _string/utf-8 _int _int _pointer -> _int))
(define _gen_rsa_cb
(_fun _int _int _pointer -> _void))
;; Check which openssl version we're dealing with
(define openssl-major #f)
(with-handlers ([exn:fail? (λ (exn) (set! openssl-major 1))])
(define-ssl EVP_RSA_gen
(_fun _int -> _EVP_PKEY-pointer))
(set! openssl-major 3))
(version-ffi-define 1
(EVP_PKEY_new (_fun -> _EVP_PKEY-pointer))
(EVP_PKEY_free (_fun _EVP_PKEY-pointer -> _void))
(RSA_generate_key (_fun _int _int _gen_rsa_cb _pointer -> _RSA-pointer))
(EVP_PKEY_assign (_fun _EVP_PKEY-pointer _int _RSA-pointer -> _int))
(EVP_sha1 (_fun -> _EVP_MD-pointer))
(X509_new (_fun -> _X509-pointer))
(X509_free (_fun _X509-pointer -> _void))
(X509_get_serialNumber (_fun _X509-pointer -> _ASN1_INTEGER-pointer))
(X509_get0_notBefore (_fun _X509-pointer -> _ASN1_TIME-pointer))
(X509_get0_notAfter (_fun _X509-pointer -> _ASN1_TIME-pointer))
(X509_gmtime_adj (_fun _ASN1_TIME-pointer _long -> _ASN1_TIME-pointer))
(X509_set_pubkey (_fun _X509-pointer _EVP_PKEY-pointer -> _int))
(X509_get_subject_name (_fun _X509-pointer -> _X509_NAME-pointer))
(X509_NAME_add_entry_by_txt (_fun _X509_NAME-pointer _string/utf-8 _int _string/utf-8 _int _int _int -> _int))
(X509_set_issuer_name (_fun _X509-pointer _X509_NAME-pointer -> _int))
(X509_sign (_fun _X509-pointer _EVP_PKEY-pointer _EVP_MD-pointer -> _int))
(X509_EXTENSION_create_by_NID (_fun _pointer ; could also be, (ep : (_ptr o _X509_EXTENSION-pointer)), but works fine when #f is provided
_int _int _ASN1_STRING-pointer -> (p : _X509_EXTENSION-pointer)
-> p))
(X509_add_ext (_fun _X509-pointer _X509_EXTENSION-pointer _int -> _int))
(X509_EXTENSION_free (_fun _X509_EXTENSION-pointer -> _void))
(ASN1_INTEGER_set (_fun _ASN1_INTEGER-pointer _long -> _int))
(ASN1_STRING_new (_fun -> _ASN1_STRING-pointer))
(ASN1_STRING_free (_fun _ASN1_STRING-pointer -> _void))
(ASN1_STRING_type_new (_fun _int -> _ASN1_STRING-pointer))
(ASN1_OCTET_STRING_set (_fun _ASN1_STRING-pointer _string/utf-8 _int -> _int))
(BIO_s_mem (_fun -> _BIO_METHOD-pointer))
(BIO_ctrl (_fun _BIO-pointer _int _long
(out : (_ptr o _bytes)) -> (len : _long) -> (list len out)))
(PEM_write_bio_PrivateKey (_fun _BIO-pointer _EVP_PKEY-pointer _EVP_CIPHER-pointer
_string/utf-8 _int _pem_password_cb _pointer -> _int))
(PEM_write_bio_X509 (_fun _BIO-pointer _X509-pointer -> _int))
(BIO_new (_fun _BIO_METHOD-pointer -> _BIO-pointer))
(BIO_puts (_fun _BIO-pointer _string/utf-8 -> _int))
(BIO_free (_fun _BIO-pointer -> _int))
)
(version-define 1
(BIO_get_mem_data (λ (bio-ptr)
(let ((r (BIO_ctrl bio-ptr BIO_CTRL_INFO 0)))
(cadr r))))
(ASN1_OCTET_STRING_new (λ ()
(ASN1_STRING_type_new V_ASN1_OCTET_STRING)))
)
(cond
((= openssl-major 3)
(error "OpenSSL major version 3 is not supported yet")))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Provided function
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(version-define 1
(generate-key
(λ (bits)
(let ((pkey (EVP_PKEY_new)))
(when (eq? pkey #f)
(error "Unable to create EVP_PKEY structure"))
(let* ((rsa (RSA_generate_key bits RSA_F4 #f #f)))
(when (= (EVP_PKEY_assign pkey EVP_PKEY_RSA rsa) 0)
(EVP_PKEY_free pkey)
(error "Unable to generate RSA key"))
pkey))))
(generate-x509
(λ (pkey duration-in-days country company hosts)
(let ((x509 (X509_new)))
(when (eq? x509 #f)
(error "Unable to create X509 structure"))
(ASN1_INTEGER_set (X509_get_serialNumber x509) 1)
(X509_gmtime_adj (X509_get0_notBefore x509) 0)
(X509_gmtime_adj (X509_get0_notAfter x509) (* duration-in-days 24 3600))
(X509_set_pubkey x509 pkey)
(let* ((x509-name (X509_get_subject_name x509))
(first-host (car hosts)))
(X509_NAME_add_entry_by_txt x509-name
"C" MBSTRING_UTF8 country -1 -1 0)
(X509_NAME_add_entry_by_txt x509-name
"O" MBSTRING_UTF8 company -1 -1 0)
(X509_NAME_add_entry_by_txt x509-name
"CN" MBSTRING_UTF8 first-host -1 -1 0)
(X509_set_issuer_name x509 x509-name)
(let* ((alt-name (string-join
(map make-alt-entry hosts) ", "))
(ext-san #f)
(subj-alt-name-asn1 #f)
)
(set! subj-alt-name-asn1 (ASN1_OCTET_STRING_new))
(when (eq? subj-alt-name-asn1 #f)
(error "Cannot allocate Subject Alt Name ASN1 string"))
(ASN1_OCTET_STRING_set subj-alt-name-asn1
alt-name (string-length alt-name))
(let ((r (X509_EXTENSION_create_by_NID #f NID_subject_alt_name 0 subj-alt-name-asn1)))
(displayln r)
(when (eq? r #f)
(error "Cannot allocate X509 Extenstion for Subject Alt Name"))
(let* ((extension_san r)
(re (X509_add_ext x509 extension_san -1)))
(when (= re 0)
(error "Cannot add extension to X509"))
(X509_EXTENSION_free extension_san)))
(ASN1_STRING_free subj-alt-name-asn1)
)
)
(when (= (X509_sign x509 pkey (EVP_sha1)) 0)
(X509_free x509)
(error "Error signing certificate"))
x509)))
(generate-self-signed-cert*
(λ (bits duration-in-days hosts country company)
(let* ((pkey (generate-key bits))
(x509 (generate-x509 pkey duration-in-days country company hosts))
(pkey-data #f)
(x509-data #f)
)
(let ((bio (BIO_new (BIO_s_mem))))
(let ((r (PEM_write_bio_PrivateKey bio pkey #f #f 0 #f #f)))
(when (= r 0)
(BIO_free bio)
(error "Unable to write private key to memory"))
(let ((data (BIO_get_mem_data bio)))
(set! pkey-data data))
(BIO_free bio)))
(let ((bio (BIO_new (BIO_s_mem))))
(let ((r (PEM_write_bio_X509 bio x509)))
(when (= r 0)
(BIO_free bio)
(error "Unable to write X.509 certificate to memory"))
(let ((data (BIO_get_mem_data bio)))
(set! x509-data data))
(BIO_free bio)))
(EVP_PKEY_free pkey)
(X509_free x509)
(make-self-signed-cert (bytes->string/utf-8 pkey-data)
(bytes->string/utf-8 x509-data))
)
)
)
)
(c:define/contract (generate-self-signed-cert bits duration-in-days hosts country company)
(c:-> integer? integer? (c:or/c is-ip? is-dns? list-of-hosts?) string? string?
self-signed-cert?)
(if (eq? generate-self-signed-cert* #f)
(error "No openssl FFI glue code available")
(let ((h (if (list-of-hosts? hosts) hosts (list hosts))))
(generate-self-signed-cert* bits duration-in-days h country company)
)
)
)

View File

@@ -0,0 +1,203 @@
#lang scribble/manual
@(require scribble/example
(for-label racket/base
racket/contract
openssl))
@title{Self-Signed Certificate Utilities}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[racket-self-signed-cert]
This module provides utilities for generating a self-signed X.509 certificate
together with a corresponding private key.
The implementation uses the @racketmodname[openssl] bindings that are
distributed with Racket. In other words, the module relies on the
OpenSSL library that ships with Racket and accesses it via Rackets
FFI interface.
The generated certificate and key are returned in PEM format and can
be used directly with Racket networking libraries such as
@racketmodname[openssl] or TLS-enabled servers.
@section{OpenSSL Integration}
The module dynamically integrates with the OpenSSL library that is
present in the running Racket installation.
During initialization the module performs the following steps:
@itemlist[
@item{
It detects the major version of the OpenSSL library available through
Rackets @racketmodname[openssl] bindings.
}
@item{
If OpenSSL version 3 is detected, the module raises an error because
the required FFI bindings currently support only the OpenSSL 1.x API.
}
@item{
The module determines which native OpenSSL library must be loaded for
FFI access. This allows the implementation to bind directly to the
required cryptographic primitives.
}
@item{
Platform-specific loading of the native OpenSSL library is performed
at runtime.
}
]
The implementation has been tested on the following platforms:
@itemlist[
@item{Windows}
@item{Linux}
]
Other platforms may work provided that a compatible OpenSSL library is
available through Racket.
@section{Data Structures}
@defstruct[self-signed-cert ([private-key string?]
[certificate string?])]{
Represents a generated self-signed certificate together with its
private key.
Both fields contain PEM encoded text.
@itemlist[
@item{@racket[private-key] — the RSA private key in PEM format.}
@item{@racket[certificate] — the X.509 certificate in PEM format.}
]
Instances of this structure are returned by
@racket[generate-self-signed-cert].
}
@defproc[(self-signed-cert? [v any/c]) boolean?]{
Returns @racket[#t] if @racket[v] is a
@racket[self-signed-cert] structure.
}
@section{Accessors}
@defproc[(private-key [ssc self-signed-cert?]) string?]{
Returns the private key stored in @racket[ssc].
The value is a PEM encoded RSA private key suitable for use with
TLS libraries or for writing to disk.
}
@defproc[(certificate [ssc self-signed-cert?]) string?]{
Returns the X.509 certificate stored in @racket[ssc].
The value is a PEM encoded certificate.
}
@defthing[x509-cert (-> self-signed-cert? string?)]{
Alias for @racket[certificate].
This name is provided for situations where the API user prefers the
term “X.509 certificate”.
}
@section{Certificate Generation}
@defproc[(generate-self-signed-cert
[bits integer?]
[duration-in-days integer?]
[hosts (or/c is-ip? is-dns? list-of-hosts?)]
[country string?]
[company string?])
self-signed-cert?]{
Generates a new self-signed RSA certificate and private key.
The implementation uses the OpenSSL functionality provided through
Rackets @racketmodname[openssl] library.
@subsection{Arguments}
@itemlist[
@item{@racket[bits] — size of the RSA key in bits (for example
@racket[2048] or @racket[4096]).}
@item{@racket[duration-in-days] — number of days for which the
certificate remains valid.}
@item{@racket[hosts] — a host name, IP address, or a list of such
values. These values are written into the certificates
@italic{Subject Alternative Name} extension.}
@item{@racket[country] — value for the certificate subjects
@tt{C} (country) attribute.}
@item{@racket[company] — value for the certificate subjects
@tt{O} (organization) attribute.}
]
The first host in the list is used as the certificates
Common Name (CN).
@subsection{Result}
Returns a @racket[self-signed-cert] structure containing:
@itemlist[
@item{the private RSA key}
@item{the corresponding self-signed X.509 certificate}
]
Both values are returned as PEM encoded strings.
@subsection{Example}
@#reader scribble/comment-reader
[racketblock
(define cert
(generate-self-signed-cert
2048
365
'("localhost" "127.0.0.1")
"NL"
"Example Company"))
(private-key cert)
(certificate cert)
]
The returned values can be written to files or supplied directly
to TLS-enabled servers.
}
@section{Notes}
@itemlist[
@item{
This module relies on the OpenSSL library distributed with Racket and
accessed through the @racketmodname[openssl] package.
}
@item{
Certificates are generated entirely in memory and returned as PEM
strings.
}
@item{
The Subject Alternative Name (SAN) extension is automatically populated
from the provided host names and IP addresses.
}
]