diff --git a/internal/ossl/ossl.c b/internal/ossl/ossl.c new file mode 100644 index 0000000..d28f980 --- /dev/null +++ b/internal/ossl/ossl.c @@ -0,0 +1,137 @@ +//go:build unix || windows + +#ifdef _WIN32 +# include +# define dlsym (void*)GetProcAddress +#else +# include // dlsym +#endif +#include // fprintf + +// go_openssl_fips_enabled returns 1 if FIPS mode is enabled, 0 otherwise. +// As a special case, it returns -1 if it cannot determine if FIPS mode is enabled. +// See openssl.FIPS for details about its implementation. +// +// This function is reimplemented here because openssl.FIPS assumes that +// all the OpenSSL bindings are loaded, that is, go_openssl_load_functions has +// already been called. On the other hand, go_openssl_fips_enabled is called from +// openssl.CheckVersion, which is used to check if a given OpenSSL shared library +// exists and is FIPS compliant. That shared library might not be the one that +// was passed to go_openssl_load_functions, or it might not even have been called at all. +// +// It is written in C because it is not possible to directly call C function pointers +// retrieved using dlsym from Go. +int +go_openssl_fips_enabled(void* handle) +{ + // For OpenSSL 1.x. + int (*FIPS_mode)(void); + FIPS_mode = (int (*)(void))dlsym(handle, "FIPS_mode"); + if (FIPS_mode != NULL) + return FIPS_mode(); + + // For OpenSSL 3.x. + int (*EVP_default_properties_is_fips_enabled)(void*) = (int (*)(void*))dlsym(handle, "EVP_default_properties_is_fips_enabled"); + void *(*EVP_MD_fetch)(void*, const char*, const char*) = (void* (*)(void*, const char*, const char*))dlsym(handle, "EVP_MD_fetch"); + void (*EVP_MD_free)(void*) = (void (*)(void*))dlsym(handle, "EVP_MD_free"); + + if (EVP_default_properties_is_fips_enabled == NULL || EVP_MD_fetch == NULL || EVP_MD_free == NULL) { + // Shouldn't happen, but if it does, we can't determine if FIPS mode is enabled. + return -1; + } + + if (EVP_default_properties_is_fips_enabled(NULL) != 1) + return 0; + + void *md = EVP_MD_fetch(NULL, "SHA2-256", NULL); + if (md == NULL) + return 0; + + EVP_MD_free(md); + return 1; +} + +static unsigned long +version_num(void* handle) +{ + unsigned long (*fn)(void); + // OPENSSL_version_num is defined in OpenSSL 1.1.0 and 1.1.1. + fn = (unsigned long (*)(void))dlsym(handle, "OpenSSL_version_num"); + if (fn != NULL) + return fn(); + + // SSLeay is defined in OpenSSL 1.0.2. + fn = (unsigned long (*)(void))dlsym(handle, "SSLeay"); + if (fn != NULL) + return fn(); + + return 0; +} + +int +go_openssl_version_major(void* handle) +{ + unsigned int (*fn)(void); + // OPENSSL_version_major is supported since OpenSSL 3. + fn = (unsigned int (*)(void))dlsym(handle, "OPENSSL_version_major"); + if (fn != NULL) + return (int)fn(); + + // If OPENSSL_version_major is not defined, try with OpenSSL 1 functions. + unsigned long num = version_num(handle); + if (num < 0x10000000L || num >= 0x20000000L) + return -1; + + return 1; +} + +int +go_openssl_version_minor(void* handle) +{ + unsigned int (*fn)(void); + // OPENSSL_version_minor is supported since OpenSSL 3. + fn = (unsigned int (*)(void))dlsym(handle, "OPENSSL_version_minor"); + if (fn != NULL) + return (int)fn(); + + // If OPENSSL_version_minor is not defined, try with OpenSSL 1 functions. + unsigned long num = version_num(handle); + // OpenSSL version number follows this schema: + // MNNFFPPS: major minor fix patch status. + if (num < 0x10000000L || num >= 0x10200000L) + { + // We only support minor version 0 and 1, + // so there is no need to implement an algorithm + // that decodes the version number into individual components. + return -1; + } + + if (num >= 0x10100000L) + return 1; + + return 0; +} + +int +go_openssl_version_patch(void* handle) +{ + unsigned int (*fn)(void); + // OPENSSL_version_patch is supported since OpenSSL 3. + fn = (unsigned int (*)(void))dlsym(handle, "OPENSSL_version_patch"); + if (fn != NULL) + return (int)fn(); + + // If OPENSSL_version_patch is not defined, try with OpenSSL 1 functions. + unsigned long num = version_num(handle); + // OpenSSL version number follows this schema: + // MNNFFPPS: major minor fix patch status. + if (num < 0x10000000L || num >= 0x10200000L) + { + // We only support minor version 0 and 1, + // so there is no need to implement an algorithm + // that decodes the version number into individual components. + return -1; + } + + return (num >> 12) & 0xff; +} diff --git a/internal/ossl/ossl.go b/internal/ossl/ossl.go index c9f8f12..180a1ac 100644 --- a/internal/ossl/ossl.go +++ b/internal/ossl/ossl.go @@ -1,3 +1,116 @@ package ossl +import ( + "errors" + "strconv" + "strings" + "unsafe" +) + //go:generate go run github.com/golang-fips/openssl/v2/internal/mkcgo -out zossl.go --package ossl --lib crypto --include ossl.h api.h + +var vMajor, vMinor, vPatch int + +func LoadLcrypto(handle unsafe.Pointer) { + mkcgoLoad_crypto(handle) + vMajor = Go_openssl_version_major(handle) + vMinor = Go_openssl_version_minor(handle) + vPatch = Go_openssl_version_minor(handle) +} + +func newError(msg string) error { + var b strings.Builder + b.WriteString(msg) + b.WriteString("\nopenssl error(s):") + for { + var ( + e uint64 + file *byte + line int32 + ) + if vMajor == 1 { + e = ERR_get_error_line(&file, &line) + } else { + e = ERR_get_error_all(&file, &line, nil, nil, nil) + } + if e == 0 { + break + } + b.WriteByte('\n') + var buf [256]byte + ERR_error_string_n(e, &buf[0], len(buf)) + b.WriteString(string(buf[:]) + "\n\t" + goString(file) + ":" + strconv.Itoa(int(line))) + } + return errors.New(b.String()) +} + +// goString converts a C null-terminated string to a Go string. +func goString(p *byte) string { + if p == nil { + return "" + } + end := unsafe.Pointer(p) + n := 0 + for *(*byte)(end) != 0 { + end = unsafe.Pointer(uintptr(end) + unsafe.Sizeof(*p)) + n++ + } + return string(unsafe.Slice(p, n)) +} + +func OpenSSL_version(typ int32) *byte { + if vMajor == 1 && vMinor == 0 { + return _SSLeay_version(typ) + } + return _OpenSSL_version(typ) +} + +func EVP_MD_CTX_free(ctx EVP_MD_CTX_PTR) { + if vMajor == 1 && vMinor == 0 { + _EVP_MD_CTX_destroy(ctx) + } else { + _EVP_MD_CTX_free(ctx) + } +} + +func EVP_MD_CTX_new() (EVP_MD_CTX_PTR, error) { + if vMajor == 1 && vMinor == 0 { + return _EVP_MD_CTX_create() + } + return _EVP_MD_CTX_new() +} + +func EVP_MD_get_size(md EVP_MD_PTR) int32 { + if vMajor == 1 { + return _EVP_MD_size(md) + } + return _EVP_MD_get_size(md) +} + +func EVP_MD_get_block_size(md EVP_MD_PTR) int32 { + if vMajor == 1 { + return _EVP_MD_block_size(md) + } + return _EVP_MD_get_block_size(md) +} + +func EVP_PKEY_get_size(pkey EVP_PKEY_PTR) int32 { + if vMajor == 1 { + return _EVP_PKEY_size(pkey) + } + return _EVP_PKEY_get_size(pkey) +} + +func EVP_PKEY_get_bits(pkey EVP_PKEY_PTR) int32 { + if vMajor == 1 { + return _EVP_PKEY_bits(pkey) + } + return _EVP_PKEY_get_bits(pkey) +} + +func EVP_CIPHER_get_block_size(cipher EVP_CIPHER_PTR) int32 { + if vMajor == 1 { + return _EVP_CIPHER_block_size(cipher) + } + return _EVP_CIPHER_get_block_size(cipher) +} diff --git a/internal/ossl/ossl_cgo.go b/internal/ossl/ossl_cgo.go index 7dd9bed..04d2d52 100644 --- a/internal/ossl/ossl_cgo.go +++ b/internal/ossl/ossl_cgo.go @@ -2,7 +2,12 @@ package ossl +/* +#include "ossl_extra.h" +#include +*/ import "C" +import "unsafe" type Point_conversion_form_t = C.point_conversion_form_t @@ -34,3 +39,156 @@ type EVP_KDF_PTR = C.EVP_KDF_PTR type EVP_KDF_CTX_PTR = C.EVP_KDF_CTX_PTR type MD5_CTX_PTR = C.MD5_CTX_PTR type SHA_CTX_PTR = C.SHA_CTX_PTR + +func EVP_EncryptUpdate_wrapper(ctx EVP_CIPHER_CTX_PTR, out *byte, in *byte, inl int32) (err error) { + r0 := C.EVP_EncryptUpdate_wrapper(ctx, (*C.uint8_t)(unsafe.Pointer(out)), (*C.uint8_t)(unsafe.Pointer(in)), C.int32_t(inl)) + if r0 != 1 { + err = newError("EVP_EncryptUpdate_wrapper") + } + return +} + +func EVP_DecryptUpdate_wrapper(ctx EVP_CIPHER_CTX_PTR, out *byte, in *byte, inl int32) (err error) { + r0 := C.EVP_DecryptUpdate_wrapper(ctx, (*C.uint8_t)(unsafe.Pointer(out)), (*C.uint8_t)(unsafe.Pointer(in)), C.int32_t(inl)) + if r0 != 1 { + err = newError("EVP_DecryptUpdate_wrapper") + } + return +} + +func EVP_CipherUpdate_wrapper(ctx EVP_CIPHER_CTX_PTR, out *byte, in *byte, inl int32) (err error) { + r0 := C.EVP_CipherUpdate_wrapper(ctx, (*C.uint8_t)(unsafe.Pointer(out)), (*C.uint8_t)(unsafe.Pointer(in)), C.int32_t(inl)) + if r0 != 1 { + err = newError("EVP_CipherUpdate_wrapper") + } + return +} + +func EVP_CIPHER_CTX_seal_wrapper(ctx EVP_CIPHER_CTX_PTR, out *byte, nonce *byte, in *byte, inl int32, aad *byte, addl int32) (err error) { + r0 := C.EVP_CIPHER_CTX_seal_wrapper(ctx, + (*C.uint8_t)(unsafe.Pointer(out)), + (*C.uint8_t)(unsafe.Pointer(nonce)), + (*C.uint8_t)(unsafe.Pointer(in)), + C.int32_t(inl), + (*C.uint8_t)(unsafe.Pointer(aad)), + C.int32_t(addl), + ) + if r0 != 1 { + err = newError("EVP_CIPHER_CTX_seal_wrapper") + } + return +} + +func EVP_CIPHER_CTX_open_wrapper(ctx EVP_CIPHER_CTX_PTR, out *byte, nonce *byte, in *byte, inl int32, aad *byte, addl int32, tag *byte) (err error) { + r0 := C.EVP_CIPHER_CTX_open_wrapper(ctx, + (*C.uint8_t)(unsafe.Pointer(out)), + (*C.uint8_t)(unsafe.Pointer(nonce)), + (*C.uint8_t)(unsafe.Pointer(in)), + C.int32_t(inl), + (*C.uint8_t)(unsafe.Pointer(aad)), + C.int32_t(addl), + (*C.uint8_t)(unsafe.Pointer(tag)), + ) + if r0 != 1 { + err = newError("EVP_CIPHER_CTX_open_wrapper") + } + return +} + +func DSA_get0_pqg_backport(dsa DSA_PTR, p, q, g *BIGNUM_PTR) { + C.go_openssl_DSA_get0_pqg_backport(dsa, p, q, g) +} + +func DSA_get0_key_backport(dsa DSA_PTR, pub_key, priv_key *BIGNUM_PTR) { + C.go_openssl_DSA_get0_key_backport(dsa, pub_key, priv_key) +} + +func DSA_set0_key_backport(dsa DSA_PTR, pub_key, priv_key BIGNUM_PTR) error { + if C.go_openssl_DSA_set0_key_backport(dsa, pub_key, priv_key) != 1 { + return newError("DSA_set0_key_backport") + } + return nil +} + +func DSA_set0_pqg_backport(dsa DSA_PTR, p, q, g BIGNUM_PTR) error { + if C.go_openssl_DSA_set0_pqg_backport(dsa, p, q, g) != 1 { + return newError("DSA_set0_pqg_backport") + } + return nil +} + +func EVP_md5_sha1_backport() EVP_MD_PTR { + return C.go_openssl_EVP_md5_sha1_backport() +} + +func HashSum(ctx, ctx2 EVP_MD_CTX_PTR, out *byte) error { + if C.go_hash_sum(ctx, ctx2, (*C.uchar)(unsafe.Pointer(out))) != 1 { + return newError("go_hash_sum") + } + return nil +} + +func EVP_PKEY_derive_wrapper(ctx EVP_PKEY_CTX_PTR, key *byte, keylen int) (int, error) { + r := C.EVP_PKEY_derive_wrapper(ctx, (*C.uchar)(unsafe.Pointer(key)), C.size_t(keylen)) + if r.result != 1 { + return 0, newError("EVP_PKEY_derive_wrapper") + } + return int(r.keylen), nil +} + +// Hand-roll custom wrappers for CRYPTO_malloc and CRYPTO_free which cast the +// function pointers to the correct signatures for OpenSSL 1.0.2. + +func CRYPTO_malloc_legacy102(num int32, file *byte, line int32) unsafe.Pointer { + return CRYPTO_malloc(int(num), file, line) +} + +func CRYPTO_free_legacy102(str unsafe.Pointer) { + CRYPTO_free(str, nil, 0) +} + +func EVP_PKEY_get_raw_public_key_wrapper(pkey EVP_PKEY_PTR, out *byte, outlen int) (int, error) { + r := C.EVP_PKEY_get_raw_public_key_wrapper(pkey, (*C.uchar)(unsafe.Pointer(out)), C.size_t(outlen)) + if r.result != 1 { + return 0, newError("EVP_PKEY_get_raw_public_key") + } + return int(r.len), nil +} + +func EVP_PKEY_get_raw_private_key_wrapper(pkey EVP_PKEY_PTR, out *byte, outlen int) (int, error) { + r := C.EVP_PKEY_get_raw_private_key_wrapper(pkey, (*C.uchar)(unsafe.Pointer(out)), C.size_t(outlen)) + if r.result != 1 { + return 0, newError("EVP_PKEY_get_raw_private_key") + } + return int(r.len), nil +} + +func EVP_DigestSign_wrapper(ctx EVP_MD_CTX_PTR, sig *byte, siglen int, msg *byte, msglen int) (int, error) { + r := C.EVP_DigestSign_wrapper(ctx, (*C.uchar)(unsafe.Pointer(sig)), C.size_t(siglen), (*C.uchar)(unsafe.Pointer(msg)), C.size_t(msglen)) + if r.result != 1 { + return 0, newError("EVP_DigestSign") + } + return int(r.siglen), nil +} + +func ThreadSetup() { + if C.go_openssl_thread_setup() != 1 { + panic("go_openssl_thread_setup") + } +} + +func Go_openssl_fips_enabled(handle unsafe.Pointer) int { + return int(C.go_openssl_fips_enabled(handle)) +} + +func Go_openssl_version_major(handle unsafe.Pointer) int { + return int(C.go_openssl_version_major(handle)) +} + +func Go_openssl_version_minor(handle unsafe.Pointer) int { + return int(C.go_openssl_version_minor(handle)) +} + +func Go_openssl_version_patch(handle unsafe.Pointer) int { + return int(C.go_openssl_version_patch(handle)) +} diff --git a/internal/ossl/ossl_extra.h b/internal/ossl/ossl_extra.h new file mode 100644 index 0000000..dd1be42 --- /dev/null +++ b/internal/ossl/ossl_extra.h @@ -0,0 +1,188 @@ +#include "api.h" +#include + +int go_openssl_fips_enabled(void* handle); +int go_openssl_version_major(void* handle); +int go_openssl_version_minor(void* handle); +int go_openssl_version_patch(void* handle); +void go_openssl_load_functions(void* handle, unsigned int major, unsigned int minor, unsigned int patch); +int go_openssl_thread_setup(void); +const EVP_MD_PTR go_openssl_EVP_md5_sha1_backport(void); +void go_openssl_DSA_get0_pqg_backport(const DSA_PTR d, BIGNUM_PTR *p, BIGNUM_PTR *q, BIGNUM_PTR *g); +int go_openssl_DSA_set0_pqg_backport(DSA_PTR d, BIGNUM_PTR p, BIGNUM_PTR q, BIGNUM_PTR g); +void go_openssl_DSA_get0_key_backport(const DSA_PTR d, BIGNUM_PTR *pub_key, BIGNUM_PTR *priv_key); +int go_openssl_DSA_set0_key_backport(DSA_PTR d, BIGNUM_PTR pub_key, BIGNUM_PTR priv_key); + +// These wrappers allocate out_len on the C stack to avoid having to pass a pointer from Go, which would escape to the heap. +// Use them only in situations where the output length can be safely discarded. +static inline int +EVP_EncryptUpdate_wrapper(EVP_CIPHER_CTX_PTR ctx, unsigned char *out, unsigned char *in, int in_len) +{ + int len; + return EVP_EncryptUpdate(ctx, out, &len, in, in_len); +} + +static inline int +EVP_DecryptUpdate_wrapper(EVP_CIPHER_CTX_PTR ctx, unsigned char *out, unsigned char *in, int in_len) +{ + int len; + return EVP_DecryptUpdate(ctx, out, &len, in, in_len); +} + +static inline int +EVP_CipherUpdate_wrapper(EVP_CIPHER_CTX_PTR ctx, unsigned char *out, unsigned char *in, int in_len) +{ + int len; + return EVP_CipherUpdate(ctx, out, &len, in, in_len); +} + + +// These wrappers allocate out_len on the C stack, and check that it matches the expected +// value, to avoid having to pass a pointer from Go, which would escape to the heap. + +static inline int +EVP_CIPHER_CTX_seal_wrapper(EVP_CIPHER_CTX_PTR ctx, + unsigned char *out, + const unsigned char *nonce, + const unsigned char *in, int in_len, + const unsigned char *aad, int aad_len) +{ + if (in_len == 0) in = (const unsigned char *)""; + if (aad_len == 0) aad = (const unsigned char *)""; + + if (EVP_EncryptInit_ex(ctx, NULL, NULL, NULL, nonce) != 1) + return 0; + + int discard_len, out_len; + if (EVP_EncryptUpdate(ctx, NULL, &discard_len, aad, aad_len) != 1 + || EVP_EncryptUpdate(ctx, out, &out_len, in, in_len) != 1 + || EVP_EncryptFinal_ex(ctx, out + out_len, &discard_len) != 1) + { + return 0; + } + + if (in_len != out_len) + return 0; + + return EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, out + out_len); +} + +static inline int +EVP_CIPHER_CTX_open_wrapper(EVP_CIPHER_CTX_PTR ctx, + unsigned char *out, + const unsigned char *nonce, + const unsigned char *in, int in_len, + const unsigned char *aad, int aad_len, + const unsigned char *tag) +{ + if (in_len == 0) { + in = (const unsigned char *)""; + // OpenSSL 1.0.2 in FIPS mode contains a bug: it will fail to verify + // unless EVP_DecryptUpdate is called at least once with a non-NULL + // output buffer. OpenSSL will not dereference the output buffer when + // the input length is zero, so set it to an arbitrary non-NULL pointer + // to satisfy OpenSSL when the caller only has authenticated additional + // data (AAD) to verify. While a stack-allocated buffer could be used, + // that would risk a stack-corrupting buffer overflow if OpenSSL + // unexpectedly dereferenced it. Instead pass a value which would + // segfault if dereferenced on any modern platform where a NULL-pointer + // dereference would also segfault. + if (out == NULL) out = (unsigned char *)1; + } + if (aad_len == 0) aad = (const unsigned char *)""; + + if (EVP_DecryptInit_ex(ctx, NULL, NULL, NULL, nonce) != 1) + return 0; + + // OpenSSL 1.0.x FIPS Object Module 2.0 versions below 2.0.5 require that + // the tag be set before the ciphertext, otherwise EVP_DecryptUpdate returns + // an error. At least one extant commercially-supported, FIPS validated + // build of OpenSSL 1.0.2 uses FIPS module version 2.0.1. Set the tag first + // to maximize compatibility with all OpenSSL version combinations. + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (unsigned char *)(tag)) != 1) + return 0; + + int discard_len, out_len; + if (EVP_DecryptUpdate(ctx, NULL, &discard_len, aad, aad_len) != 1 + || EVP_DecryptUpdate(ctx, out, &out_len, in, in_len) != 1) + { + return 0; + } + + if (EVP_DecryptFinal_ex(ctx, out + out_len, &discard_len) != 1) + return 0; + + if (out_len != in_len) + return 0; + + return 1; +} + +// go_hash_sum copies ctx into ctx2 and calls EVP_DigestFinal using ctx2. +// This is necessary because Go hash.Hash mandates that Sum has no effect +// on the underlying stream. In particular it is OK to Sum, then Write more, +// then Sum again, and the second Sum acts as if the first didn't happen. +// It is written in C because Sum() tend to be in the hot path, +// and doing one cgo call instead of two is a significant performance win. +static inline int +go_hash_sum(EVP_MD_CTX_PTR ctx, EVP_MD_CTX_PTR ctx2, unsigned char *out) +{ + if (EVP_MD_CTX_copy(ctx2, ctx) != 1) + return 0; + // TODO: use EVP_DigestFinal_ex once we know why it leaks + // memory on OpenSSL 1.0.2. + return EVP_DigestFinal(ctx2, out, NULL); +} + +// These wrappers also allocate length variables on the C stack to avoid escape to the heap, but do return the result. +// A struct is returned that contains multiple return values instead of OpenSSL's approach of using pointers. + +typedef struct +{ + int result; + size_t keylen; +} EVP_PKEY_derive_wrapper_out; + +static inline EVP_PKEY_derive_wrapper_out +EVP_PKEY_derive_wrapper(EVP_PKEY_CTX_PTR ctx, unsigned char *key, size_t keylen) +{ + EVP_PKEY_derive_wrapper_out r = {0, keylen}; + r.result = EVP_PKEY_derive(ctx, key, &r.keylen); + return r; +} + +typedef struct +{ + int result; + size_t len; +} EVP_PKEY_get_raw_key_out; + +static inline EVP_PKEY_get_raw_key_out +EVP_PKEY_get_raw_public_key_wrapper(const EVP_PKEY_PTR pkey, unsigned char *pub, size_t len) +{ + EVP_PKEY_get_raw_key_out r = {0, len}; + r.result = EVP_PKEY_get_raw_public_key(pkey, pub, &r.len); + return r; +} + +static inline EVP_PKEY_get_raw_key_out +EVP_PKEY_get_raw_private_key_wrapper(const EVP_PKEY_PTR pkey, unsigned char *priv, size_t len) +{ + EVP_PKEY_get_raw_key_out r = {0, len}; + r.result = EVP_PKEY_get_raw_private_key(pkey, priv, &r.len); + return r; +} + +typedef struct +{ + int result; + size_t siglen; +} EVP_DigestSign_wrapper_out; + +static inline EVP_DigestSign_wrapper_out +EVP_DigestSign_wrapper(EVP_MD_CTX_PTR ctx, unsigned char *sigret, size_t siglen, const unsigned char *tbs, size_t tbslen) +{ + EVP_DigestSign_wrapper_out r = {0, siglen}; + r.result = EVP_DigestSign(ctx, sigret, &r.siglen, tbs, tbslen); + return r; +}