diff --git a/99-solo.rules b/99-solo.rules index 9a78a00..1301cb7 100644 --- a/99-solo.rules +++ b/99-solo.rules @@ -10,9 +10,12 @@ LABEL="mm_usb_device_blacklist_end" # Solo -## access +## bootloader + firmware access ATTRS{idVendor}=="0483", ATTRS{idProduct}=="a2ca", TAG+="uaccess", GROUP="plugdev" +## DFU access +ATTRS{idVendor}=="0483", ATTRS{idProduct}=="df11", TAG+="uaccess", GROUP="plugdev" + ## Solo Secure symlink SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="a2ca", ATTRS{product}=="Solo [1-9]*", SYMLINK+="solokey" ## Solo Hacker symlink diff --git a/Dockerfile b/Dockerfile index f987e91..fb80684 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,5 +28,4 @@ RUN ln -s /opt/conda/bin/python3 /usr/local/bin/python3 RUN ln -s /opt/conda/bin/python3 /usr/local/bin/python # 3. Source code -RUN git clone --recurse-submodules https://github.com/solokeys/solo /solo - +RUN git clone --recurse-submodules https://github.com/solokeys/solo /solo --config core.autocrlf=input diff --git a/Makefile b/Makefile index bb5530f..1e6b38b 100644 --- a/Makefile +++ b/Makefile @@ -75,10 +75,12 @@ fido2-test: venv venv/bin/python tools/ctap_test.py DOCKER_IMAGE := "solokeys/solo-firmware:local" -SOLO_VERSION := "master" +SOLO_VERSIONISH := "master" docker-build: docker build -t $(DOCKER_IMAGE) . - docker run --rm -v$(PWD)/builds:/builds -v$(PWD)/docker-build.sh:/build.sh $(DOCKER_IMAGE) /build.sh $(SOLO_VERSION) + docker run --rm -v "$(CURDIR)/builds:/builds" \ + -v "$(CURDIR)/in-docker-build.sh:/in-docker-build.sh" \ + $(DOCKER_IMAGE) /in-docker-build.sh $(SOLO_VERSIONISH) CPPCHECK_FLAGS=--quiet --error-exitcode=2 diff --git a/docker-build.sh b/docker-build.sh deleted file mode 100755 index b102399..0000000 --- a/docker-build.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -xe - -version=${1:-master} - -export PREFIX=/opt/gcc-arm-none-eabi-8-2018-q4-major/bin/ - -cd /solo/targets/stm32l432 -git checkout ${version} -version=$(git describe) -make cbor -make all-hacker - -cd / - -out_dir="builds" -out_hex="solo-${version}.hex" -out_sha2="solo-${version}.sha2" -cp /solo/targets/stm32l432/solo.hex ${out_dir}/${out_hex} -cd ${out_dir} -sha256sum ${out_hex} > ${out_sha2} - diff --git a/fido2/ctap.c b/fido2/ctap.c index 3f323d9..845e66f 100644 --- a/fido2/ctap.c +++ b/fido2/ctap.c @@ -21,6 +21,7 @@ #include "device.h" #include APP_CONFIG #include "wallet.h" +#include "extensions.h" #include "device.h" @@ -776,7 +777,18 @@ int ctap_filter_invalid_credentials(CTAP_getAssertion * GA) if (! ctap_authenticate_credential(&GA->rp, &GA->creds[i])) { printf1(TAG_GA, "CRED #%d is invalid\n", GA->creds[i].credential.id.count); - GA->creds[i].credential.id.count = 0; // invalidate +#ifdef ENABLE_U2F_EXTENSIONS + if (is_extension_request((uint8_t*)&GA->creds[i].credential.id, sizeof(CredentialId))) + { + printf1(TAG_EXT, "CRED #%d is extension\n", GA->creds[i].credential.id.count); + count++; + } + else +#endif + { + GA->creds[i].credential.id.count = 0; // invalidate + } + } else { @@ -856,6 +868,7 @@ uint8_t ctap_end_get_assertion(CborEncoder * map, CTAP_credentialDescriptor * cr int ret; uint8_t sigbuf[64]; uint8_t sigder[72]; + int sigder_sz; if (add_user) { @@ -869,7 +882,16 @@ uint8_t ctap_end_get_assertion(CborEncoder * map, CTAP_credentialDescriptor * cr crypto_ecc256_load_key((uint8_t*)&cred->credential.id, sizeof(CredentialId), NULL, 0); - int sigder_sz = ctap_calculate_signature(auth_data_buf, sizeof(CTAP_authDataHeader), clientDataHash, auth_data_buf, sigbuf, sigder); +#ifdef ENABLE_U2F_EXTENSIONS + if ( extend_fido2(&cred->credential.id, sigder) ) + { + sigder_sz = 72; + } + else +#endif + { + sigder_sz = ctap_calculate_signature(auth_data_buf, sizeof(CTAP_authDataHeader), clientDataHash, auth_data_buf, sigbuf, sigder); + } { ret = cbor_encode_int(map, RESP_signature); @@ -988,8 +1010,21 @@ uint8_t ctap_get_assertion(CborEncoder * encoder, uint8_t * request, int length) ret = cbor_encoder_create_map(encoder, &map, map_size); check_ret(ret); - ret = ctap_make_auth_data(&GA.rp, &map, auth_data_buf, sizeof(auth_data_buf), NULL, 0,0,NULL, 0); - check_retr(ret); +#ifdef ENABLE_U2F_EXTENSIONS + if ( is_extension_request((uint8_t*)&GA.creds[validCredCount - 1].credential.id, sizeof(CredentialId)) ) + { + ret = cbor_encode_int(&map,RESP_authData); + check_ret(ret); + memset(auth_data_buf,0,sizeof(auth_data_buf)); + ret = cbor_encode_byte_string(&map, auth_data_buf, sizeof(auth_data_buf)); + check_ret(ret); + } + else +#endif + { + ret = ctap_make_auth_data(&GA.rp, &map, auth_data_buf, sizeof(auth_data_buf), NULL, 0,0,NULL, 0); + check_retr(ret); + } /*for (int j = 0; j < GA.credLen; j++)*/ /*{*/ diff --git a/fido2/extensions/extensions.c b/fido2/extensions/extensions.c index cc56d51..d14cab8 100644 --- a/fido2/extensions/extensions.c +++ b/fido2/extensions/extensions.c @@ -8,6 +8,7 @@ #include #include "extensions.h" #include "u2f.h" +#include "ctap.h" #include "wallet.h" #include "solo.h" #include "device.h" @@ -57,7 +58,8 @@ int16_t bridge_u2f_to_extensions(uint8_t * _chal, uint8_t * _appid, uint8_t klen #elif defined(WALLET_EXTENSION) ret = bridge_u2f_to_wallet(_chal, _appid, klen, keyh); #else - ret = bridge_u2f_to_solo(_chal, _appid, klen, keyh); + ret = bridge_u2f_to_solo(sig, keyh, klen); + u2f_response_writeback(sig,72); #endif if (ret != 0) @@ -74,6 +76,21 @@ int16_t bridge_u2f_to_extensions(uint8_t * _chal, uint8_t * _appid, uint8_t klen return U2F_SW_NO_ERROR; } +// Returns 1 if this is a extension request. +// Else 0 if nothing is done. +int16_t extend_fido2(CredentialId * credid, uint8_t * output) +{ + if (is_extension_request((uint8_t*)credid, sizeof(CredentialId))) + { + output[0] = bridge_u2f_to_solo(output+1, (uint8_t*)credid, sizeof(CredentialId)); + return 1; + } + else + { + return 0; + } +} + int16_t extend_u2f(struct u2f_request_apdu* req, uint32_t len) { @@ -93,7 +110,7 @@ int16_t extend_u2f(struct u2f_request_apdu* req, uint32_t len) { rcode = U2F_SW_WRONG_DATA; } - printf1(TAG_EXT,"Ignoring U2F request\n"); + printf1(TAG_EXT,"Ignoring U2F check request\n"); dump_hex1(TAG_EXT, (uint8_t *) &auth->kh, auth->khl); goto end; } @@ -102,7 +119,7 @@ int16_t extend_u2f(struct u2f_request_apdu* req, uint32_t len) if ( ! is_extension_request((uint8_t *) &auth->kh, auth->khl)) // Pin requests { rcode = U2F_SW_WRONG_PAYLOAD; - printf1(TAG_EXT, "Ignoring U2F request\n"); + printf1(TAG_EXT, "Ignoring U2F auth request\n"); dump_hex1(TAG_EXT, (uint8_t *) &auth->kh, auth->khl); goto end; } diff --git a/fido2/extensions/extensions.h b/fido2/extensions/extensions.h index 86a0f54..ea871a5 100644 --- a/fido2/extensions/extensions.h +++ b/fido2/extensions/extensions.h @@ -10,6 +10,10 @@ int16_t extend_u2f(struct u2f_request_apdu* req, uint32_t len); +int16_t extend_fido2(CredentialId * credid, uint8_t * output); + int bootloader_bridge(int klen, uint8_t * keyh); +int is_extension_request(uint8_t * kh, int len); + #endif /* EXTENSIONS_H_ */ diff --git a/fido2/extensions/solo.c b/fido2/extensions/solo.c index eff6eef..8951506 100644 --- a/fido2/extensions/solo.c +++ b/fido2/extensions/solo.c @@ -31,27 +31,26 @@ #include "log.h" #include APP_CONFIG -int16_t bridge_u2f_to_solo(uint8_t * _chal, uint8_t * _appid, uint8_t klen, uint8_t * keyh) +// output must be at least 71 bytes +int16_t bridge_u2f_to_solo(uint8_t * output, uint8_t * keyh, int keylen) { - static uint8_t msg_buf[72]; int8_t ret = 0; wallet_request * req = (wallet_request *) keyh; - printf1(TAG_WALLET, "u2f-solo [%d]: ", klen); dump_hex1(TAG_WALLET, keyh, klen); + printf1(TAG_WALLET, "u2f-solo [%d]: ", keylen); dump_hex1(TAG_WALLET, keyh, keylen); switch(req->operation) { case WalletVersion: - msg_buf[0] = SOLO_VERSION_MAJ; - msg_buf[1] = SOLO_VERSION_MIN; - msg_buf[2] = SOLO_VERSION_PATCH; - u2f_response_writeback(msg_buf, 3); + output[0] = SOLO_VERSION_MAJ; + output[1] = SOLO_VERSION_MIN; + output[2] = SOLO_VERSION_PATCH; break; case WalletRng: printf1(TAG_WALLET,"SoloRng\n"); - ret = ctap_generate_rng(msg_buf, 72); + ret = ctap_generate_rng(output, 71); if (ret != 1) { printf1(TAG_WALLET,"Rng failed\n"); @@ -60,7 +59,6 @@ int16_t bridge_u2f_to_solo(uint8_t * _chal, uint8_t * _appid, uint8_t klen, uint } ret = 0; - u2f_response_writeback((uint8_t *)msg_buf,72); break; default: diff --git a/fido2/extensions/solo.h b/fido2/extensions/solo.h index 04e9d8d..ae6574f 100644 --- a/fido2/extensions/solo.h +++ b/fido2/extensions/solo.h @@ -22,6 +22,6 @@ #ifndef SOLO_H_ #define SOLO_H_ -int16_t bridge_u2f_to_solo(uint8_t * _chal, uint8_t * _appid, uint8_t klen, uint8_t * keyh); +int16_t bridge_u2f_to_solo(uint8_t * output, uint8_t * keyh, int keylen); #endif diff --git a/fido2/log.c b/fido2/log.c index 5b9aa13..04764d6 100644 --- a/fido2/log.c +++ b/fido2/log.c @@ -47,7 +47,7 @@ struct logtag tagtable[] = { {TAG_WALLET,"WALLET"}, {TAG_STOR,"STOR"}, {TAG_BOOT,"BOOT"}, - {TAG_BOOT,"EXT"}, + {TAG_EXT,"EXT"}, }; diff --git a/fido2/log.h b/fido2/log.h index 3062681..e5ded84 100644 --- a/fido2/log.h +++ b/fido2/log.h @@ -41,7 +41,7 @@ typedef enum TAG_STOR = (1 << 15), TAG_DUMP2 = (1 << 16), TAG_BOOT = (1 << 17), - TAG_EXT = (1 << 17), + TAG_EXT = (1 << 18), TAG_FILENO = (1u << 31) } LOG_TAG; diff --git a/fido2/u2f.c b/fido2/u2f.c index d7caf5e..5b56479 100644 --- a/fido2/u2f.c +++ b/fido2/u2f.c @@ -44,7 +44,7 @@ void u2f_request(struct u2f_request_apdu* req, CTAP_RESPONSE * resp) #ifdef ENABLE_U2F_EXTENSIONS rcode = extend_u2f(req, len); #endif - if (rcode != U2F_SW_NO_ERROR) // If the extension didn't do anything... + if (rcode != U2F_SW_NO_ERROR && rcode != U2F_SW_CONDITIONS_NOT_SATISFIED) // If the extension didn't do anything... { #ifdef ENABLE_U2F switch(req->ins) @@ -224,7 +224,7 @@ static int16_t u2f_authenticate(struct u2f_authenticate_request * req, uint8_t c } count = ctap_atomic_count(0); - hash[0] = (count >> 24) & 0xff; + hash[0] = 0xff; hash[1] = (count >> 16) & 0xff; hash[2] = (count >> 8) & 0xff; hash[3] = (count >> 0) & 0xff; @@ -241,7 +241,7 @@ static int16_t u2f_authenticate(struct u2f_authenticate_request * req, uint8_t c crypto_ecc256_sign(hash, 32, sig); u2f_response_writeback(&up,1); - hash[0] = (count >> 24) & 0xff; + hash[0] = 0xff; hash[1] = (count >> 16) & 0xff; hash[2] = (count >> 8) & 0xff; hash[3] = (count >> 0) & 0xff; diff --git a/in-docker-build.sh b/in-docker-build.sh new file mode 100755 index 0000000..3b17ebd --- /dev/null +++ b/in-docker-build.sh @@ -0,0 +1,37 @@ +#!/bin/bash -xe + +version=${1:-master} + +export PREFIX=/opt/gcc-arm-none-eabi-8-2018-q4-major/bin/ + +cd /solo/targets/stm32l432 +git fetch +git checkout ${version} +version=$(git describe) + +make cbor + +out_dir="/builds" + +function build() { + part=${1} + variant=${2} + output=${3:-${part}} + what="${part}-${variant}" + + make full-clean + + make ${what} + + out_hex="${what}-${version}.hex" + out_sha2="${what}-${version}.sha2" + + mv ${output}.hex ${out_hex} + sha256sum ${out_hex} > ${out_sha2} + cp ${out_hex} ${out_sha2} ${out_dir} +} + +build bootloader nonverifying +build bootloader verifying +build firmware hacker solo +build firmware secure solo diff --git a/targets/stm32l432/Makefile b/targets/stm32l432/Makefile index 8634507..0ac5843 100644 --- a/targets/stm32l432/Makefile +++ b/targets/stm32l432/Makefile @@ -9,6 +9,25 @@ merge_hex=../../tools/solotool.py mergehex .PHONY: all all-hacker all-locked debugboot-app debugboot-boot boot-sig-checking boot-no-sig build-release-locked build-release build-release build-hacker build-debugboot clean clean2 flash flash_dfu flashboot detach cbor test + +# The following are the main targets for reproducible builds. +# TODO: better explanation +firmware-hacker: + $(MAKE) -f $(APPMAKE) -j8 solo.hex PREFIX=$(PREFIX) DEBUG=0 EXTRA_DEFINES='-DSOLO_HACKER -DFLASH_ROP=0' + +firmware-secure: + $(MAKE) -f $(APPMAKE) -j8 solo.hex PREFIX=$(PREFIX) DEBUG=0 EXTRA_DEFINES='-DUSE_SOLOKEYS_CERT -DFLASH_ROP=2' + +bootloader-nonverifying: + $(MAKE) -f $(BOOTMAKE) -j8 bootloader.hex PREFIX=$(PREFIX) EXTRA_DEFINES='-DSOLO_HACKER' DEBUG=0 + +bootloader-verifying: + $(MAKE) -f $(BOOTMAKE) -j8 bootloader.hex PREFIX=$(PREFIX) DEBUG=0 + +full-clean: clean2 + + +# The older targets, may be re-organised all: $(MAKE) -f $(APPMAKE) -j8 solo.hex PREFIX=$(PREFIX) DEBUG=$(DEBUG) EXTRA_DEFINES='-DFLASH_ROP=1' diff --git a/targets/stm32l432/build/application.mk b/targets/stm32l432/build/application.mk index 37c4ea5..a22bc1d 100644 --- a/targets/stm32l432/build/application.mk +++ b/targets/stm32l432/build/application.mk @@ -4,7 +4,7 @@ include build/common.mk SRC = src/main.c src/init.c src/redirect.c src/flash.c src/rng.c src/led.c src/device.c SRC += src/fifo.c src/crypto.c src/attestation.c SRC += src/startup_stm32l432xx.s src/system_stm32l4xx.c -SRC += $(wildcard lib/*.c) $(wildcard lib/usbd/*.c) +SRC += $(DRIVER_LIBS) $(USB_LIB) # FIDO2 lib SRC += ../../fido2/util.c ../../fido2/u2f.c ../../fido2/test_power.c diff --git a/targets/stm32l432/build/bootloader.mk b/targets/stm32l432/build/bootloader.mk index 24f2751..0f1f2d3 100644 --- a/targets/stm32l432/build/bootloader.mk +++ b/targets/stm32l432/build/bootloader.mk @@ -5,7 +5,7 @@ SRC = bootloader/main.c bootloader/bootloader.c SRC += src/init.c src/redirect.c src/flash.c src/rng.c src/led.c src/device.c SRC += src/fifo.c src/crypto.c src/attestation.c SRC += src/startup_stm32l432xx.s src/system_stm32l4xx.c -SRC += $(wildcard lib/*.c) $(wildcard lib/usbd/*.c) +SRC += $(DRIVER_LIBS) $(USB_LIB) # FIDO2 lib SRC += ../../fido2/util.c ../../fido2/u2f.c ../../fido2/extensions/extensions.c diff --git a/targets/stm32l432/build/common.mk b/targets/stm32l432/build/common.mk index 854204d..4170e51 100644 --- a/targets/stm32l432/build/common.mk +++ b/targets/stm32l432/build/common.mk @@ -3,6 +3,15 @@ CP=$(PREFIX)arm-none-eabi-objcopy SZ=$(PREFIX)arm-none-eabi-size AR=$(PREFIX)arm-none-eabi-ar +DRIVER_LIBS := lib/stm32l4xx_hal_pcd.c lib/stm32l4xx_hal_pcd_ex.c lib/stm32l4xx_ll_gpio.c \ + lib/stm32l4xx_ll_rcc.c lib/stm32l4xx_ll_rng.c lib/stm32l4xx_ll_tim.c \ + lib/stm32l4xx_ll_usb.c lib/stm32l4xx_ll_utils.c lib/stm32l4xx_ll_pwr.c \ + lib/stm32l4xx_ll_usart.c + +USB_LIB := lib/usbd/usbd_cdc.c lib/usbd/usbd_cdc_if.c lib/usbd/usbd_composite.c \ + lib/usbd/usbd_conf.c lib/usbd/usbd_core.c lib/usbd/usbd_ioreq.c \ + lib/usbd/usbd_ctlreq.c lib/usbd/usbd_desc.c lib/usbd/usbd_hid.c + VERSION:=$(shell git describe --abbrev=0 ) VERSION_FULL:=$(shell git describe) VERSION_MAJ:=$(shell python -c 'print("$(VERSION)".split(".")[0])') @@ -10,7 +19,7 @@ VERSION_MIN:=$(shell python -c 'print("$(VERSION)".split(".")[1])') VERSION_PAT:=$(shell python -c 'print("$(VERSION)".split(".")[2])') VERSION_FLAGS= -DSOLO_VERSION_MAJ=$(VERSION_MAJ) -DSOLO_VERSION_MIN=$(VERSION_MIN) \ - -DSOLO_VERSION_PATCH=$(VERSION_PAT) -DVERSION=\"$(VERSION_FULL)\" + -DSOLO_VERSION_PATCH=$(VERSION_PAT) -DSOLO_VERSION=\"$(VERSION_FULL)\" _all: echo $(VERSION_FULL) diff --git a/targets/stm32l432/lib/usbd/usbd_desc.c b/targets/stm32l432/lib/usbd/usbd_desc.c index 3505328..8bc1d83 100644 --- a/targets/stm32l432/lib/usbd/usbd_desc.c +++ b/targets/stm32l432/lib/usbd/usbd_desc.c @@ -166,6 +166,32 @@ uint8_t *USBD_HID_ManufacturerStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *l */ uint8_t *USBD_HID_SerialStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length) { - USBD_GetString((uint8_t *)USBD_SERIAL_NUM, USBD_StrDesc, length); - return USBD_StrDesc; + // Match the same alg as the DFU to make serial number + volatile uint8_t * UUID = (volatile uint8_t *)0x1FFF7590; + const char hexdigit[] = "0123456789ABCDEF"; + uint8_t uuid[6]; + uint8_t uuid_str[13]; + uint8_t c; + int i; + uuid_str[12] = 0; + + uuid[0] = UUID[11]; + uuid[1] = UUID[10] + UUID[2]; + uuid[2] = UUID[9]; + uuid[3] = UUID[8] + UUID[0]; + uuid[4] = UUID[7]; + uuid[5] = UUID[6]; + + // quick method to convert to hex string + for (i = 0; i < 6; i++) + { + c = (uuid[i]>>4) & 0x0f; + uuid_str[i * 2 + 0] = hexdigit[ c ]; + c = (uuid[i]>>0) & 0x0f; + uuid_str[i * 2 + 1] = hexdigit[ c ]; + } + + + USBD_GetString((uint8_t *)uuid_str, USBD_StrDesc, length); + return USBD_StrDesc; } diff --git a/tools/ctap_test.py b/tools/ctap_test.py index 4b44f5d..787ee03 100755 --- a/tools/ctap_test.py +++ b/tools/ctap_test.py @@ -383,12 +383,16 @@ class Tester: def test_u2f(self,): chal = sha256(b"AAA") appid = sha256(b"BBB") + lastc = 0 for i in range(0, 5): reg = self.ctap1.register(chal, appid) reg.verify(appid, chal) auth = self.ctap1.authenticate(chal, appid, reg.key_handle) # check endianness - assert auth.counter < 0x10000 + if lastc: + assert (auth.counter - lastc) < 10 + lastc = auth.counter + print(hex(lastc)) print("U2F reg + auth pass %d/5" % (i + 1)) def test_fido2_simple(self, pin_token=None): diff --git a/tools/solotool.py b/tools/solotool.py index aafe5f5..1573979 100755 --- a/tools/solotool.py +++ b/tools/solotool.py @@ -201,6 +201,24 @@ class SoloClient: return res.signature[1:] + def exchange_fido2(self, cmd, addr=0, data=b"A" * 16): + chal = "B" * 32 + + req = SoloClient.format_request(cmd, addr, data) + + assertions, client_data = self.client.get_assertion( + self.host, chal, [{"id": req, "type": "public-key"}] + ) + if len(assertions) < 1: + raise RuntimeError("Device didn't respond to FIDO2 extended assertion") + + res = assertions[0] + ret = res.signature[0] + if ret != CtapError.ERR.SUCCESS: + raise RuntimeError("Device returned non-success code %02x" % (ret,)) + + return res.signature[1:] + def bootloader_version(self,): data = self.exchange(SoloBootloader.version) if len(data) > 2: @@ -208,7 +226,7 @@ class SoloClient: return data[0] def solo_version(self,): - data = self.exchange_u2f(SoloExtension.version) + data = self.exchange_fido2(SoloExtension.version) return (data[0], data[1], data[2]) def write_flash(self, addr, data): @@ -585,6 +603,7 @@ def solo_main(): action="store_true", help="Continuously dump random numbers generated from Solo.", ) + parser.add_argument("--wink", action="store_true", help="HID Wink command.") parser.add_argument( "--reset", @@ -596,6 +615,9 @@ def solo_main(): action="store_true", help="Verify that the Solo firmware is from SoloKeys. Check firmware version.", ) + parser.add_argument( + "--version", action="store_true", help="Check firmware version on Solo." + ) args = parser.parse_args() p = SoloClient() @@ -627,6 +649,9 @@ def solo_main(): else: print("Unknown fingerprint! ", cert.fingerprint(hashes.SHA256())) + args.version = True + + if args.version: try: v = p.solo_version() print("Version: ", v)