From be6b65b9d914f7a2e9e53aad73a2c4f72a44cfca Mon Sep 17 00:00:00 2001 From: Tristan Zaton <50082122+coma64@users.noreply.github.com> Date: Fri, 14 Jun 2024 10:58:04 +0200 Subject: [PATCH] Add bindings for DANE and handshake tracing. --- .gitignore | 3 + CHANGELOG.md | 6 ++ README.md | 1 + cert.go | 24 +++++++ cert_test.go | 24 +++++++ ctx.go | 32 ++++++++++ digest.go | 3 +- shim.c | 10 +++ shim.h | 2 + ssl.go | 91 +++++++++++++++++++++++++++ ssl_test.go | 175 ++++++++++++++++++++++++++++++++++++++++++++++++++- 11 files changed, 369 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 805d350b..ce68b846 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ openssl.test +/.ccls-cache/ +/.ccls +/.idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e35e6f69..222f8f89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. ### Added +- Bindings for [DANE](https://docs.openssl.org/1.1.1/man3/SSL_CTX_dane_enable/). +- Bindings for [TLS handshake tracing](https://docs.openssl.org/master/man3/SSL_CTX_set_msg_callback/). +- Bindings for `X509_digest()`. +- Bindings for `X509_verify_cert_error_string()`. +- Bindings for `SSL_get_version()`. + ### Changed ### Fixed diff --git a/README.md b/README.md index 7b9b6ad0..53334f3a 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Forked from https://github.com/libp2p/openssl (unmaintained) to add: 2. Fix build on Apple M1. 3. Fix static build. 4. Fix error extraction on key reading. +5. Bindings for DANE. ### License diff --git a/cert.go b/cert.go index 97c788f7..294e450c 100644 --- a/cert.go +++ b/cert.go @@ -430,3 +430,27 @@ func (c *Certificate) GetExtensionValue(nid NID) []byte { val := C.get_extention(c.x, C.int(nid), &dataLength) return C.GoBytes(unsafe.Pointer(val), dataLength) } + +// Hash uses the given digest to generate a hash of the certificate. Use GetDigestByName +// to get a digest. +func (c *Certificate) Hash(digest *Digest) []byte { + var hashLength C.uint + hash := make([]byte, C.EVP_MAX_MD_SIZE) + + C.X509_digest(c.x, digest.ptr, (*C.uchar)(unsafe.Pointer(&hash[0])), &hashLength) + + return hash[:hashLength] +} + +// VerifyCertErrorString returns a human-readable error string for the given verification error. +// https://www.openssl.org/docs/man3.1/man3/X509_verify_cert_error_string.html +func VerifyCertErrorString(result VerifyResult) string { + // Locking the thread because the docs say: + // If an unrecognised error code is passed to X509_verify_cert_error_string() the + // numerical value of the unknown code is returned in a static buffer. This is not + // thread safe but will never happen unless an invalid code is passed. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + return C.GoString(C.X509_verify_cert_error_string(C.long(result))) +} diff --git a/cert_test.go b/cert_test.go index 45107a0a..49fe58ff 100644 --- a/cert_test.go +++ b/cert_test.go @@ -15,6 +15,7 @@ package openssl import ( + "encoding/hex" "math/big" "testing" "time" @@ -162,3 +163,26 @@ func TestCertVersion(t *testing.T) { t.Fatalf("bad version: %d", vers) } } + +func TestCertHash(t *testing.T) { + cert, err := LoadCertificateFromPEM(certBytes) + if err != nil { + t.Fatal(err) + } + + digest, err := GetDigestByName("SHA256") + if err != nil { + t.Fatal(err) + } + + if hash := hex.EncodeToString(cert.Hash(digest)); hash != certHashHex { + t.Fatalf("Wrong hash returned, expected %q, got %q", certHashHex, hash) + } +} + +func TestVerifyCertErrorString(t *testing.T) { + expected := "unable to get issuer certificate" + if result := VerifyCertErrorString(UnableToGetIssuerCert); result != expected { + t.Fatalf("Wrong cert error string returned, expected %q, got %q", expected, result) + } +} diff --git a/ctx.go b/ctx.go index 3bebf0d5..e8fd6b50 100644 --- a/ctx.go +++ b/ctx.go @@ -616,3 +616,35 @@ func (c *Ctx) SessSetCacheSize(t int) int { func (c *Ctx) SessGetCacheSize() int { return int(C.X_SSL_CTX_sess_get_cache_size(c.ctx)) } + +// DaneEnable initializes shared state required for DANE support. Must be +// called before any other Dane function. +// https://www.openssl.org/docs/man1.1.1/man3/SSL_dane_clear_flags.html +func (c *Ctx) DaneEnable() error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if C.SSL_CTX_dane_enable(c.ctx) <= 0 { + return errorFromErrorQueue() + } + + return nil +} + +type DaneFlags int + +const ( + DaneFlagNoDaneEeNamechecks DaneFlags = C.DANE_FLAG_NO_DANE_EE_NAMECHECKS +) + +// DaneSetFlags enables the default flags of every connection associated +// with this context. See DaneFlag for available flags. +// https://www.openssl.org/docs/man1.1.1/man3/SSL_dane_clear_flags.html +func (c *Ctx) DaneSetFlags(flags DaneFlags) DaneFlags { + return DaneFlags(C.SSL_CTX_dane_set_flags(c.ctx, C.ulong(flags))) +} + +// DaneClearFlags disables flags set by DaneSetFlags. +func (c *Ctx) DaneClearFlags(flags DaneFlags) DaneFlags { + return DaneFlags(C.SSL_CTX_dane_clear_flags(c.ctx, C.ulong(flags))) +} diff --git a/digest.go b/digest.go index 6d8d2635..66190811 100644 --- a/digest.go +++ b/digest.go @@ -28,7 +28,8 @@ type Digest struct { } // GetDigestByName returns the Digest with the name or nil and an error if the -// digest was not found. +// digest was not found. Use `openssl list -digest-algorithms` to list available +// digest names. func GetDigestByName(name string) (*Digest, error) { cname := C.CString(name) defer C.free(unsafe.Pointer(cname)) diff --git a/shim.c b/shim.c index ae951029..8d499acc 100644 --- a/shim.c +++ b/shim.c @@ -439,6 +439,16 @@ int X_SSL_verify_cb(int ok, X509_STORE_CTX* store) { return go_ssl_verify_cb_thunk(p, ok, store); } +void X_SSL_toggle_tracing(SSL* ssl, FILE* output, short enable) { + if (enable) { + SSL_set_msg_callback(ssl, SSL_trace); + SSL_set_msg_callback_arg(ssl, BIO_new_fp(output, BIO_NOCLOSE)); + } else { + SSL_set_msg_callback(ssl, NULL); + SSL_set_msg_callback_arg(ssl, NULL); + } +} + const SSL_METHOD *X_SSLv23_method() { return SSLv23_method(); } diff --git a/shim.h b/shim.h index fbd2b26b..b04c742f 100644 --- a/shim.h +++ b/shim.h @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -53,6 +54,7 @@ extern long X_SSL_set_tlsext_host_name(SSL *ssl, const char *name); extern const char * X_SSL_get_cipher_name(const SSL *ssl); extern int X_SSL_session_reused(SSL *ssl); extern int X_SSL_new_index(); +extern void X_SSL_toggle_tracing(SSL* ssl, FILE* output, short enable); extern const SSL_METHOD *X_SSLv23_method(); extern const SSL_METHOD *X_SSLv3_method(); diff --git a/ssl.go b/ssl.go index b187d15d..e7f6b423 100644 --- a/ssl.go +++ b/ssl.go @@ -19,6 +19,7 @@ import "C" import ( "os" + "runtime" "unsafe" "github.com/mattn/go-pointer" @@ -92,6 +93,23 @@ func (s *SSL) ClearOptions(options Options) Options { return Options(C.X_SSL_clear_options(s.ssl, C.long(options))) } +// EnableTracing enables TLS handshake tracing using openssls +// SSL_trace function. If useStderr is false, stdout is used. +// https://www.openssl.org/docs/manmaster/man3/SSL_trace.html +func (s *SSL) EnableTracing(useStderr bool) { + output := C.stdout + if useStderr { + output = C.stderr + } + + C.X_SSL_toggle_tracing(s.ssl, output, 1) +} + +// DisableTracing unsets the msg callback from EnableTracing. +func (s *SSL) DisableTracing() { + C.X_SSL_toggle_tracing(s.ssl, nil, 0) +} + // SetVerify controls peer verification settings. See // http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html func (s *SSL) SetVerify(options VerifyOptions, verify_cb VerifyCallback) { @@ -152,6 +170,79 @@ func (s *SSL) SetSSLCtx(ctx *Ctx) { C.SSL_set_SSL_CTX(s.ssl, ctx.ctx) } +// GetVersion() returns the name of the protocol used for the connection. It +// should only be called after the initial handshake has been completed otherwise +// the result may be unreliable. +// https://www.openssl.org/docs/man1.0.2/man3/SSL_get_version.html +func (s *SSL) GetVersion() string { + return C.GoString(C.SSL_get_version(s.ssl)) +} + +// DaneEnable enables DANE validation for this connection. It must be called +// before the TLS handshake. +// https://www.openssl.org/docs/man1.1.1/man3/SSL_dane_clear_flags.html +func (s *SSL) DaneEnable(tlsaBaseDomain string) error { + tlsaBaseDomainCString := C.CString(tlsaBaseDomain) + defer C.free(unsafe.Pointer(tlsaBaseDomainCString)) + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if C.SSL_dane_enable(s.ssl, tlsaBaseDomainCString) <= 0 { + return errorFromErrorQueue() + } + + return nil +} + +// DaneTlsaAdd loads a TLSA record that will be validated against the presented certificate. +// Data must be in wire form, not hex ASCII. If all TLSA records you try to add are unusable +// (bool return value) an opportunistic application must disable peer authentication by +// using a verify mode equal to VerifyNone. +// https://www.openssl.org/docs/man1.1.1/man3/SSL_dane_clear_flags.html +func (s *SSL) DaneTlsaAdd(usage, selector, matchingType byte, data []byte) (bool, error) { + cData := C.CBytes(data) + defer C.free(cData) + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if status := C.SSL_dane_tlsa_add( + s.ssl, + C.uchar(usage), + C.uchar(selector), + C.uchar(matchingType), + (*C.uchar)(cData), + C.size_t(len(data)), + ); status < 0 { + return false, errorFromErrorQueue() + } else if status == 0 { + return false, nil + } + return true, nil +} + +// DaneGet0DaneAuthority returns a value that is negative if DANE verification failed (or +// was not enabled), 0 if an EE TLSA record directly matched the leaf certificate, or a +// positive number indicating the depth at which a TA record matched an issuer certificate. +// However, the depth doesn't refer to the list of certificates as sent by the peer but rather +// how it's returned from SSL_get0_verified_chain. +// https://www.openssl.org/docs/man1.1.1/man3/SSL_dane_clear_flags.html +func (s *SSL) DaneGet0DaneAuthority() int { + return int(C.SSL_get0_dane_authority(s.ssl, nil, nil)) +} + +// DaneSetFlags enables given flags for this connection. Returns previous flags. +// https://www.openssl.org/docs/man1.1.1/man3/SSL_dane_clear_flags.html +func (s *SSL) DaneSetFlags(flags DaneFlags) DaneFlags { + return DaneFlags(C.SSL_dane_set_flags(s.ssl, C.ulong(flags))) +} + +// DaneClearFlags disables flags set by DaneSetFlags. Returns previous flags. +func (s *SSL) DaneClearFlags(flags DaneFlags) DaneFlags { + return DaneFlags(C.SSL_dane_clear_flags(s.ssl, C.ulong(flags))) +} + //export sni_cb_thunk func sni_cb_thunk(p unsafe.Pointer, con *C.SSL, ad unsafe.Pointer, arg unsafe.Pointer) C.int { defer func() { diff --git a/ssl_test.go b/ssl_test.go index 5eb0c514..c7e24939 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -19,6 +19,7 @@ import ( "crypto/rand" "crypto/tls" "encoding/base64" + "encoding/hex" "fmt" "io" "io/ioutil" @@ -53,7 +54,8 @@ tCzcieQbb6KqUyxxzgTelXq2IxJUyU74Jv96BZ8cA7Qvwv1jwsfxYv7VHLuFAmtW KCDFmLjMtdrKX+q5zJe7 -----END CERTIFICATE----- `) - keyBytes = []byte(`-----BEGIN RSA PRIVATE KEY----- + certHashHex = "b67bf8116986404c17877098a2992a30b77d0b6f3b5f531340afa27804955a69" + keyBytes = []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEA3X94nDbxbK5a5zS4vEqHLHKpUmxavqRL5oXEqKoAy6nm56rv C3e9xySe+DBlxIEV/MWU+RYpzjC99QkerfRP493aleqfhn3ZRS3tyKrQtP2z1Zwg wYqwcoASOLgqzKvtVYQMT1nJaw6O5fUEWG7BMR/ZX5/kcr8XjTGYjgEmrL1WTZ3G @@ -935,3 +937,174 @@ func TestOpenSSLLotsOfConnsWithFail(t *testing.T) { }) } } + +func newDefaultServer(t testing.TB, serverConn net.Conn, ctx *Ctx) (*Conn, error) { + ctx.SetVerify(VerifyNone, passThruVerify(t)) + key, err := LoadPrivateKeyFromPEM(keyBytes) + if err != nil { + return nil, err + } + err = ctx.UsePrivateKey(key) + if err != nil { + return nil, err + } + cert, err := LoadCertificateFromPEM(certBytes) + if err != nil { + return nil, err + } + err = ctx.UseCertificate(cert) + if err != nil { + return nil, err + } + err = ctx.SetCipherList("AES128-SHA") + if err != nil { + return nil, err + } + server, err := Server(serverConn, ctx) + if err != nil { + return nil, err + } + + return server, nil + +} + +func doHandshake(t testing.TB, server, client *Conn) { + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + + err := client.Handshake() + if err != nil { + t.Fatal(err) + } + }() + go func() { + defer wg.Done() + + err := server.Handshake() + if err != nil { + t.Fatal(err) + } + }() + wg.Wait() +} + +func TestOpenSSLGetVersion(t *testing.T) { + serverConn, clientConn := NetPipe(t) + + ctx, err := NewCtx() + if err != nil { + t.Fatal(err) + } + + if ok := ctx.SetMinProtoVersion(TLS1_3_VERSION); !ok { + t.Fatal("Failed to set TLS min version") + } + if ok := ctx.SetMaxProtoVersion(TLS1_3_VERSION); !ok { + t.Fatal("Failed to set TLS max version") + } + + client, err := Client(clientConn, ctx) + if err != nil { + t.Fatal(err) + } + + server, err := newDefaultServer(t, serverConn, ctx) + if err != nil { + t.Fatal(err) + } + + doHandshake(t, server, client) + + // We limited the allowed proto version to only tls1.3. + expected := "TLSv1.3" + if version := client.GetVersion(); version != expected { + t.Fatalf("Wrong version returned, expected %s, got %s", expected, version) + } +} + +type tlsa struct { + usage, selector, matchingType byte + tlsaRecord []byte +} + +func TestOpenSSLDaneValidation(t *testing.T) { + certHash, err := hex.DecodeString(certHashHex) + if err != nil { + t.Fatal(err) + } + + matchingTlsa := tlsa{3, 0, 1, certHash} + // Needs to be X bytes long, otherwise it's considered invalid. + notMatchingTlsa := tlsa{3, 0, 1, bytes.Repeat([]byte{69}, len(certHash))} + + cases := []struct { + name string + tlsaRecords []tlsa + shouldSucceed bool + }{ + {"matching TLSA record", []tlsa{matchingTlsa}, true}, + {"no matching TLSA record", []tlsa{notMatchingTlsa}, false}, + {"no TLSA records found", []tlsa{}, false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + serverConn, clientConn := NetPipe(t) + + ctx, err := NewCtx() + if err != nil { + t.Fatal(err) + } + + // This is needed as it initializes shared state in the ssl context. + if err = ctx.DaneEnable(); err != nil { + t.Fatal(err) + } + ctx.DaneSetFlags(DaneFlagNoDaneEeNamechecks) + + client, err := Client(clientConn, ctx) + if err != nil { + t.Fatal(err) + } + + // The basedomain doesn't matter as it's used as SNI and for server name checks - both + // aren't relevant for this test. + if err = client.DaneEnable("foo.bar"); err != nil { + t.Fatal(err) + } + + for i, tlsa := range tc.tlsaRecords { + isUsable, err := client.DaneTlsaAdd( + tlsa.usage, + tlsa.selector, + tlsa.matchingType, + tlsa.tlsaRecord, + ) + if err != nil { + t.Fatal(err) + } else if !isUsable { + t.Fatalf("tlsa record %d is unusable", i) + } + } + + server, err := newDefaultServer(t, serverConn, ctx) + if err != nil { + t.Fatal(err) + } + + doHandshake(t, server, client) + + isSuccess := client.DaneGet0DaneAuthority() >= 0 + if isSuccess != tc.shouldSucceed { + t.Fatalf( + "Wrong validation result returned, expected %t, got %t", + tc.shouldSucceed, + isSuccess, + ) + } + }) + } +}