From e9beafe52c0614e7a8184e905a38baf787676ae5 Mon Sep 17 00:00:00 2001 From: cheliequan Date: Fri, 19 Jul 2024 19:25:47 +0800 Subject: [PATCH 1/3] slove the compile error on openEuler 22.03sp3 with libbpf 0.8.1 Signed-off-by: cheliequan modified: ebpf/ebpf_rss.c --- ebpf/ebpf_rss.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ebpf/ebpf_rss.c b/ebpf/ebpf_rss.c index 118c68da83..cee658c158 100644 --- a/ebpf/ebpf_rss.c +++ b/ebpf/ebpf_rss.c @@ -49,7 +49,7 @@ bool ebpf_rss_load(struct EBPFRSSContext *ctx) goto error; } - bpf_program__set_socket_filter(rss_bpf_ctx->progs.tun_rss_steering_prog); + bpf_program__set_type(rss_bpf_ctx->progs.tun_rss_steering_prog, BPF_PROG_TYPE_SOCKET_FILTER); if (rss_bpf__load(rss_bpf_ctx)) { trace_ebpf_error("eBPF RSS", "can not load RSS program"); -- Gitee From 5ffc13a3bea3ac3b2d5155ac68044f345b7f3c35 Mon Sep 17 00:00:00 2001 From: cheliequan Date: Fri, 19 Jul 2024 20:46:14 +0800 Subject: [PATCH 2/3] add sm3 hash alg and sm4 cipher-alg, by default use sm4 crypto alg Signed-off-by: cheliequan modified: ../crypto/block-luks.c modified: ../crypto/cipher-gcrypt.c.inc modified: ../crypto/cipher-nettle.c.inc modified: ../crypto/cipher.c modified: ../crypto/hash-gcrypt.c modified: ../crypto/hash.c modified: ../crypto/hmac-gcrypt.c modified: ../crypto/pbkdf-gcrypt.c modified: ../meson.build modified: ../qapi/crypto.json new file: test_encvm/create_domain_xml.sh new file: test_encvm/create_secret_xml.sh new file: test_encvm/createsecvm.sh new file: test_encvm/secret.xml new file: test_encvm/secret_set.sh new file: test_encvm/template.xml new file: test_encvm/test_encvm.sh new file: test_encvm/vm.xml new file: test_encvm/vm_kvm.xml modified: unit/test-crypto-cipher.c modified: unit/test-crypto-hash.c modified: unit/test-crypto-hmac.c modified: unit/test-crypto-pbkdf.c --- crypto/block-luks.c | 13 ++- crypto/cipher-gcrypt.c.inc | 8 ++ crypto/cipher-nettle.c.inc | 49 +++++++++++ crypto/cipher.c | 6 ++ crypto/hash-gcrypt.c | 3 + crypto/hash.c | 3 + crypto/hmac-gcrypt.c | 3 + crypto/pbkdf-gcrypt.c | 6 ++ meson.build | 54 ++++++++++++ qapi/crypto.json | 6 +- tests/test_encvm/create_domain_xml.sh | 22 +++++ tests/test_encvm/create_secret_xml.sh | 22 +++++ tests/test_encvm/createsecvm.sh | 33 +++++++ tests/test_encvm/secret.xml | 6 ++ tests/test_encvm/secret_set.sh | 11 +++ tests/test_encvm/template.xml | 119 ++++++++++++++++++++++++++ tests/test_encvm/test_encvm.sh | 17 ++++ tests/test_encvm/vm.xml | 113 ++++++++++++++++++++++++ tests/test_encvm/vm_kvm.xml | 119 ++++++++++++++++++++++++++ tests/unit/test-crypto-cipher.c | 13 +++ tests/unit/test-crypto-hash.c | 10 +++ tests/unit/test-crypto-hmac.c | 8 ++ tests/unit/test-crypto-pbkdf.c | 16 ++++ 23 files changed, 656 insertions(+), 4 deletions(-) create mode 100644 tests/test_encvm/create_domain_xml.sh create mode 100644 tests/test_encvm/create_secret_xml.sh create mode 100644 tests/test_encvm/createsecvm.sh create mode 100644 tests/test_encvm/secret.xml create mode 100644 tests/test_encvm/secret_set.sh create mode 100644 tests/test_encvm/template.xml create mode 100644 tests/test_encvm/test_encvm.sh create mode 100644 tests/test_encvm/vm.xml create mode 100644 tests/test_encvm/vm_kvm.xml diff --git a/crypto/block-luks.c b/crypto/block-luks.c index fe8f04ffb2..24789857b2 100644 --- a/crypto/block-luks.c +++ b/crypto/block-luks.c @@ -126,12 +126,23 @@ qcrypto_block_luks_cipher_size_map_twofish[] = { { 0, 0 }, }; +#ifdef CONFIG_CRYPTO_SM4 +static const QCryptoBlockLUKSCipherSizeMap +qcrypto_block_luks_cipher_size_map_sm4[] = { + { 16, QCRYPTO_CIPHER_ALG_SM4}, + { 0, 0 }, +}; +#endif + static const QCryptoBlockLUKSCipherNameMap qcrypto_block_luks_cipher_name_map[] = { { "aes", qcrypto_block_luks_cipher_size_map_aes }, { "cast5", qcrypto_block_luks_cipher_size_map_cast5 }, { "serpent", qcrypto_block_luks_cipher_size_map_serpent }, { "twofish", qcrypto_block_luks_cipher_size_map_twofish }, +#ifdef CONFIG_CRYPTO_SM4 + { "sm4", qcrypto_block_luks_cipher_size_map_sm4}, +#endif }; @@ -1340,7 +1351,7 @@ qcrypto_block_luks_create(QCryptoBlock *block, luks_opts.iter_time = QCRYPTO_BLOCK_LUKS_DEFAULT_ITER_TIME_MS; } if (!luks_opts.has_cipher_alg) { - luks_opts.cipher_alg = QCRYPTO_CIPHER_ALG_AES_256; + luks_opts.cipher_alg = QCRYPTO_CIPHER_ALG_SM4; } if (!luks_opts.has_cipher_mode) { luks_opts.cipher_mode = QCRYPTO_CIPHER_MODE_XTS; diff --git a/crypto/cipher-gcrypt.c.inc b/crypto/cipher-gcrypt.c.inc index a6a0117717..1377cbaf14 100644 --- a/crypto/cipher-gcrypt.c.inc +++ b/crypto/cipher-gcrypt.c.inc @@ -35,6 +35,9 @@ bool qcrypto_cipher_supports(QCryptoCipherAlgorithm alg, case QCRYPTO_CIPHER_ALG_SERPENT_256: case QCRYPTO_CIPHER_ALG_TWOFISH_128: case QCRYPTO_CIPHER_ALG_TWOFISH_256: +#ifdef CONFIG_CRYPTO_SM4 + case QCRYPTO_CIPHER_ALG_SM4: +#endif break; default: return false; @@ -219,6 +222,11 @@ static QCryptoCipher *qcrypto_cipher_ctx_new(QCryptoCipherAlgorithm alg, case QCRYPTO_CIPHER_ALG_TWOFISH_256: gcryalg = GCRY_CIPHER_TWOFISH; break; +#ifdef CONFIG_CRYPTO_SM4 + case QCRYPTO_CIPHER_ALG_SM4: + gcryalg = GCRY_CIPHER_SM4; + break; +#endif default: error_setg(errp, "Unsupported cipher algorithm %s", QCryptoCipherAlgorithm_str(alg)); diff --git a/crypto/cipher-nettle.c.inc b/crypto/cipher-nettle.c.inc index 24cc61f87b..b9a0cc0ac1 100644 --- a/crypto/cipher-nettle.c.inc +++ b/crypto/cipher-nettle.c.inc @@ -33,6 +33,9 @@ #ifndef CONFIG_QEMU_PRIVATE_XTS #include #endif +#ifdef CONFIG_CRYPTO_SM4 +#include +#endif static inline bool qcrypto_length_check(size_t len, size_t blocksize, Error **errp) @@ -45,6 +48,30 @@ static inline bool qcrypto_length_check(size_t len, size_t blocksize, return true; } +#ifdef CONFIG_CRYPTO_SM4 +typedef struct QCryptoNettleSm4 { + QCryptoCipher base; + struct sm4_ctx key[2]; +} QCryptoNettleSm4; + +static void sm4_encrypt_native(void *ctx, size_t length, + uint8_t *dst, const uint8_t *src) +{ + struct sm4_ctx *keys = ctx; + sm4_crypt(&keys[0], length, dst, src); +} + +static void sm4_decrypt_native(void *ctx, size_t length, + uint8_t *dst, const uint8_t *src) +{ + struct sm4_ctx *keys = ctx; + sm4_crypt(&keys[1], length, dst, src); +} + +DEFINE_ECB(qcrypto_nettle_sm4, + QCryptoNettleSm4, SM4_BLOCK_SIZE, + sm4_encrypt_native, sm4_decrypt_native) +#endif static void qcrypto_cipher_ctx_free(QCryptoCipher *ctx) { @@ -443,6 +470,9 @@ bool qcrypto_cipher_supports(QCryptoCipherAlgorithm alg, case QCRYPTO_CIPHER_ALG_TWOFISH_128: case QCRYPTO_CIPHER_ALG_TWOFISH_192: case QCRYPTO_CIPHER_ALG_TWOFISH_256: +#ifdef CONFIG_CRYPTO_SM4 + case QCRYPTO_CIPHER_ALG_SM4: +#endif break; default: return false; @@ -701,6 +731,25 @@ static QCryptoCipher *qcrypto_cipher_ctx_new(QCryptoCipherAlgorithm alg, return &ctx->base; } +#ifdef CONFIG_CRYPTO_SM4 + case QCRYPTO_CIPHER_ALG_SM4: + { + QCryptoNettleSm4 *ctx = g_new0(QCryptoNettleSm4, 1); + + switch (mode) { + case QCRYPTO_CIPHER_MODE_ECB: + ctx->base.driver = &qcrypto_nettle_sm4_driver_ecb; + break; + default: + goto bad_cipher_mode; + } + + sm4_set_encrypt_key(&ctx->key[0], key); + sm4_set_decrypt_key(&ctx->key[1], key); + + return &ctx->base; + } +#endif default: error_setg(errp, "Unsupported cipher algorithm %s", diff --git a/crypto/cipher.c b/crypto/cipher.c index 74b09a5b26..425e9cca83 100644 --- a/crypto/cipher.c +++ b/crypto/cipher.c @@ -38,6 +38,9 @@ static const size_t alg_key_len[QCRYPTO_CIPHER_ALG__MAX] = { [QCRYPTO_CIPHER_ALG_TWOFISH_128] = 16, [QCRYPTO_CIPHER_ALG_TWOFISH_192] = 24, [QCRYPTO_CIPHER_ALG_TWOFISH_256] = 32, +#ifdef CONFIG_CRYPTO_SM4 + [QCRYPTO_CIPHER_ALG_SM4] = 16, +#endif }; static const size_t alg_block_len[QCRYPTO_CIPHER_ALG__MAX] = { @@ -53,6 +56,9 @@ static const size_t alg_block_len[QCRYPTO_CIPHER_ALG__MAX] = { [QCRYPTO_CIPHER_ALG_TWOFISH_128] = 16, [QCRYPTO_CIPHER_ALG_TWOFISH_192] = 16, [QCRYPTO_CIPHER_ALG_TWOFISH_256] = 16, +#ifdef CONFIG_CRYPTO_SM4 + [QCRYPTO_CIPHER_ALG_SM4] = 16, +#endif }; static const bool mode_need_iv[QCRYPTO_CIPHER_MODE__MAX] = { diff --git a/crypto/hash-gcrypt.c b/crypto/hash-gcrypt.c index 829e48258d..d3bdfe5633 100644 --- a/crypto/hash-gcrypt.c +++ b/crypto/hash-gcrypt.c @@ -33,6 +33,9 @@ static int qcrypto_hash_alg_map[QCRYPTO_HASH_ALG__MAX] = { [QCRYPTO_HASH_ALG_SHA384] = GCRY_MD_SHA384, [QCRYPTO_HASH_ALG_SHA512] = GCRY_MD_SHA512, [QCRYPTO_HASH_ALG_RIPEMD160] = GCRY_MD_RMD160, +#ifdef CONFIG_CRYPTO_SM3 + [QCRYPTO_HASH_ALG_SM3] = GCRY_MD_SM3, +#endif }; gboolean qcrypto_hash_supports(QCryptoHashAlgorithm alg) diff --git a/crypto/hash.c b/crypto/hash.c index b0f8228bdc..3613a5bcaa 100644 --- a/crypto/hash.c +++ b/crypto/hash.c @@ -30,6 +30,9 @@ static size_t qcrypto_hash_alg_size[QCRYPTO_HASH_ALG__MAX] = { [QCRYPTO_HASH_ALG_SHA384] = 48, [QCRYPTO_HASH_ALG_SHA512] = 64, [QCRYPTO_HASH_ALG_RIPEMD160] = 20, +#ifdef CONFIG_CRYPTO_SM4 + [QCRYPTO_HASH_ALG_SM3] = 32, +#endif }; size_t qcrypto_hash_digest_len(QCryptoHashAlgorithm alg) diff --git a/crypto/hmac-gcrypt.c b/crypto/hmac-gcrypt.c index 0c6f979711..888afb86ed 100644 --- a/crypto/hmac-gcrypt.c +++ b/crypto/hmac-gcrypt.c @@ -26,6 +26,9 @@ static int qcrypto_hmac_alg_map[QCRYPTO_HASH_ALG__MAX] = { [QCRYPTO_HASH_ALG_SHA384] = GCRY_MAC_HMAC_SHA384, [QCRYPTO_HASH_ALG_SHA512] = GCRY_MAC_HMAC_SHA512, [QCRYPTO_HASH_ALG_RIPEMD160] = GCRY_MAC_HMAC_RMD160, +#ifdef CONFIG_CRYPTO_SM3 + [QCRYPTO_HASH_ALG_SM3] = GCRY_MAC_HMAC_SM3, +#endif }; typedef struct QCryptoHmacGcrypt QCryptoHmacGcrypt; diff --git a/crypto/pbkdf-gcrypt.c b/crypto/pbkdf-gcrypt.c index a8d8e64f4d..f90df262bb 100644 --- a/crypto/pbkdf-gcrypt.c +++ b/crypto/pbkdf-gcrypt.c @@ -33,7 +33,10 @@ bool qcrypto_pbkdf2_supports(QCryptoHashAlgorithm hash) case QCRYPTO_HASH_ALG_SHA384: case QCRYPTO_HASH_ALG_SHA512: case QCRYPTO_HASH_ALG_RIPEMD160: +#ifdef CONFIG_CRYPTO_SM3 + case QCRYPTO_HASH_ALG_SM3: return true; +#endif default: return false; } @@ -54,6 +57,9 @@ int qcrypto_pbkdf2(QCryptoHashAlgorithm hash, [QCRYPTO_HASH_ALG_SHA384] = GCRY_MD_SHA384, [QCRYPTO_HASH_ALG_SHA512] = GCRY_MD_SHA512, [QCRYPTO_HASH_ALG_RIPEMD160] = GCRY_MD_RMD160, +#ifdef CONFIG_CRYPTO_SM3 + [QCRYPTO_HASH_ALG_SM3] = GCRY_MD_SM3, +#endif }; int ret; diff --git a/meson.build b/meson.build index 45bc69bf0c..7675bdcb81 100644 --- a/meson.build +++ b/meson.build @@ -1010,6 +1010,8 @@ endif # gcrypt over nettle for performance reasons. gcrypt = not_found nettle = not_found +crypto_sm4 = not_found +crypto_sm3 = not_found xts = 'none' if get_option('nettle').enabled() and get_option('gcrypt').enabled() @@ -1035,6 +1037,28 @@ if not gnutls_crypto.found() gcrypt, cc.find_library('gpg-error', required: true, kwargs: static_kwargs)]) endif + crypto_sm4 = gcrypt + # SM4 ALG is available in libgcrypt >= 1.9 + if gcrypt.found() and not cc.links(''' + #include + int main(void) { + gcry_cipher_hd_t handler; + gcry_cipher_open(&handler, GCRY_CIPHER_SM4, GCRY_CIPHER_MODE_ECB, 0); + return 0; + }''', dependencies: gcrypt) + crypto_sm4 = not_found + endif + crypto_sm3 = gcrypt + # SM3 ALG is available in libgcrypt >= 1.8 + if gcrypt.found() and not cc.links(''' + #include + int main(void) { + gcry_md_hd_t handler; + gcry_md_open(&handler, GCRY_MD_SM3, 0); + return 0; + }''', dependencies: gcrypt) + crypto_sm3 = not_found + endif endif if (not get_option('nettle').auto() or have_system) and not gcrypt.found() nettle = dependency('nettle', version: '>=3.4', @@ -1044,6 +1068,32 @@ if not gnutls_crypto.found() if nettle.found() and not cc.has_header('nettle/xts.h', dependencies: nettle) xts = 'private' endif + crypto_sm4 = nettle + # SM4 ALG is available in nettle >= 3.9 + if nettle.found() and not cc.links(''' + #include + int main(void) { + struct sm4_ctx ctx; + unsigned char key[16] = {0}; + sm4_set_encrypt_key(&ctx, key); + return 0; + }''', dependencies: nettle) + crypto_sm4 = not_found + endif + crypto_sm3 = nettle + # SM3 ALG is available in nettle >= 3.4 + if nettle.found() and not cc.links(''' + #include + int main(void) { + struct sm3_ctx ctx; + unsigned char data[64] = {0}; + sm3_init(&ctx); + sm3_update(&ctx, 64, data); + sm3_digest(&ctx, 32, data); + return 0; + }''', dependencies: nettle) + crypto_sm3 = not_found + endif endif endif @@ -1487,6 +1537,8 @@ config_host_data.set('CONFIG_GNUTLS', gnutls.found()) config_host_data.set('CONFIG_GNUTLS_CRYPTO', gnutls_crypto.found()) config_host_data.set('CONFIG_GCRYPT', gcrypt.found()) config_host_data.set('CONFIG_NETTLE', nettle.found()) +config_host_data.set('CONFIG_CRYPTO_SM4', crypto_sm4.found()) +config_host_data.set('CONFIG_CRYPTO_SM3', crypto_sm3.found()) config_host_data.set('CONFIG_QEMU_PRIVATE_XTS', xts == 'private') config_host_data.set('CONFIG_MALLOC_TRIM', has_malloc_trim) config_host_data.set('CONFIG_STATX', has_statx) @@ -3432,6 +3484,8 @@ summary_info += {'nettle': nettle} if nettle.found() summary_info += {' XTS': xts != 'private'} endif +summary_info += {'SM4 ALG support': crypto_sm4} +summary_info += {'SM3 ALG support': crypto_sm3} summary_info += {'crypto afalg': config_host.has_key('CONFIG_AF_ALG')} summary_info += {'rng-none': config_host.has_key('CONFIG_RNG_NONE')} summary_info += {'Linux keyring': config_host.has_key('CONFIG_SECRET_KEYRING')} diff --git a/qapi/crypto.json b/qapi/crypto.json index 1ec54c15ca..e90ab3c22e 100644 --- a/qapi/crypto.json +++ b/qapi/crypto.json @@ -55,7 +55,7 @@ ## { 'enum': 'QCryptoHashAlgorithm', 'prefix': 'QCRYPTO_HASH_ALG', - 'data': ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'ripemd160']} + 'data': ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'ripemd160', 'sm3']} ## @@ -83,8 +83,8 @@ 'des', '3des', 'cast5-128', 'serpent-128', 'serpent-192', 'serpent-256', - 'twofish-128', 'twofish-192', 'twofish-256']} - + 'twofish-128', 'twofish-192', 'twofish-256', + 'sm4']} ## # @QCryptoCipherMode: diff --git a/tests/test_encvm/create_domain_xml.sh b/tests/test_encvm/create_domain_xml.sh new file mode 100644 index 0000000000..3275b64dcb --- /dev/null +++ b/tests/test_encvm/create_domain_xml.sh @@ -0,0 +1,22 @@ +#!/bin/bash + + +# 检查是否提供了足够的参数 +if [ "$#" -ne 2 ]; then + echo "Usage: $0 UUID VOLUME_PATH" + exit 1 +fi + +# 读取参数 +UUID=$1 +VOLUME=$2 + +# 指定模板文件和输出文件 +TEMPLATE_FILE="vm_kvm.xml" +OUTPUT_FILE="domain_${UUID}.xml" + +# 读取模板文件并替换占位符,然后写入到新的文件 +sed "s|\${uuid}|${UUID}|g; s|\${volume}|${VOLUME}|g" $TEMPLATE_FILE > $OUTPUT_FILE + +echo "Output written to $OUTPUT_FILE" + diff --git a/tests/test_encvm/create_secret_xml.sh b/tests/test_encvm/create_secret_xml.sh new file mode 100644 index 0000000000..ad2d8bd8e7 --- /dev/null +++ b/tests/test_encvm/create_secret_xml.sh @@ -0,0 +1,22 @@ +#!/bin/bash + + +# 检查是否提供了足够的参数 +if [ "$#" -ne 2 ]; then + echo "Usage: $0 UUID VOLUME_PATH" + exit 1 +fi + +# 读取参数 +UUID=$1 +VOLUME=$2 + +# 指定模板文件和输出文件 +TEMPLATE_FILE="secret.xml" +OUTPUT_FILE="secxml_${UUID}.xml" + +# 读取模板文件并替换占位符,然后写入到新的文件 +sed "s|\${uuid}|${UUID}|g; s|\${volume}|${VOLUME}|g" $TEMPLATE_FILE > $OUTPUT_FILE + +echo "Output written to $OUTPUT_FILE" + diff --git a/tests/test_encvm/createsecvm.sh b/tests/test_encvm/createsecvm.sh new file mode 100644 index 0000000000..c5bb16b16c --- /dev/null +++ b/tests/test_encvm/createsecvm.sh @@ -0,0 +1,33 @@ +# 检查是否提供了足够的参数 +if [ "$#" -ne 2 ]; then + echo "Usage: $0 UUID VOLUME_PATH" + exit 1 +fi + +# 读取参数 +uuid=$1 +volume=$2 +echo ${uuid} + +if [ ! -f ${volume} ];then + echo "not exist ${volume}!\n" + exit 1 +fi + +sec_volume="${volume%.qcow2}_enc.qcow2" +sh -x ./create_secret_xml.sh ${uuid} ${sec_volume} +secuuid=$(virsh secret-list |grep ${sec_volume}|cut -d " " -f2) + +if [ ! -z ${secuuid} ];then + virsh secret-undefine ${secuuid} +fi +virsh secret-define secxml_${uuid}.xml +sh -x secret_set.sh ${uuid} +qemu-img convert -O qcow2 --object secret,id=sec0,data=${uuid} -o encrypt.format=luks,encrypt.key-secret=sec0,encrypt.cipher-alg=sm4,encrypt.cipher-mode=xts,encrypt.ivgen-alg=plain64,encrypt.hash-alg=sm3 ${volume} ${sec_volume} +if [ $? != 0 ];then + echo "qemu-img convert sec img error!\n" + exit 1 +fi +sh -x ./create_domain_xml.sh ${uuid} ${sec_volume} +virsh define domain_${uuid}.xml +virsh start ${uuid} diff --git a/tests/test_encvm/secret.xml b/tests/test_encvm/secret.xml new file mode 100644 index 0000000000..e205e53c41 --- /dev/null +++ b/tests/test_encvm/secret.xml @@ -0,0 +1,6 @@ + + ${uuid} + + ${volume} + + diff --git a/tests/test_encvm/secret_set.sh b/tests/test_encvm/secret_set.sh new file mode 100644 index 0000000000..ff5940fd3a --- /dev/null +++ b/tests/test_encvm/secret_set.sh @@ -0,0 +1,11 @@ +#!/bin/sh +if [ "$#" -ne 1 ]; then + echo "Usage: $0 UUID" + exit 1 +fi + +# 读取参数 +uuid=$1 +seuuid=`echo -n ${uuid} |base64` +echo ${seuuid} >/tmp/${uuid} +virsh secret-set-value ${uuid} --file /tmp/${uuid} diff --git a/tests/test_encvm/template.xml b/tests/test_encvm/template.xml new file mode 100644 index 0000000000..c63983c319 --- /dev/null +++ b/tests/test_encvm/template.xml @@ -0,0 +1,119 @@ + + + ${uuid} + ${uuid} + 524288 + 16777216 + 16 + + /machine + + + hvm + + + + + + + + qemu64 + + + + + + + destroy + restart + destroy + + + + + + /usr/bin/qemu-system-x86_64 + + + + + + + + + +
+ + + + + +
+ + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + + + +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +