Is it possible to use a password protected key from a TPM2 with the openssl function OSSL_STORE_open_ex
using the tpm2-openssl provider?
Based on: I build a small example that shows my problem.
I have the following key creation using a SWTPM:
# create a primary key
tpm2_createprimary -g sha256 -G ecc256 -f pem -o storagekey_PK.pem -c storagekey.ctx
# make primary key persistent
tpm2_evictcontrol -c storagekey.ctx 0x81000001
# create a key; use a password during the key creation
tpm2_create -C 0x81000001 -g sha256 -G ecc256 -u client_PK.obj -r client_SK.obj -p "1234" -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign|decrypt"
# load the key
tpm2_load -C 0x81000001 -u client_PK.obj -r client_SK.obj -c client_key.ctx
# make the key persistent
tpm2_evictcontrol -C o -c client_key.ctx 0x81000002
I have no problem with my code when creating a key that is not protected by a password.
# create a key
tpm2_create -C 0x81000001 -g sha256 -G ecc256 -u client_PK.obj -r client_SK.obj -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign|decrypt"
In the "what did you try" section I will show 4 different Versions of my code:
- VERSION 1: working version with a TPM2 key with no pw.
- VERSION 2: Custom and adding
?pass
or?password
to the handle; based on openssl commands that use the tpm2-openssl-provider. - VERSION 3: Using the same custom
ui_method
like VERSION 2 but keeping the handle without?pass
or?password
. - VERSION 4: Adding a
OSSL_PARAM
object.
(More details and some error messages can be found as comments inside the code example)
What I would expect:
I want to have a EVP_PKEY
object representing a TPM2 key that can be used by a function like sign. This works for TPM2 keys that are not protected by a password but fails when using a password during the creation. Is it possible to do this?
Some versions and additional information:
- OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)
- tpm2-openssl: 1.3.0
For the code below I use the following Makefile:
# compiler flags
CC = gcc
CFLAGS = -g
CFLAGSWAR = -W -Wall -Wextra
LDLIBS = -lcrypto -lssl # linked libs
build:
$(CC) -o main.out ./src/main.c $(CFLAGSWAR) $(LDLIBS)
#include <stdio.h>
#include <openssl/err.h>
#include <openssl/provider.h>
#include <string.h>
#include <openssl/store.h>
#include <openssl/ui.h>
/*
From: .c#L125
*/
int sign_message(EVP_PKEY *pkey)
{
EVP_MD_CTX *sctx = NULL;
EVP_MD_CTX *vctx = NULL;
unsigned char *sig = NULL;
size_t sig_len = 0;
int ret = 1;
const char *message = "Sabai Sabai";
// sign
if (!(sctx = EVP_MD_CTX_new()))
goto error;
if (!EVP_DigestSignInit_ex(sctx, NULL, "SHA-256", NULL, "provider=tpm2", pkey, NULL)
|| !EVP_DigestSign(sctx, NULL, &sig_len, (const unsigned char *)message, strlen(message)))
goto error;
if (!(sig = OPENSSL_malloc(sig_len)))
goto error;
if (!EVP_DigestSign(sctx, sig, &sig_len, (const unsigned char *)message, strlen(message)))
goto error;
// verify
if (!(vctx = EVP_MD_CTX_new()))
goto error;
if (!EVP_DigestVerifyInit_ex(vctx, NULL, "SHA-256", NULL, "provider=tpm2", pkey, NULL)
|| EVP_DigestVerify(vctx, sig, sig_len, (const unsigned char *)message, strlen(message)) != 1)
goto error;
ret = 0;
error:
OPENSSL_free(sig);
EVP_MD_CTX_free(vctx);
EVP_MD_CTX_free(sctx);
return ret;
}
static int tpm2_ui_callback(UI *ui, UI_STRING *uis)
{
int res = 0;
printf("-- tpm2_ui_callback --\n");
// set password into the UI result if the UI type is a prompt
if (UI_get_string_type(uis) == UIT_PROMPT) {
printf("-- setting callback pw --\n");
res = UI_set_result(ui, uis, "1234");
printf("-- result of pw setting %i --\n", res);
}
// fail if the wrong UI type was found
return res;
}
int main() {
OSSL_LIB_CTX *tpm2_libctx = NULL;
tpm2_libctx = OSSL_LIB_CTX_new();
OSSL_PROVIDER *defprov = NULL;
// load default provider
if ((defprov = OSSL_PROVIDER_load(tpm2_libctx, "default")) == NULL){
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
OSSL_PROVIDER *tpm2prov = NULL;
// load tpm2 provider
if ((tpm2prov = OSSL_PROVIDER_load(tpm2_libctx, "tpm2")) == NULL){
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
EVP_PKEY *TPMpkey = NULL;
OSSL_STORE_INFO *info = NULL;
OSSL_STORE_CTX *storeCtx = NULL;
/*
VERSION 1
This works for TPM2 keys with no pw.
Given a handle with pw the following error is given:
WARNING:esys:src/tss2-esys/api/Esys_Sign.c:311:Esys_Sign_Finish() Received TPM Error
ERROR:esys:src/tss2-esys/api/Esys_Sign.c:105:Esys_Sign() Esys Finish ErrorCode (0x0000098e)
*/
storeCtx = OSSL_STORE_open_ex("handle:0x81000002", tpm2_libctx, "?provider=tpm2", UI_create_method("handle"), NULL, NULL,NULL, NULL);
/*
VERSION 2
Adding ?pass or ?password to the handle; based on openssl commands that use the tpm2-openssl-provider like:
openssl s_client -provider tpm2 -provider default --propquery '?provider=tpm2' -cert ./credentials/client_crt.pem -key handle:0x81000002?password -connect localhost:4433 -tls1_3 -CAfile ./credentials/root_crt.pem -debug
Custom ui_method that is triggered when the storeCtx object is used in the OSSL_STORE_load function.
Triggers the ui callback but "info = OSSL_STORE_load(storeCtx))" is NULL -> cant load the key.
*/
// UI_METHOD *ui_method = UI_create_method("handle-with-pw");
// UI_method_set_reader(ui_method, tpm2_ui_callback);
// storeCtx = OSSL_STORE_open_ex("handle:0x81000002?pass", tpm2_libctx, "?provider=tpm2", ui_method, NULL, NULL,NULL, NULL);
/*
VERSION 3
Using the same custom ui_method like VERSION2 but keeping the handle without ?pass or ?password.
Key is loaded but cant be used because of an AUTH ERROR:
WARNING:esys:src/tss2-esys/api/Esys_Sign.c:311:Esys_Sign_Finish() Received TPM Error
ERROR:esys:src/tss2-esys/api/Esys_Sign.c:105:Esys_Sign() Esys Finish ErrorCode (0x0000098e)
*/
// UI_METHOD *ui_method = UI_create_method("handle-with-pw");
// UI_method_set_reader(ui_method, tpm2_ui_callback);
// storeCtx = OSSL_STORE_open_ex("handle:0x81000002", tpm2_libctx, "?provider=tpm2", ui_method, NULL, NULL,NULL, NULL);
/*
VERSION 4
Adding a OSSL_PARAM object.
Based on: .c#L14
Key is loaded but cant be used because of an AUTH ERROR:
WARNING:esys:src/tss2-esys/api/Esys_Sign.c:311:Esys_Sign_Finish() Received TPM Error
ERROR:esys:src/tss2-esys/api/Esys_Sign.c:105:Esys_Sign() Esys Finish ErrorCode (0x0000098e)
*/
// OSSL_PARAM params[2];
// params[0] = OSSL_PARAM_construct_utf8_string("user-auth", (char *)"1234", 0);
// params[1] = OSSL_PARAM_construct_end();
// storeCtx = OSSL_STORE_open_ex("handle:0x81000002", tpm2_libctx, "?provider=tpm2", NULL, NULL, params,NULL, NULL);
/*
Fetching the pkey from the TPM2 using the created storeCtx object.
*/
while (!OSSL_STORE_eof(storeCtx) && (info = OSSL_STORE_load(storeCtx)) != NULL) {
int type = OSSL_STORE_INFO_get_type(info);
if (type == OSSL_STORE_INFO_PKEY) {
TPMpkey = OSSL_STORE_INFO_get1_PKEY(info);
OSSL_STORE_INFO_free(info);
if (TPMpkey) {
break; // Exit the loop since the key has been found
}
} else {
OSSL_STORE_INFO_free(info); // Ensure info is freed if not the expected type
}
}
OSSL_STORE_close(storeCtx);
sign_message(TPMpkey);
return 0;
}
The error 0x0000098e
shows that the autorization to use the key failed:
tpm:session(1):the authorization HMAC check failed and DA counter incremented
Is it possible to use a password protected key from a TPM2 with the openssl function OSSL_STORE_open_ex
using the tpm2-openssl provider?
Based on: https://stackoverflow/a/78205985/13622395 I build a small example that shows my problem.
I have the following key creation using a SWTPM:
# create a primary key
tpm2_createprimary -g sha256 -G ecc256 -f pem -o storagekey_PK.pem -c storagekey.ctx
# make primary key persistent
tpm2_evictcontrol -c storagekey.ctx 0x81000001
# create a key; use a password during the key creation
tpm2_create -C 0x81000001 -g sha256 -G ecc256 -u client_PK.obj -r client_SK.obj -p "1234" -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign|decrypt"
# load the key
tpm2_load -C 0x81000001 -u client_PK.obj -r client_SK.obj -c client_key.ctx
# make the key persistent
tpm2_evictcontrol -C o -c client_key.ctx 0x81000002
I have no problem with my code when creating a key that is not protected by a password.
# create a key
tpm2_create -C 0x81000001 -g sha256 -G ecc256 -u client_PK.obj -r client_SK.obj -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign|decrypt"
In the "what did you try" section I will show 4 different Versions of my code:
- VERSION 1: working version with a TPM2 key with no pw.
- VERSION 2: Custom and adding
?pass
or?password
to the handle; based on openssl commands that use the tpm2-openssl-provider. - VERSION 3: Using the same custom
ui_method
like VERSION 2 but keeping the handle without?pass
or?password
. - VERSION 4: Adding a
OSSL_PARAM
object.
(More details and some error messages can be found as comments inside the code example)
What I would expect:
I want to have a EVP_PKEY
object representing a TPM2 key that can be used by a function like sign. This works for TPM2 keys that are not protected by a password but fails when using a password during the creation. Is it possible to do this?
Some versions and additional information:
- OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)
- tpm2-openssl: 1.3.0
For the code below I use the following Makefile:
# compiler flags
CC = gcc
CFLAGS = -g
CFLAGSWAR = -W -Wall -Wextra
LDLIBS = -lcrypto -lssl # linked libs
build:
$(CC) -o main.out ./src/main.c $(CFLAGSWAR) $(LDLIBS)
#include <stdio.h>
#include <openssl/err.h>
#include <openssl/provider.h>
#include <string.h>
#include <openssl/store.h>
#include <openssl/ui.h>
/*
From: https://github/tpm2-software/tpm2-openssl/blob/master/test/ec_genpkey_store_load.c#L125
*/
int sign_message(EVP_PKEY *pkey)
{
EVP_MD_CTX *sctx = NULL;
EVP_MD_CTX *vctx = NULL;
unsigned char *sig = NULL;
size_t sig_len = 0;
int ret = 1;
const char *message = "Sabai Sabai";
// sign
if (!(sctx = EVP_MD_CTX_new()))
goto error;
if (!EVP_DigestSignInit_ex(sctx, NULL, "SHA-256", NULL, "provider=tpm2", pkey, NULL)
|| !EVP_DigestSign(sctx, NULL, &sig_len, (const unsigned char *)message, strlen(message)))
goto error;
if (!(sig = OPENSSL_malloc(sig_len)))
goto error;
if (!EVP_DigestSign(sctx, sig, &sig_len, (const unsigned char *)message, strlen(message)))
goto error;
// verify
if (!(vctx = EVP_MD_CTX_new()))
goto error;
if (!EVP_DigestVerifyInit_ex(vctx, NULL, "SHA-256", NULL, "provider=tpm2", pkey, NULL)
|| EVP_DigestVerify(vctx, sig, sig_len, (const unsigned char *)message, strlen(message)) != 1)
goto error;
ret = 0;
error:
OPENSSL_free(sig);
EVP_MD_CTX_free(vctx);
EVP_MD_CTX_free(sctx);
return ret;
}
static int tpm2_ui_callback(UI *ui, UI_STRING *uis)
{
int res = 0;
printf("-- tpm2_ui_callback --\n");
// set password into the UI result if the UI type is a prompt
if (UI_get_string_type(uis) == UIT_PROMPT) {
printf("-- setting callback pw --\n");
res = UI_set_result(ui, uis, "1234");
printf("-- result of pw setting %i --\n", res);
}
// fail if the wrong UI type was found
return res;
}
int main() {
OSSL_LIB_CTX *tpm2_libctx = NULL;
tpm2_libctx = OSSL_LIB_CTX_new();
OSSL_PROVIDER *defprov = NULL;
// load default provider
if ((defprov = OSSL_PROVIDER_load(tpm2_libctx, "default")) == NULL){
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
OSSL_PROVIDER *tpm2prov = NULL;
// load tpm2 provider
if ((tpm2prov = OSSL_PROVIDER_load(tpm2_libctx, "tpm2")) == NULL){
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
EVP_PKEY *TPMpkey = NULL;
OSSL_STORE_INFO *info = NULL;
OSSL_STORE_CTX *storeCtx = NULL;
/*
VERSION 1
This works for TPM2 keys with no pw.
Given a handle with pw the following error is given:
WARNING:esys:src/tss2-esys/api/Esys_Sign.c:311:Esys_Sign_Finish() Received TPM Error
ERROR:esys:src/tss2-esys/api/Esys_Sign.c:105:Esys_Sign() Esys Finish ErrorCode (0x0000098e)
*/
storeCtx = OSSL_STORE_open_ex("handle:0x81000002", tpm2_libctx, "?provider=tpm2", UI_create_method("handle"), NULL, NULL,NULL, NULL);
/*
VERSION 2
Adding ?pass or ?password to the handle; based on openssl commands that use the tpm2-openssl-provider like:
openssl s_client -provider tpm2 -provider default --propquery '?provider=tpm2' -cert ./credentials/client_crt.pem -key handle:0x81000002?password -connect localhost:4433 -tls1_3 -CAfile ./credentials/root_crt.pem -debug
Custom ui_method that is triggered when the storeCtx object is used in the OSSL_STORE_load function.
Triggers the ui callback but "info = OSSL_STORE_load(storeCtx))" is NULL -> cant load the key.
*/
// UI_METHOD *ui_method = UI_create_method("handle-with-pw");
// UI_method_set_reader(ui_method, tpm2_ui_callback);
// storeCtx = OSSL_STORE_open_ex("handle:0x81000002?pass", tpm2_libctx, "?provider=tpm2", ui_method, NULL, NULL,NULL, NULL);
/*
VERSION 3
Using the same custom ui_method like VERSION2 but keeping the handle without ?pass or ?password.
Key is loaded but cant be used because of an AUTH ERROR:
WARNING:esys:src/tss2-esys/api/Esys_Sign.c:311:Esys_Sign_Finish() Received TPM Error
ERROR:esys:src/tss2-esys/api/Esys_Sign.c:105:Esys_Sign() Esys Finish ErrorCode (0x0000098e)
*/
// UI_METHOD *ui_method = UI_create_method("handle-with-pw");
// UI_method_set_reader(ui_method, tpm2_ui_callback);
// storeCtx = OSSL_STORE_open_ex("handle:0x81000002", tpm2_libctx, "?provider=tpm2", ui_method, NULL, NULL,NULL, NULL);
/*
VERSION 4
Adding a OSSL_PARAM object.
Based on: https://github/tpm2-software/tpm2-openssl/blob/8a5572639742dee5bc0b370ac04547b4b8ab2e0f/test/ec_genpkey_store_load.c#L14
Key is loaded but cant be used because of an AUTH ERROR:
WARNING:esys:src/tss2-esys/api/Esys_Sign.c:311:Esys_Sign_Finish() Received TPM Error
ERROR:esys:src/tss2-esys/api/Esys_Sign.c:105:Esys_Sign() Esys Finish ErrorCode (0x0000098e)
*/
// OSSL_PARAM params[2];
// params[0] = OSSL_PARAM_construct_utf8_string("user-auth", (char *)"1234", 0);
// params[1] = OSSL_PARAM_construct_end();
// storeCtx = OSSL_STORE_open_ex("handle:0x81000002", tpm2_libctx, "?provider=tpm2", NULL, NULL, params,NULL, NULL);
/*
Fetching the pkey from the TPM2 using the created storeCtx object.
*/
while (!OSSL_STORE_eof(storeCtx) && (info = OSSL_STORE_load(storeCtx)) != NULL) {
int type = OSSL_STORE_INFO_get_type(info);
if (type == OSSL_STORE_INFO_PKEY) {
TPMpkey = OSSL_STORE_INFO_get1_PKEY(info);
OSSL_STORE_INFO_free(info);
if (TPMpkey) {
break; // Exit the loop since the key has been found
}
} else {
OSSL_STORE_INFO_free(info); // Ensure info is freed if not the expected type
}
}
OSSL_STORE_close(storeCtx);
sign_message(TPMpkey);
return 0;
}
The error 0x0000098e
shows that the autorization to use the key failed:
tpm:session(1):the authorization HMAC check failed and DA counter incremented
Share
Improve this question
edited Mar 31 at 8:11
Simon
asked Mar 28 at 8:04
SimonSimon
1548 bronze badges
1 Answer
Reset to default 0Just in case anyone here happens to come across it. After some more testing and a closer look into the tpm2-openssl provider source code, I found my mistakes and adapted version 2.
All important steps and findings are included as comments in the modified code below.
The key error was to use the result returned by UI_set_result
as the return value of the callback function. UI_set_result
returns 0
on success. However, the tpm2-provider checks the return of the callback function as:
// https://github/tpm2-software/tpm2-openssl/blob/8a5572639742dee5bc0b370ac04547b4b8ab2e0f/src/tpm2-provider-store-handle.c#L358
if (!pw_cb((char *)userauth.buffer, sizeof(TPMU_HA), &plen, NULL, pw_cbarg)) {
TPM2_ERROR_raise(sctx->core, TPM2_ERR_AUTHORIZATION_FAILURE);
goto error;
}
// continue
So it falls into the error case if 0
is returned and only continues if 1
(or all other values except 0
) is returned. Which I personally found quite confusing, but will certainly have good reasons. Therefore I had to adapt my callback function to return 1 after successful use of UI_set_result
.
#include <stdio.h>
#include <openssl/err.h>
#include <openssl/provider.h>
#include <string.h>
#include <openssl/store.h>
#include <openssl/ui.h>
// struct to hold the specified passphrase
typedef struct {
const char *passphrase;
} _TPM2PassowrdData;
/*
From: https://github/tpm2-software/tpm2-openssl/blob/master/test/ec_genpkey_store_load.c#L125
*/
int sign_message(EVP_PKEY *pkey)
{
EVP_MD_CTX *sctx = NULL;
EVP_MD_CTX *vctx = NULL;
unsigned char *sig = NULL;
size_t sig_len = 0;
int ret = 1;
const char *message = "Sabai Sabai";
// sign
if (!(sctx = EVP_MD_CTX_new()))
goto error;
if (!EVP_DigestSignInit_ex(sctx, NULL, "SHA-256", NULL, "provider=tpm2", pkey, NULL)
|| !EVP_DigestSign(sctx, NULL, &sig_len, (const unsigned char *)message, strlen(message)))
goto error;
if (!(sig = OPENSSL_malloc(sig_len)))
goto error;
if (!EVP_DigestSign(sctx, sig, &sig_len, (const unsigned char *)message, strlen(message)))
goto error;
// verify
if (!(vctx = EVP_MD_CTX_new()))
goto error;
if (!EVP_DigestVerifyInit_ex(vctx, NULL, "SHA-256", NULL, "provider=tpm2", pkey, NULL)
|| EVP_DigestVerify(vctx, sig, sig_len, (const unsigned char *)message, strlen(message)) != 1)
goto error;
printf("[DEBUG] Signature: %s \n", sig);
// set return fallue to success
ret = 0;
error:
OPENSSL_free(sig);
EVP_MD_CTX_free(vctx);
EVP_MD_CTX_free(sctx);
return ret;
}
static int tpm2_ui_callback(UI *ui, UI_STRING *uis)
{
// receive the previously set user data (containing the passphrase)
_TPM2PassowrdData *ui_data = (_TPM2PassowrdData *)UI_get0_user_data(ui);
// prepare retrun value
int ret = 1;
// set passphrase into the UI result if the UI type is a prompt
if (UI_get_string_type(uis) == UIT_PROMPT) {
// check if the passphrase inside the user data is empty
if (ui_data->passphrase[0] != '\0'){
/*
Set the passphrase as the result for the ui prompt
UI_set_result returns 0 on success.
*/
ret = UI_set_result(ui, uis, ui_data->passphrase);
printf("[DEBUG] PW-CALLBACK SET WITH RESULT UI_set_result: %i\n", ret);
/*
Return 1 on success because the tpm2-openssl provider checks as follows:
https://github/tpm2-software/tpm2-openssl/blob/8a5572639742dee5bc0b370ac04547b4b8ab2e0f/src/tpm2-provider-store-handle.c#L358
if(!pw_cb(...)){
// ERROR CASE
}
// SUCESS
*/
return 1;
}
}
printf("[DEBUG] UI_set_result FAILED RETURNING DEFAULT CALLBACK FUNCTION\n");
// call the default ui reader function (promt) if no passphrase was givenö
return UI_method_get_reader(UI_OpenSSL())(ui, uis);
}
int main() {
OSSL_LIB_CTX *tpm2_libctx = NULL;
tpm2_libctx = OSSL_LIB_CTX_new();
OSSL_PROVIDER *defprov = NULL;
// load default provider
if ((defprov = OSSL_PROVIDER_load(tpm2_libctx, "default")) == NULL){
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
OSSL_PROVIDER *tpm2prov = NULL;
// load tpm2 provider
if ((tpm2prov = OSSL_PROVIDER_load(tpm2_libctx, "tpm2")) == NULL){
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
EVP_PKEY *TPMpkey = NULL;
OSSL_STORE_INFO *info = NULL;
OSSL_STORE_CTX *storeCtx = NULL;
/*
initially VERSION 2
Adding ?pass or ?password to the handle -> triggers the demand for a passphrase in the tpm2-openssl provider
Configure a coustom ui_method, that handles the given passphrase.
*/
UI_METHOD *ui_method = UI_create_method("handle-with-pw");
UI_method_set_reader(ui_method, tpm2_ui_callback);
// build a user data struct
_TPM2PassowrdData ui_data = { "1234" };
/*
Open a STORE CTX using:
- the handle string
- OSSL_LIB_CTX including the loaded tpm2-openssl provider
- a query string to insturct OpenSSL to use the tpm2-provider to open the STORE ctx
- a pointer to the previously configured ui_method
- a pointer to the user data for the STORE ctx to use inside the ui_method
*/
storeCtx = OSSL_STORE_open_ex("handle:0x81000002?pass", tpm2_libctx, "?provider=tpm2", ui_method, &ui_data, NULL,NULL, NULL);
// fetching the key from the opened STORE ctx
while (!OSSL_STORE_eof(storeCtx) && (info = OSSL_STORE_load(storeCtx)) != NULL) {
int type = OSSL_STORE_INFO_get_type(info);
if (type == OSSL_STORE_INFO_PKEY) {
TPMpkey = OSSL_STORE_INFO_get1_PKEY(info);
OSSL_STORE_INFO_free(info);
if (TPMpkey) {
break; // Exit the loop since the key has been found
}
} else {
OSSL_STORE_INFO_free(info); // Ensure info is freed if not the expected type
}
}
// close the opened STORE ctx
OSSL_STORE_close(storeCtx);
// check the output of the sig function
if (sign_message(TPMpkey) != 0)
return 1;
return 0;
}