Making Shared Libraries with Go – Versioned Symbols

Having previously made u2f-luks tools and scripts to allow me to use my surplus U2F (FIDO1) keys as a second factor for disk encryption, it was with some excitement that I read about the newly merged cryptsetup loadable plugin feature. The script used a custom keyscript for cryptsetup, but keyscripts will not be supported by systemd. This meant that the required volumes need to be unlocked and mounted in initramfs with the original cryptsetup tooling, which has its own pitfalls (removable devices etc)

Lets convert my keyscript into a cryptsetup plugin!

How do I convert my u2-luks tool, written in Go, into a shared library for a C program? cgo (godoc) is the Go and C interface layer, and can create shared libraries as nicely documented by other bloggers. The cryptsetup API (libcryptsetup.h) is fairly simple, only one function is required “cryptsetup_token_open”, so this sounded like a perfect evening project, but I ended up spending a few evenings and implementing most of them.

The required function signature from libcryptsetup.h:

typedef int (*crypt_token_open_func) (
	struct crypt_device *cd,
	int token,
	char **buffer,
	size_t *buffer_len,
	void *usrptr);

What does this look like in Go?

package main

// #cgo pkg-config: libcryptsetup
// #cgo LDFLAGS: -Wl,--version-script=cryptsetup_token.map
// #include <errno.h>
// #include <stdlib.h>
// #include <libcryptsetup.h>
import "C"

// ... snip ... 

//export cryptsetup_token_open
func cryptsetup_token_open(cd *C.struct_crypt_device, token C.int, 
    password **C.char, password_len *C.size_t, usrptr *C.char) C.int {

    if cerr := C.crypt_token_json_get(cd, token, &cjson); cerr < 0 {
        C.crypt_log(cd, C.CRYPT_LOG_ERROR,
            C.CString(fmt.Sprintf("token_json_get failed: errno %v\n", -cerr)))
        return -C.EINVAL
    }
    // ... snip ... 
}

The main takeaways I’d like to point out from this:

  • cgo shared libraries are built from the main package
  • C include and #cgo pragmas must appear directly before the import “C”
  • cgo exported functions are lowercase named
  • cgo exported functions have an export comment
  • C structs appear in the C package (with a struct_ prefix), that match types from the included headers.
  • There isn’t a void type directly, its equivalent in Go is unsafe.Pointer, but I didn’t want to use unsafe package, so I left that with a char pointer type, though I don’t plan to use it.
  • C likes negative Enum values as errors (-C.EINVAL), which are easy to use but look a bit weird in Go.
  • I can pass the referenced struct back into other calls in the API to get other needed data than provided in the function signature.
  • Go Strings need converted to C Strings – for both the format and to protect them from garbage collection by the Go Runtime, only pointers created by the C package can be passed into cgo calls.

The most difficult nut to crack was figuring out how to version the exported symbols, I tried not bothering to set any versions, but the module failed loading with symbol not found messages:

$ cryptsetup open --type luks  --debug ./testcontainer testcontainer
[snip]
Trying to load /lib/x86_64-linux-gnu/cryptsetup/libcryptsetup-token-u2f.so
/lib/x86_64-linux-gnu/cryptsetup/libcryptsetup-token-u2f.so: undefined symbol: cryptsetup_token_open, version CRYPTSETUP_TOKEN_1.0
[snip]

I thought I could export the function with a new name:

//export cryptsetup_token_open@CRYPTSETUP_TOKEN_1.0

But the compiler complained:

./main.go:28:1: export comment has wrong name "cryptsetup_token_open@CRYPTSETUP_TOKEN_1.0", want "cryptsetup_token_open"

So, time to read up on how linking works with versioned symbols. Eventually I ended up at GNU ld Manual – Version Scripts that explains how to configure the linker to add the versions, so lets try it out, I created the following file.

CRYPTSETUP_TOKEN_1.0 {
   global:
    cryptsetup_token_open;
    cryptsetup_token_open_pin;
    cryptsetup_token_validate;
    cryptsetup_token_dump;
    cryptsetup_token_version;
};

Adding the #cgo LDFLAGS pragma to say: tell the compiler when linking (LDFLAGS), to pass the linker a flag (-Wl) setting (--version-script) to this file cryptsetup_token.map

// #cgo LDFLAGS: -Wl,--version-script=cryptsetup_token.map 

This however results in the following compiler error

go build github.com/darkskiez/u2f-luks/lukstoken/plugin: invalid flag in #cgo LDFLAGS: -Wl,--version-script=cryptsetup_token.map

Which is fixed by setting CGO_LDFLAGS_ALLOW environment to an appropriate regex, so now the Makefile looks like:

libcrypt-token-u2f.so: main.go cryptsetup_token.map
    CGO_LDFLAGS_ALLOW='-Wl,--version-script=.*' go build -x \
    -buildmode c-shared -o libcrypt-token-u2f.so

Success!

The module loads and works, supports a presence only mode – for unlocking with just a touch, or with a passphrase/pin and presence. I had hoped to make cryptsetup automatically prompt the user when a password was required in addition to the token, but this does not yet work – cryptsetup issue #670 – there are unresolved UX implications that are amplified in severity when using tokens that may self-erase after incorrect guesses (eg. FIDO2 tokens lock out after 8 attempts.)

Unresolved issues: I’d like to use the crypt_safe_memzero or a comparable go function to make sure the key data is securely erased from the go runtime memory, but you cant (safely) pass go memory into C calls (cgo – Passing Pointers), so I’m not sure what would be best here.

My next steps will be investigating how systemd integrates fido2 tokens with cryptsetup, which wasn’t released at the time I wrote this module, and seeing if I can improve the UX with a similar system.

The rest of the code is available on github.com/darkskiez/u2f-luks/tree/master/lukstoken