diff --git a/docker/src/lemur.conf.py b/docker/src/lemur.conf.py index 3cc51792..4cb3ae0c 100644 --- a/docker/src/lemur.conf.py +++ b/docker/src/lemur.conf.py @@ -1,4 +1,7 @@ import os +import random +import string +import base64 from ast import literal_eval _basedir = os.path.abspath(os.path.dirname(__file__)) @@ -6,10 +9,20 @@ _basedir = os.path.abspath(os.path.dirname(__file__)) CORS = os.environ.get("CORS") == "True" debug = os.environ.get("DEBUG") == "True" -SECRET_KEY = repr(os.environ.get('SECRET_KEY','Hrs8kCDNPuT9vtshsSWzlrYW+d+PrAXvg/HwbRE6M3vzSJTTrA/ZEw==')) -LEMUR_TOKEN_SECRET = repr(os.environ.get('LEMUR_TOKEN_SECRET','YVKT6nNHnWRWk28Lra1OPxMvHTqg1ZXvAcO7bkVNSbrEuDQPABM0VQ==')) -LEMUR_ENCRYPTION_KEYS = repr(os.environ.get('LEMUR_ENCRYPTION_KEYS','Ls-qg9j3EMFHyGB_NL0GcQLI6622n9pSyGM_Pu0GdCo=')) +def get_random_secret(length): + secret_key = ''.join(random.choice(string.ascii_uppercase) for x in range(round(length / 4))) + secret_key = secret_key + ''.join(random.choice("~!@#$%^&*()_+") for x in range(round(length / 4))) + secret_key = secret_key + ''.join(random.choice(string.ascii_lowercase) for x in range(round(length / 4))) + return secret_key + ''.join(random.choice(string.digits) for x in range(round(length / 4))) + + +SECRET_KEY = repr(os.environ.get('SECRET_KEY', get_random_secret(32).encode('utf8'))) + +LEMUR_TOKEN_SECRET = repr(os.environ.get('LEMUR_TOKEN_SECRET', + base64.b64encode(get_random_secret(32).encode('utf8')))) +LEMUR_ENCRYPTION_KEYS = repr(os.environ.get('LEMUR_ENCRYPTION_KEYS', + base64.b64encode(get_random_secret(32).encode('utf8')))) LEMUR_WHITELISTED_DOMAINS = [] diff --git a/lemur/authorities/schemas.py b/lemur/authorities/schemas.py index ef6263a8..f80d1581 100644 --- a/lemur/authorities/schemas.py +++ b/lemur/authorities/schemas.py @@ -43,13 +43,13 @@ class AuthorityInputSchema(LemurInputSchema): organization = fields.String( missing=lambda: current_app.config.get("LEMUR_DEFAULT_ORGANIZATION") ) - location = fields.String( - missing=lambda: current_app.config.get("LEMUR_DEFAULT_LOCATION") - ) + location = fields.String() country = fields.String( missing=lambda: current_app.config.get("LEMUR_DEFAULT_COUNTRY") ) state = fields.String(missing=lambda: current_app.config.get("LEMUR_DEFAULT_STATE")) + # Creating a String field instead of Email to allow empty value + email = fields.String() plugin = fields.Nested(PluginInputSchema) diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index f71d2199..60442de2 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -235,6 +235,7 @@ class Certificate(db.Model): self.replaces = kwargs.get("replaces", []) self.rotation = kwargs.get("rotation") self.rotation_policy = kwargs.get("rotation_policy") + self.key_type = kwargs.get("key_type") self.signing_algorithm = defaults.signing_algorithm(cert) self.bits = defaults.bitstrength(cert) self.external_id = kwargs.get("external_id") diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index 56c91196..a360140e 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -107,9 +107,7 @@ class CertificateInputSchema(CertificateCreationSchema): organization = fields.String( missing=lambda: current_app.config.get("LEMUR_DEFAULT_ORGANIZATION") ) - location = fields.String( - missing=lambda: current_app.config.get("LEMUR_DEFAULT_LOCATION") - ) + location = fields.String() country = fields.String( missing=lambda: current_app.config.get("LEMUR_DEFAULT_COUNTRY") ) @@ -155,6 +153,14 @@ class CertificateInputSchema(CertificateCreationSchema): key_type = cert_utils.get_key_type_from_csr(data["csr"]) if key_type: data["key_type"] = key_type + + # This code will be exercised for certificate import (without CSR) + if data.get("key_type") is None: + if data.get("body"): + data["key_type"] = utils.get_key_type_from_certificate(data["body"]) + else: + data["key_type"] = "RSA2048" # default value + return missing.convert_validity_years(data) @@ -277,6 +283,7 @@ class CertificateOutputSchema(LemurOutputSchema): serial = fields.String() serial_hex = Hex(attribute="serial") signing_algorithm = fields.String() + key_type = fields.String(allow_none=True) status = fields.String() user = fields.Nested(UserNestedOutputSchema) @@ -317,6 +324,7 @@ class CertificateUploadInputSchema(CertificateCreationSchema): body = fields.String(required=True) chain = fields.String(missing=None, allow_none=True) csr = fields.String(required=False, allow_none=True, validate=validators.csr) + key_type = fields.String() destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True) notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True) @@ -364,6 +372,16 @@ class CertificateUploadInputSchema(CertificateCreationSchema): # Throws ValidationError validators.verify_cert_chain([cert] + chain) + @pre_load + def load_data(self, data): + if data.get("body"): + try: + data["key_type"] = utils.get_key_type_from_certificate(data["body"]) + except ValueError: + raise ValidationError( + "Public certificate presented is not valid.", field_names=["body"] + ) + class CertificateExportInputSchema(LemurInputSchema): plugin = fields.Nested(PluginInputSchema) diff --git a/lemur/dns_providers/schemas.py b/lemur/dns_providers/schemas.py index 05b6471d..af9377b3 100644 --- a/lemur/dns_providers/schemas.py +++ b/lemur/dns_providers/schemas.py @@ -8,7 +8,7 @@ class DnsProvidersNestedOutputSchema(LemurOutputSchema): __envelope__ = False id = fields.Integer() name = fields.String() - providerType = fields.String() + provider_type = fields.String() description = fields.String() credentials = fields.String() api_endpoint = fields.String() diff --git a/lemur/migrations/versions/c301c59688d2_.py b/lemur/migrations/versions/c301c59688d2_.py index 3b0a86f7..669c934f 100644 --- a/lemur/migrations/versions/c301c59688d2_.py +++ b/lemur/migrations/versions/c301c59688d2_.py @@ -90,7 +90,7 @@ def update_key_type(): # Loop through all certificates that are valid today or expired in the last 30 days. for cert_id, body in conn.execute( text( - "select id, body from certificates where bits < 1024 and not_after > CURRENT_DATE - 31 and key_type is null") + "select id, body from certificates where not_after > CURRENT_DATE - 31 and key_type is null") ): try: cert_key_type = utils.get_key_type_from_certificate(body) diff --git a/lemur/static/app/angular/authorities/authority/authority.js b/lemur/static/app/angular/authorities/authority/authority.js index 9863bf4d..a449cff5 100644 --- a/lemur/static/app/angular/authorities/authority/authority.js +++ b/lemur/static/app/angular/authorities/authority/authority.js @@ -124,4 +124,8 @@ angular.module('lemur') opened: false }; + $scope.populateSubjectEmail = function () { + $scope.authority.email = $scope.authority.owner; + }; + }); diff --git a/lemur/static/app/angular/authorities/authority/distinguishedName.tpl.html b/lemur/static/app/angular/authorities/authority/distinguishedName.tpl.html index c6a7d312..e94f856e 100644 --- a/lemur/static/app/angular/authorities/authority/distinguishedName.tpl.html +++ b/lemur/static/app/angular/authorities/authority/distinguishedName.tpl.html @@ -26,8 +26,7 @@ Location
- -

You must enter a location

+
+
+ +
+ +
+
diff --git a/lemur/static/app/angular/authorities/authority/tracking.tpl.html b/lemur/static/app/angular/authorities/authority/tracking.tpl.html index 72d7e3d5..a561745f 100644 --- a/lemur/static/app/angular/authorities/authority/tracking.tpl.html +++ b/lemur/static/app/angular/authorities/authority/tracking.tpl.html @@ -21,7 +21,7 @@
+ class="form-control" ng-change="populateSubjectEmail()" required/>

You must enter an Certificate Authority owner

diff --git a/lemur/static/app/angular/certificates/certificate/certificate.js b/lemur/static/app/angular/certificates/certificate/certificate.js index 6b275328..9fadb655 100644 --- a/lemur/static/app/angular/certificates/certificate/certificate.js +++ b/lemur/static/app/angular/certificates/certificate/certificate.js @@ -251,10 +251,13 @@ angular.module('lemur') $scope.certificate.csr = null; // should not clone CSR in case other settings are changed in clone $scope.certificate.validityStart = null; $scope.certificate.validityEnd = null; - $scope.certificate.keyType = 'RSA2048'; // default algo to show during clone $scope.certificate.description = 'Cloning from cert ID ' + editId; $scope.certificate.replacedBy = []; // should not clone 'replaced by' info $scope.certificate.removeReplaces(); // should not clone 'replacement cert' info + + if(!$scope.certificate.keyType) { + $scope.certificate.keyType = 'RSA2048'; // default algo to select during clone if backend did not return algo + } CertificateService.getDefaults($scope.certificate); }); diff --git a/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html b/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html index 72f168a0..bc08c786 100644 --- a/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html +++ b/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html @@ -38,9 +38,7 @@ Location
- -

You must enter a - location

+
Key Length
{{ certificate.bits }}
+
Key Type
+
{{ certificate.keyType }}
Signing Algorithm
{{ certificate.signingAlgorithm }}
diff --git a/lemur/tests/conf.py b/lemur/tests/conf.py index 0a288327..8255e674 100644 --- a/lemur/tests/conf.py +++ b/lemur/tests/conf.py @@ -1,5 +1,6 @@ # This is just Python which means you can inherit and tweak settings +import base64 import os import random import string @@ -9,8 +10,10 @@ _basedir = os.path.abspath(os.path.dirname(__file__)) # generate random secrets for unittest def get_random_secret(length): - input_ascii = string.ascii_letters + string.digits - return ''.join(random.choice(input_ascii) for i in range(length)) + secret_key = ''.join(random.choice(string.ascii_uppercase) for x in range(round(length / 4))) + secret_key = secret_key + ''.join(random.choice("~!@#$%^&*()_+") for x in range(round(length / 4))) + secret_key = secret_key + ''.join(random.choice(string.ascii_lowercase) for x in range(round(length / 4))) + return secret_key + ''.join(random.choice(string.digits) for x in range(round(length / 4))) THREADS_PER_PAGE = 8 @@ -23,12 +26,14 @@ debug = False TESTING = True -# this is the secret key used by flask session management -SECRET_KEY = "I/dVhOZNSMZMqrFJa5tWli6VQccOGudKerq3eWPMSzQNmHHVhMAQfQ==" +# this is the secret key used by flask session management (utf8 encoded) +SECRET_KEY = get_random_secret(length=32).encode('utf8') -# You should consider storing these separately from your config + +# You should consider storing these separately from your config (should be URL-safe) LEMUR_TOKEN_SECRET = "test" -LEMUR_ENCRYPTION_KEYS = "o61sBLNBSGtAckngtNrfVNd8xy8Hp9LBGDstTbMbqCY=" +LEMUR_ENCRYPTION_KEYS = base64.urlsafe_b64encode(get_random_secret(length=32).encode('utf8')) + # List of domain regular expressions that non-admin users can issue LEMUR_WHITELISTED_DOMAINS = [ @@ -61,7 +66,8 @@ LEMUR_ALLOW_WEEKEND_EXPIRATION = False # Database -# modify this if you are not using a local database +# modify this if you are not using a local database. Do not use any development or production DBs, +# as Unit Tests drop the whole schema, recreate and again drop everything at the end SQLALCHEMY_DATABASE_URI = os.getenv( "SQLALCHEMY_DATABASE_URI", "postgresql://lemur:lemur@localhost:5432/lemur" ) diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index 41584cb3..a0a3b54e 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -154,7 +154,8 @@ def test_get_certificate_primitives(certificate): with freeze_time(datetime.date(year=2016, month=10, day=30)): primitives = get_certificate_primitives(certificate) - assert len(primitives) == 26 + assert len(primitives) == 25 + assert (primitives["key_type"] == "RSA2048") def test_certificate_output_schema(session, certificate, issuer_plugin): @@ -253,17 +254,18 @@ def test_certificate_input_schema(client, authority): "validityStart": arrow.get(2018, 11, 9).isoformat(), "validityEnd": arrow.get(2019, 11, 9).isoformat(), "dnsProvider": None, + "location": "A Place" } data, errors = CertificateInputSchema().load(input_data) assert not errors assert data["authority"].id == authority.id + assert data["location"] == "A Place" # make sure the defaults got set assert data["common_name"] == "test.example.com" assert data["country"] == "US" - assert data["location"] == "Los Gatos" assert len(data.keys()) == 19 @@ -759,6 +761,7 @@ def test_reissue_certificate( certificate.authority = crypto_authority new_cert = reissue_certificate(certificate) assert new_cert + assert (new_cert.key_type == "RSA2048") def test_create_csr(): diff --git a/lemur/tests/test_pending_certificates.py b/lemur/tests/test_pending_certificates.py index 3e755574..3718ef0a 100644 --- a/lemur/tests/test_pending_certificates.py +++ b/lemur/tests/test_pending_certificates.py @@ -55,6 +55,7 @@ def test_create_pending(pending_certificate, user, session): assert real_cert.notify == pending_certificate.notify assert real_cert.private_key == pending_certificate.private_key assert real_cert.external_id == "54321" + assert real_cert.key_type == "RSA2048" @pytest.mark.parametrize( diff --git a/requirements-dev.txt b/requirements-dev.txt index 166722e8..c53c57f9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ cffi==1.14.0 # via cryptography cfgv==3.1.0 # via pre-commit chardet==3.0.4 # via requests colorama==0.4.3 # via twine -cryptography==3.1 # via secretstorage +cryptography==3.1.1 # via secretstorage distlib==0.3.0 # via virtualenv docutils==0.16 # via readme-renderer filelock==3.0.12 # via virtualenv diff --git a/requirements-docs.txt b/requirements-docs.txt index 5c6fdf92..18338b5a 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -17,8 +17,8 @@ bcrypt==3.1.7 # via -r requirements.txt, flask-bcrypt, paramiko beautifulsoup4==4.9.1 # via -r requirements.txt, cloudflare billiard==3.6.3.0 # via -r requirements.txt, celery blinker==1.4 # via -r requirements.txt, flask-mail, flask-principal, raven -boto3==1.15.2 # via -r requirements.txt -botocore==1.18.2 # via -r requirements.txt, boto3, s3transfer +boto3==1.15.12 # via -r requirements.txt +botocore==1.18.12 # via -r requirements.txt, boto3, s3transfer celery[redis]==4.4.2 # via -r requirements.txt certifi==2020.6.20 # via -r requirements.txt, requests certsrv==2.1.1 # via -r requirements.txt @@ -26,7 +26,7 @@ cffi==1.14.0 # via -r requirements.txt, bcrypt, cryptography, pynac chardet==3.0.4 # via -r requirements.txt, requests click==7.1.1 # via -r requirements.txt, flask cloudflare==2.8.13 # via -r requirements.txt -cryptography==3.1 # via -r requirements.txt, acme, josepy, paramiko, pyopenssl, requests +cryptography==3.1.1 # via -r requirements.txt, acme, josepy, paramiko, pyopenssl, requests dnspython3==1.15.0 # via -r requirements.txt dnspython==1.15.0 # via -r requirements.txt, dnspython3 docutils==0.15.2 # via sphinx diff --git a/requirements-tests.txt b/requirements-tests.txt index b2b51cd7..2a81f432 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -10,21 +10,21 @@ aws-sam-translator==1.22.0 # via cfn-lint aws-xray-sdk==2.5.0 # via moto bandit==1.6.2 # via -r requirements-tests.in black==20.8b1 # via -r requirements-tests.in -boto3==1.15.2 # via aws-sam-translator, moto +boto3==1.15.12 # via aws-sam-translator, moto boto==2.49.0 # via moto -botocore==1.18.2 # via aws-xray-sdk, boto3, moto, s3transfer +botocore==1.18.12 # via aws-xray-sdk, boto3, moto, s3transfer certifi==2020.6.20 # via requests cffi==1.14.0 # via cryptography cfn-lint==0.29.5 # via moto chardet==3.0.4 # via requests click==7.1.2 # via black, flask coverage==5.3 # via -r requirements-tests.in -cryptography==3.1 # via moto, python-jose, sshpubkeys +cryptography==3.1.1 # via moto, python-jose, sshpubkeys decorator==4.4.2 # via networkx docker==4.2.0 # via moto ecdsa==0.14.1 # via moto, python-jose, sshpubkeys -factory-boy==3.0.1 # via -r requirements-tests.in -faker==4.1.3 # via -r requirements-tests.in, factory-boy +factory-boy==3.1.0 # via -r requirements-tests.in +faker==4.4.0 # via -r requirements-tests.in, factory-boy fakeredis==1.4.3 # via -r requirements-tests.in flask==1.1.2 # via pytest-flask freezegun==1.0.0 # via -r requirements-tests.in @@ -44,7 +44,7 @@ jsonpointer==2.0 # via jsonpatch jsonschema==3.2.0 # via aws-sam-translator, cfn-lint markupsafe==1.1.1 # via jinja2, moto mock==4.0.2 # via moto -more-itertools==8.2.0 # via moto, pytest +more-itertools==8.2.0 # via moto moto==1.3.16 # via -r requirements-tests.in mypy-extensions==0.4.3 # via black networkx==2.4 # via cfn-lint @@ -61,7 +61,7 @@ pyparsing==2.4.7 # via packaging pyrsistent==0.16.0 # via jsonschema pytest-flask==1.0.0 # via -r requirements-tests.in pytest-mock==3.3.1 # via -r requirements-tests.in -pytest==6.0.2 # via -r requirements-tests.in, pytest-flask, pytest-mock +pytest==6.1.1 # via -r requirements-tests.in, pytest-flask, pytest-mock python-dateutil==2.8.1 # via botocore, faker, freezegun, moto python-jose[cryptography]==3.1.0 # via moto pytz==2019.3 # via moto diff --git a/requirements.txt b/requirements.txt index cab2a8ed..83013eac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,8 +15,8 @@ bcrypt==3.1.7 # via flask-bcrypt, paramiko beautifulsoup4==4.9.1 # via cloudflare billiard==3.6.3.0 # via celery blinker==1.4 # via flask-mail, flask-principal, raven -boto3==1.15.2 # via -r requirements.in -botocore==1.18.2 # via -r requirements.in, boto3, s3transfer +boto3==1.15.12 # via -r requirements.in +botocore==1.18.12 # via -r requirements.in, boto3, s3transfer celery[redis]==4.4.2 # via -r requirements.in certifi==2020.6.20 # via -r requirements.in, requests certsrv==2.1.1 # via -r requirements.in @@ -24,7 +24,7 @@ cffi==1.14.0 # via bcrypt, cryptography, pynacl chardet==3.0.4 # via requests click==7.1.1 # via flask cloudflare==2.8.13 # via -r requirements.in -cryptography==3.1 # via -r requirements.in, acme, josepy, paramiko, pyopenssl, requests +cryptography==3.1.1 # via -r requirements.in, acme, josepy, paramiko, pyopenssl, requests dnspython3==1.15.0 # via -r requirements.in dnspython==1.15.0 # via dnspython3 dyn==1.8.1 # via -r requirements.in