From 8857b380e21b7140508cbcdd57abbcafdc658463 Mon Sep 17 00:00:00 2001 From: "Dr. Stephen Henson" Date: Wed, 9 Mar 2011 23:44:06 +0000 Subject: [PATCH] Add ECDH to validated module. --- CHANGES | 3 + Makefile.fips | 6 +- Makefile.org | 2 + crypto/ecdh/ech_key.c | 2 + crypto/ecdh/ech_ossl.c | 13 ++ fips/Makefile | 2 +- fips/ecdh/Makefile | 78 ++++++++ fips/ecdh/fips_ecdhvs.c | 418 ++++++++++++++++++++++++++++++++++++++++ fips/fips.h | 1 + test/Makefile | 11 +- 10 files changed, 530 insertions(+), 6 deletions(-) create mode 100644 fips/ecdh/Makefile create mode 100644 fips/ecdh/fips_ecdhvs.c diff --git a/CHANGES b/CHANGES index 3690126846..e39a923fbe 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,9 @@ Changes between 1.0.1 and 1.1.0 [xx XXX xxxx] + *) Add ECDH code to fips module and fips_ecdhvs for primitives only testing. + [Steve Henson] + *) New algorithm test program fips_dhvs to handle DH primitives only testing. [Steve Henson] diff --git a/Makefile.fips b/Makefile.fips index 8a26206e5c..5bc8e92483 100644 --- a/Makefile.fips +++ b/Makefile.fips @@ -148,13 +148,13 @@ SHLIBDIRS= crypto SDIRS= \ sha hmac des aes modes \ bn ec rsa dsa ecdsa dh \ - buffer rand evp # ecdh cmac + buffer rand evp ecdh # cmac # keep in mind that the above list is adjusted by ./Configure # according to no-xxx arguments... LINKDIRS= \ objects sha hmac des aes modes \ - bn ec rsa dsa ecdsa dh ecdh engine \ + bn ec rsa dsa ecdh ecdsa dh engine \ buffer bio stack lhash rand err \ evp asn1 ui cmac @@ -325,6 +325,8 @@ FIPS_EX_OBJ= ../crypto/aes/aes_cfb.o \ ../crypto/ec/ecp_smpl.o \ ../crypto/ec/ec2_mult.o \ ../crypto/ec/ec2_smpl.o \ + ../crypto/ecdh/ech_key.o \ + ../crypto/ecdh/ech_ossl.o \ ../crypto/ecdsa/ecs_ossl.o \ ../crypto/evp/e_aes.o \ ../crypto/evp/e_des3.o \ diff --git a/Makefile.org b/Makefile.org index 80c126d8de..00ccc32b73 100644 --- a/Makefile.org +++ b/Makefile.org @@ -319,6 +319,8 @@ FIPS_EX_OBJ= ../crypto/aes/aes_cfb.o \ ../crypto/ec/ecp_smpl.o \ ../crypto/ec/ec2_mult.o \ ../crypto/ec/ec2_smpl.o \ + ../crypto/ecdh/ech_key.o \ + ../crypto/ecdh/ech_ossl.o \ ../crypto/ecdsa/ecs_ossl.o \ ../crypto/evp/e_aes.o \ ../crypto/evp/e_des3.o \ diff --git a/crypto/ecdh/ech_key.c b/crypto/ecdh/ech_key.c index f44da9298b..52a4649dc6 100644 --- a/crypto/ecdh/ech_key.c +++ b/crypto/ecdh/ech_key.c @@ -67,6 +67,8 @@ * */ +#define OPENSSL_FIPSAPI + #include "ech_locl.h" #ifndef OPENSSL_NO_ENGINE #include diff --git a/crypto/ecdh/ech_ossl.c b/crypto/ecdh/ech_ossl.c index 4a30628fbc..ceaa2f06b6 100644 --- a/crypto/ecdh/ech_ossl.c +++ b/crypto/ecdh/ech_ossl.c @@ -67,6 +67,7 @@ * */ +#define OPENSSL_FIPSAPI #include #include @@ -213,3 +214,15 @@ err: if (buf) OPENSSL_free(buf); return(ret); } + +#ifdef OPENSSL_FIPSCANISTER +/* FIPS stanadlone version of ecdh_check: just return FIPS method */ +ECDH_DATA *fips_ecdh_check(EC_KEY *key) + { + static ECDH_DATA rv = { + 0,0,0, + &openssl_ecdh_meth + }; + return &rv; + } +#endif diff --git a/fips/Makefile b/fips/Makefile index 20383e13f1..1373fdaf97 100644 --- a/fips/Makefile +++ b/fips/Makefile @@ -35,7 +35,7 @@ AFLAGS=$(ASFLAGS) LIBS= -FDIRS=sha rand des aes dsa ecdsa rsa dh hmac utl +FDIRS=sha rand des aes dsa ecdh ecdsa rsa dh hmac utl GENERAL=Makefile README fips-lib.com install.com diff --git a/fips/ecdh/Makefile b/fips/ecdh/Makefile new file mode 100644 index 0000000000..b588ea46af --- /dev/null +++ b/fips/ecdh/Makefile @@ -0,0 +1,78 @@ +# +# OpenSSL/fips/ecdh/Makefile +# + +DIR= ecdh +TOP= ../.. +CC= cc +INCLUDES= +CFLAG=-g +INSTALL_PREFIX= +OPENSSLDIR= /usr/local/ssl +INSTALLTOP=/usr/local/ssl +MAKEDEPPROG= makedepend +MAKEDEPEND= $(TOP)/util/domd $(TOP) -MD $(MAKEDEPPROG) +MAKEFILE= Makefile +AR= ar r + +CFLAGS= $(INCLUDES) $(CFLAG) + +GENERAL=Makefile +TEST= fips_ecdhvs.c +APPS= + +LIB=$(TOP)/libcrypto.a +LIBSRC= +LIBOBJ= + +SRC= $(LIBSRC) + +EXHEADER= +HEADER= $(EXHEADER) + +ALL= $(GENERAL) $(SRC) $(HEADER) + +top: + (cd $(TOP); $(MAKE) DIRS=fips FDIRS=$(DIR) sub_all) + +all: lib + +lib: $(LIBOBJ) + @echo $(LIBOBJ) > lib + +files: + $(PERL) $(TOP)/util/files.pl Makefile >> $(TOP)/MINFO + +links: + @$(PERL) $(TOP)/util/mklink.pl $(TOP)/include/openssl $(EXHEADER) + @$(PERL) $(TOP)/util/mklink.pl $(TOP)/test $(TEST) + @$(PERL) $(TOP)/util/mklink.pl $(TOP)/apps $(APPS) + +install: + @headerlist="$(EXHEADER)"; for i in $$headerlist; \ + do \ + (cp $$i $(INSTALL_PREFIX)$(INSTALLTOP)/include/openssl/$$i; \ + chmod 644 $(INSTALL_PREFIX)$(INSTALLTOP)/include/openssl/$$i ); \ + done + +tags: + ctags $(SRC) + +tests: + +fips_test: + +lint: + lint -DLINT $(INCLUDES) $(SRC)>fluff + +depend: + $(MAKEDEPEND) -- $(CFLAG) $(INCLUDES) $(DEPFLAG) -- $(SRC) $(TEST) + +dclean: + $(PERL) -pe 'if (/^# DO NOT DELETE THIS LINE/) {print; exit(0);}' $(MAKEFILE) >Makefile.new + mv -f Makefile.new $(MAKEFILE) + +clean: + rm -f *.o *.obj lib tags core .pure .nfs* *.old *.bak fluff +# DO NOT DELETE THIS LINE -- make depend depends on it. + diff --git a/fips/ecdh/fips_ecdhvs.c b/fips/ecdh/fips_ecdhvs.c new file mode 100644 index 0000000000..3182f6e4ce --- /dev/null +++ b/fips/ecdh/fips_ecdhvs.c @@ -0,0 +1,418 @@ +/* fips/ecdh/fips_ecdhvs.c */ +/* Written by Dr Stephen N Henson (steve@openssl.org) for the OpenSSL + * project. + */ +/* ==================================================================== + * Copyright (c) 2011 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.OpenSSL.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * licensing@OpenSSL.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.OpenSSL.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + */ + + +#define OPENSSL_FIPSAPI +#include + +#ifndef OPENSSL_FIPS +#include + +int main(int argc, char **argv) +{ + printf("No FIPS ECDH support\n"); + return(0); +} +#else + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fips_utl.h" + +static const EVP_MD *parse_md(char *line) + { + char *p; + if (line[0] != '[' || line[1] != 'E') + return NULL; + p = strchr(line, '-'); + if (!p) + return NULL; + line = p + 1; + p = strchr(line, ']'); + if (!p) + return NULL; + *p = 0; + p = line; + while(isspace(*p)) + p++; + if (!strcmp(p, "SHA1")) + return EVP_sha1(); + else if (!strcmp(p, "SHA224")) + return EVP_sha224(); + else if (!strcmp(p, "SHA256")) + return EVP_sha256(); + else if (!strcmp(p, "SHA384")) + return EVP_sha384(); + else if (!strcmp(p, "SHA512")) + return EVP_sha512(); + else + return NULL; + } + +static int lookup_curve(char *cname) + { + char *p; + p = strchr(cname, ':'); + if (!p) + { + fprintf(stderr, "Parse error: missing :\n"); + return NID_undef; + } + cname = p + 1; + while(isspace(*cname)) + cname++; + p = strchr(cname, ']'); + if (!p) + { + fprintf(stderr, "Parse error: missing ]\n"); + return NID_undef; + } + *p = 0; + + if (!strcmp(cname, "B-163")) + return NID_sect163r2; + if (!strcmp(cname, "B-233")) + return NID_sect233r1; + if (!strcmp(cname, "B-283")) + return NID_sect283r1; + if (!strcmp(cname, "B-409")) + return NID_sect409r1; + if (!strcmp(cname, "B-571")) + return NID_sect571r1; + if (!strcmp(cname, "K-163")) + return NID_sect163k1; + if (!strcmp(cname, "K-233")) + return NID_sect233k1; + if (!strcmp(cname, "K-283")) + return NID_sect283k1; + if (!strcmp(cname, "K-409")) + return NID_sect409k1; + if (!strcmp(cname, "K-571")) + return NID_sect571k1; + if (!strcmp(cname, "P-192")) + return NID_X9_62_prime192v1; + if (!strcmp(cname, "P-224")) + return NID_secp224r1; + if (!strcmp(cname, "P-256")) + return NID_X9_62_prime256v1; + if (!strcmp(cname, "P-384")) + return NID_secp384r1; + if (!strcmp(cname, "P-521")) + return NID_secp521r1; + + fprintf(stderr, "Unknown Curve name %s\n", cname); + return NID_undef; + } + +static EC_POINT *make_peer(EC_GROUP *group, BIGNUM *x, BIGNUM *y) + { + EC_POINT *peer; + int rv; + BN_CTX *c; + peer = EC_POINT_new(group); + if (!peer) + return NULL; + c = BN_CTX_new(); + if (EC_METHOD_get_field_type(EC_GROUP_method_of(group)) + == NID_X9_62_characteristic_two_field) + rv = EC_POINT_set_affine_coordinates_GF2m(group, peer, x, y, c); + else + rv = EC_POINT_set_affine_coordinates_GFp(group, peer, x, y, c); + + BN_CTX_free(c); + if (rv) + return peer; + EC_POINT_free(peer); + return NULL; + } + +static int ec_print_pubkey(FILE *out, EC_KEY *key) + { + const EC_POINT *pt; + const EC_GROUP *grp; + const EC_METHOD *meth; + int rv; + BIGNUM *tx, *ty; + BN_CTX *ctx; + ctx = BN_CTX_new(); + if (!ctx) + return 0; + tx = BN_CTX_get(ctx); + ty = BN_CTX_get(ctx); + if (!tx || !ty) + return 0; + grp = EC_KEY_get0_group(key); + pt = EC_KEY_get0_public_key(key); + meth = EC_GROUP_method_of(grp); + if (EC_METHOD_get_field_type(meth) == NID_X9_62_prime_field) + rv = EC_POINT_get_affine_coordinates_GFp(grp, pt, tx, ty, ctx); + else + rv = EC_POINT_get_affine_coordinates_GF2m(grp, pt, tx, ty, ctx); + + do_bn_print_name(out, "QeIUTx", tx); + do_bn_print_name(out, "QeIUTy", ty); + + BN_CTX_free(ctx); + + return rv; + + } + +static void ec_output_Zhash(FILE *out, int exout, EC_GROUP *group, + BIGNUM *ix, BIGNUM *iy, BIGNUM *id, BIGNUM *cx, + BIGNUM *cy, const EVP_MD *md, + unsigned char *rhash, size_t rhashlen) + { + EC_KEY *ec = NULL; + EC_POINT *peerkey = NULL; + unsigned char *Z; + unsigned char chash[EVP_MAX_MD_SIZE]; + int Zlen; + ec = EC_KEY_new(); + EC_KEY_set_group(ec, group); + peerkey = make_peer(group, cx, cy); + if (rhash == NULL) + { + rhashlen = M_EVP_MD_size(md); + EC_KEY_generate_key(ec); + ec_print_pubkey(out, ec); + } + else + { + EC_KEY_set_public_key_affine_coordinates(ec, ix, iy); + EC_KEY_set_private_key(ec, id); + } + Zlen = (EC_GROUP_get_degree(group) + 7)/8; + Z = OPENSSL_malloc(Zlen); + if (!Z) + exit(1); + ECDH_compute_key(Z, Zlen, peerkey, ec, 0); + if (exout) + OutputValue("Z", Z, Zlen, out, 0); + FIPS_digest(Z, Zlen, chash, NULL, md); + OutputValue(rhash ? "IUTHashZZ" : "HashZZ", chash, rhashlen, out, 0); + if (rhash) + { + fprintf(out, "Result = %s\n", + memcmp(chash, rhash, rhashlen) ? "F" : "P"); + } + OPENSSL_cleanse(Z, Zlen); + OPENSSL_free(Z); + EC_KEY_free(ec); + EC_POINT_free(peerkey); + } + +int main(int argc,char **argv) + { + char **args = argv + 1; + int argn = argc - 1; + FILE *in, *out; + char buf[2048], lbuf[2048]; + unsigned char *rhash; + long rhashlen; + BIGNUM *cx = NULL, *cy = NULL; + BIGNUM *id = NULL, *ix = NULL, *iy = NULL; + const EVP_MD *md = NULL; + EC_GROUP *group = NULL; + char *keyword = NULL, *value = NULL; + int do_verify = -1, exout = 0; + + int curve_nids[5] = {0,0,0,0,0}; + int param_set = -1; + + fips_set_error_print(); + if(!FIPS_mode_set(1)) + exit(1); + + if (argn && !strcmp(*args, "ecdhver")) + { + do_verify = 1; + args++; + argn--; + } + else if (argn && !strcmp(*args, "ecdhgen")) + { + do_verify = 0; + args++; + argn--; + } + + if (argn && !strcmp(*args, "-exout")) + { + exout = 1; + args++; + argn--; + } + + if (do_verify == -1) + { + fprintf(stderr,"%s [ecdhver|ecdhgen|] [-exout] (infile outfile)\n",argv[0]); + exit(1); + } + + if (argn == 2) + { + in = fopen(*args, "r"); + if (!in) + { + fprintf(stderr, "Error opening input file\n"); + exit(1); + } + out = fopen(args[1], "w"); + if (!out) + { + fprintf(stderr, "Error opening output file\n"); + exit(1); + } + } + else if (argn == 0) + { + in = stdin; + out = stdout; + } + else + { + fprintf(stderr,"%s [dhver|dhgen|] [-exout] (infile outfile)\n",argv[0]); + exit(1); + } + + while (fgets(buf, sizeof(buf), in) != NULL) + { + fputs(buf, out); + if (buf[0] == '[' && buf[1] == 'E') + { + int c = buf[2]; + if (c < 'A' || c > 'E') + goto parse_error; + param_set = c - 'A'; + /* If just [E?] then initial paramset */ + if (buf[3] == ']') + continue; + if (group) + EC_GROUP_free(group); + group = EC_GROUP_new_by_curve_name(curve_nids[c - 'A']); + } + if (strlen(buf) > 10 && !strncmp(buf, "[Curve", 6)) + { + int nid; + if (param_set == -1) + goto parse_error; + nid = lookup_curve(buf); + if (nid == NID_undef) + goto parse_error; + curve_nids[param_set] = nid; + } + + if (strlen(buf) > 6 && !strncmp(buf, "[E", 2)) + { + md = parse_md(buf); + if (md == NULL) + goto parse_error; + continue; + } + if (!parse_line(&keyword, &value, lbuf, buf)) + continue; + if (!strcmp(keyword, "QeCAVSx")) + { + if (!do_hex2bn(&cx, value)) + goto parse_error; + } + else if (!strcmp(keyword, "QeCAVSy")) + { + if (!do_hex2bn(&cy, value)) + goto parse_error; + if (do_verify == 0) + ec_output_Zhash(out, exout, group, + NULL, NULL, NULL, + cx, cy, md, rhash, rhashlen); + } + else if (!strcmp(keyword, "deIUT")) + { + if (!do_hex2bn(&id, value)) + goto parse_error; + } + else if (!strcmp(keyword, "QeIUTx")) + { + if (!do_hex2bn(&ix, value)) + goto parse_error; + } + else if (!strcmp(keyword, "QeIUTy")) + { + if (!do_hex2bn(&iy, value)) + goto parse_error; + } + else if (!strcmp(keyword, "CAVSHashZZ")) + { + if (!md) + goto parse_error; + rhash = hex2bin_m(value, &rhashlen); + if (!rhash || rhashlen != M_EVP_MD_size(md)) + goto parse_error; + ec_output_Zhash(out, exout, group, ix, iy, id, cx, cy, + md, rhash, rhashlen); + } + } + return 0; + parse_error: + fprintf(stderr, "Error Parsing request file\n"); + exit(1); + } + +#endif diff --git a/fips/fips.h b/fips/fips.h index 3fc7a5e40f..fa4f68087d 100644 --- a/fips/fips.h +++ b/fips/fips.h @@ -165,6 +165,7 @@ void FIPS_set_locking_callbacks(void (*func)(int mode, int type, #define ECDSA_SIG_free FIPS_ecdsa_sig_free #define ecdsa_check fips_ecdsa_check +#define ecdh_check fips_ecdh_check #endif diff --git a/test/Makefile b/test/Makefile index b5aedfd7ed..5ca47542d7 100644 --- a/test/Makefile +++ b/test/Makefile @@ -76,6 +76,7 @@ FIPS_DSSVS= fips_dssvs FIPS_RNGVS= fips_rngvs FIPS_DRBGVS= fips_drbgvs FIPS_DHVS= fips_dhvs +FIPS_ECDHVS= fips_ecdhvs FIPS_ECDSAVS= fips_ecdsavs FIPS_TEST_SUITE=fips_test_suite @@ -98,7 +99,8 @@ FIPSEXE=$(FIPS_SHATEST)$(EXE_EXT) $(FIPS_DESTEST)$(EXE_EXT) \ $(FIPS_DSSVS)$(EXE_EXT) $(FIPS_DSATEST)$(EXE_EXT) \ $(FIPS_RNGVS)$(EXE_EXT) $(FIPS_DRBGVS)$(EXE_EXT) \ $(FIPS_DHVS)$(EXE_EXT) $(FIPS_TEST_SUITE)$(EXE_EXT) \ - $(FIPS_GCMTEST)$(EXE_EXT) $(FIPS_ECDSAVS)$(EXE_EXT) + $(FIPS_GCMTEST)$(EXE_EXT) $(FIPS_ECDSAVS)$(EXE_EXT) \ + $(FIPS_ECDHVS)$(EXE_EXT) # $(METHTEST)$(EXE_EXT) @@ -115,7 +117,7 @@ OBJ= $(BNTEST).o $(ECTEST).o $(ECDSATEST).o $(ECDHTEST).o $(IDEATEST).o \ $(FIPS_RSASTEST).o $(FIPS_RSAGTEST).o $(FIPS_GCMTEST).o \ $(FIPS_DSSVS).o $(FIPS_DSATEST).o $(FIPS_RNGVS).o $(FIPS_DRBGVS).o \ $(FIPS_TEST_SUITE).o $(FIPS_DHVS).o $(FIPS_ECDSAVS).o \ - $(EVPTEST).o $(IGETEST).o $(JPAKETEST).o + $(FIPS_ECDHVS).o $(EVPTEST).o $(IGETEST).o $(JPAKETEST).o SRC= $(BNTEST).c $(ECTEST).c $(ECDSATEST).c $(ECDHTEST).c $(IDEATEST).c \ $(MD2TEST).c $(MD4TEST).c $(MD5TEST).c \ $(HMACTEST).c $(WPTEST).c \ @@ -128,7 +130,7 @@ SRC= $(BNTEST).c $(ECTEST).c $(ECDSATEST).c $(ECDHTEST).c $(IDEATEST).c \ $(FIPS_RSASTEST).c $(FIPS_RSAGTEST).c $(FIPS_GCMTEST).c \ $(FIPS_DSSVS).c $(FIPS_DSATEST).c $(FIPS_RNGVS).c $(FIPS_DRBGVS).c \ $(FIPS_TEST_SUITE).c $(FIPS_DHVS).c $(FIPS_ECDSAVS).c \ - $(EVPTEST).c $(IGETEST).c $(JPAKETEST).c + $(FIPS_ECDHVS).c $(EVPTEST).c $(IGETEST).c $(JPAKETEST).c EXHEADER= HEADER= $(EXHEADER) @@ -476,6 +478,9 @@ $(FIPS_DSSVS)$(EXE_EXT): $(FIPS_DSSVS).o $(DLIBCRYPTO) $(FIPS_DHVS)$(EXE_EXT): $(FIPS_DHVS).o $(DLIBCRYPTO) @target=$(FIPS_DHVS); $(FIPS_BUILD_CMD) +$(FIPS_ECDHVS)$(EXE_EXT): $(FIPS_ECDHVS).o $(DLIBCRYPTO) + @target=$(FIPS_ECDHVS); $(FIPS_BUILD_CMD) + $(FIPS_ECDSAVS)$(EXE_EXT): $(FIPS_ECDSAVS).o $(DLIBCRYPTO) @target=$(FIPS_ECDSAVS); $(FIPS_BUILD_CMD) -- GitLab