Compare commits

..

36 Commits

Author SHA1 Message Date
bb5b32a435 add OpenSSH plugin 2021-05-22 16:45:44 +02:00
da75d31fac Merge pull request #3485 from hosseinsh/changelog-0.9.0
release 0.9.0 changelog
2021-03-17 15:17:39 -07:00
b295679cc3 Merge branch 'master' into changelog-0.9.0 2021-03-17 13:14:35 -07:00
e3db887b07 Merge pull request #3488 from Netflix/enable-codeql-scanning
Enable CodeQL scanning
2021-03-17 13:13:53 -07:00
5d61ed4d5b Merge branch 'master' into enable-codeql-scanning 2021-03-17 12:52:14 -07:00
ba1c549070 Merge branch 'master' into changelog-0.9.0 2021-03-17 12:36:43 -07:00
f4178fefd2 Merge pull request #3487 from Netflix/codeowners
codeowners
2021-03-17 12:36:20 -07:00
3a757f8f94 Update codeql-analysis.yml 2021-03-17 12:16:14 -07:00
2bb1d9ee21 Update codeql-analysis.yml 2021-03-17 12:13:07 -07:00
28a4d21bcc Update codeql-analysis.yml 2021-03-17 12:09:55 -07:00
49800bf9da Merge branch 'master' into changelog-0.9.0 2021-03-17 11:51:24 -07:00
2c081df06b Merge branch 'master' into codeowners 2021-03-17 11:51:13 -07:00
1636847040 Merge pull request #3486 from Netflix/stats_whitelist_01
Add allow_list to stats endpoint
2021-03-17 11:51:01 -07:00
c977826d62 Create codeql-analysis.yml 2021-03-17 11:28:12 -07:00
dbea35ba19 Update CODEOWNERS 2021-03-17 11:27:45 -07:00
91d0b36a6a Merge branch 'master' into stats_whitelist_01 2021-03-17 11:18:26 -07:00
890a016ee0 Merge branch 'master' into codeowners 2021-03-17 11:18:22 -07:00
35a933ce9b Merge branch 'master' into changelog-0.9.0 2021-03-17 11:17:59 -07:00
acf6ac1531 Merge pull request #3484 from jtschladen/security-fixes
Security fixes
2021-03-17 11:17:48 -07:00
ad742e6eee codeowners 2021-03-17 11:17:23 -07:00
f7938bf226 Merge branch 'master' into stats_whitelist_01 2021-03-17 11:06:24 -07:00
deb7586372 Merge branch 'master' into changelog-0.9.0 2021-03-17 11:06:15 -07:00
2da9754ffa Security fixes 2021-03-17 10:51:21 -07:00
4f409547a0 release 0.9.0 2021-03-17 09:59:51 -07:00
c1168399a4 Merge pull request #3472 from hosseinsh/auto-release-doc
updated docs for automated release
2021-03-16 16:26:00 -07:00
a40298df08 Merge branch 'master' into auto-release-doc 2021-03-16 16:10:53 -07:00
0175df821c Merge pull request #3478 from jtschladen/upgrade-dependabot
Add config to uptake GitHub's native Dependabot with auto-merge action
2021-03-16 16:08:38 -07:00
b5c38c2854 Merge branch 'master' into upgrade-dependabot 2021-03-16 15:41:18 -07:00
dc1f1c247a Add config to uptake GitHub's native Dependabot with auto-merge action 2021-03-16 15:39:22 -07:00
28b9a73a83 Merge pull request #3476 from Netflix/dependabot/pip/pre-commit-2.11.1 2021-03-15 18:25:10 +00:00
d097da685a Bump pre-commit from 2.11.0 to 2.11.1
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.11.0 to 2.11.1.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v2.11.0...v2.11.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-03-15 18:06:22 +00:00
1c137e6596 Merge pull request #3473 from Netflix/dependabot/pip/boto3-1.17.27 2021-03-15 18:04:23 +00:00
0d388a85bb Bump boto3 from 1.17.22 to 1.17.27
Bumps [boto3](https://github.com/boto/boto3) from 1.17.22 to 1.17.27.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.17.22...1.17.27)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-03-15 13:49:12 +00:00
a0a5e66cc3 fixing broken doc 2021-03-12 12:10:38 -08:00
1d486cf1fd updated docs for automated release 2021-03-12 11:49:17 -08:00
377ba25413 Adding allow_list to stats endpoint 2021-02-22 14:56:34 -08:00
25 changed files with 690 additions and 27 deletions

2
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,2 @@
# These owners will be the default owners for everything in the repo.
* @hosseinsh @csine-nflx @charhate @jtschladen

15
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,15 @@
version: 2
updates:
- directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "08:00"
timezone: "America/Los_Angeles"
package-ecosystem: "pip"
reviewers:
- "hosseinsh"
- "csine-nflx"
- "charhate"
- "jtschladen"
versioning-strategy: lockfile-only

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '15 16 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Install prerequisites for python-ldap. See: https://www.python-ldap.org/en/python-ldap-3.3.0/installing.html#build-prerequisites
- name: Install python-ldap prerequisites
run: sudo apt-get install libldap2-dev libsasl2-dev
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -0,0 +1,14 @@
name: dependabot-auto-merge
on:
pull_request:
jobs:
auto-merge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ahmadnassri/action-dependabot-auto-merge@v2
with:
target: minor
github-token: ${{ secrets.DEPENDABOT_GITHUB_TOKEN }}

View File

@ -1,6 +1,13 @@
Changelog
=========
0.9.0 - `2021-03-17`
~~~~~~~~~~~~~~~~~~~~
This release fixes three critical vulnerabilities where an authenticated user could retrieve/access
unauthorized information. (Issue `#3463 <https://github.com/Netflix/lemur/issues/3463>`_)
0.8.1 - `2021-03-12`
~~~~~~~~~~~~~~~~~~~~

View File

@ -1,10 +1,18 @@
Doing a release
===============
Doing a release of ``lemur`` requires a few steps.
Doing a release of ``lemur`` is now mostly automated and consists of the following steps:
Bumping the version number
--------------------------
* Raise a PR to add the release date and summary in the :doc:`/changelog`.
* Merge above PR and create a new `Github release <https://github.com/Netflix/lemur/releaes>`_: set the tag starting with v, e.g., v0.9.0
The `publish workflow <https://github.com/Netflix/lemur/actions/workflows/lemur-publish-release-pypi.yml>`_ uses the git
tag to set the release version.
The following describes the manual release steps, which is now obsolete:
Manually Bumping the version number
-----------------------------------
The next step in doing a release is bumping the version number in the
software.
@ -14,8 +22,8 @@ software.
* Do a commit indicating this, and raise a pull request with this.
* Wait for it to be merged.
Performing the release
----------------------
Manually Performing the release
-------------------------------
The commit that merged the version number bump is now the official release
commit for this release. You need an `API key <https://pypi.org/manage/account/#api-tokens>`_,

View File

@ -600,3 +600,118 @@ Using `python-jwt` converting an existing private key in PEM format is quite eas
{"body": {}, "uri": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/<ACCOUNT_NUMBER>"}
The URI can be retrieved from the ACME create account endpoint when creating a new account, using the existing key.
OpenSSH
=======
OpenSSH (also known as OpenBSD Secure Shell) is a suite of secure networking utilities based on the Secure Shell (SSH) protocol, which provides a secure channel over an unsecured network in a clientserver architecture.
Using a PKI with OpenSSH means you can sign a key for a user and it can log into any server that trust the CA.
Using a CA avoids TOFU or synchronize a list of server public keys to `known_hosts` files.
This is useful when you're managing large number of machines or for an immutable infrastructure.
Add first OpenSSH authority
---------------------------
To start issuing OpenSSH, you need to create an OpenSSH authority. To do this, visit
Authorities -> Create. Set the applicable attributes:
- Name : OpenSSH
- Common Name: example.net
Then click "More Options" and change the plugin value to "OpenSSH".
Just click to "Create" button to add this authority.
.. note:: OpenSSH do not support sub CA feature.
Add a server certificate
-------------------------
Now visit Certificates -> Create to add a server certificate. Set the applicable attributes:
- Common Name: server.example.net
Then click "More Options" and set the Certificate Template to "Server Certificate".
This step is important, a certificat for a server and for a client is not exactly the same thing.
In this case "Common Name" and all Subject Alternate Names with type DNSName will be added in the certificate.
Finally click on "Create" button.
Add a client certificate
------------------------
Now visit Certificates -> Create to add a client certificate. Set the applicable attributes:
- Common Name: example.net
Then click "More Options" and set the Certificate Template to "Client Certificate".
In this case the name of the creator is used as principal (in this documentation we assume that this certificate is created by the user "lemur").
Finally click on "Create" button.
Configure OpenSSH server
------------------------
Connect to the server.example.net server to correctly configure the OpenSSH server with the CA created previously.
First of all add the CA chain, private and public certificates:
- Create file `/etc/ssh/ca.pub` and copy the "CHAIN" content of the *server certificate* (everything in one line).
- Create file `/etc/ssh/ssh_host_key` and copy "PRIVATE KEY" content.
- Create file `/etc/ssh/ssh_host_key.pub` and copy "PUBLIC CERTIFICATE" content (everything in one line).
Set the appropriate right:
.. code-block:: bash
chmod 600 /etc/ssh/ca.pub /etc/ssh/ssh_host_key
chmod 644 /etc/ssh/ssh_host_key.pub
chown root: /etc/ssh/ca.pub /etc/ssh/ssh_host_key /etc/ssh/ssh_host_key.pub
Then change OpenSSH server configuration to use these files. Edit `/etc/ssh/sshd_config` and add::
TrustedUserCAKeys /etc/ssh/ca.pub
HostKey /etc/ssh/ssh_host_key
HostCertificate /etc/ssh/ssh_host_key.pub
You can remove all other `HostKey` lines.
Finally restart OpenSSH.
Configure the OpenSSH client
----------------------------
Now you can configure the user's computer.
First of all add private and public certificates:
- Create file `~/.ssh/key` and copy "PRIVATE KEY" content.
- Create file `~/.ssh/key.pub` and copy "PUBLIC CERTIFICATE" content of the *client certicate* (everything in one line).
Set the appropriate right:
.. code-block:: bash
chmod 600 ~/.ssh/key.pub ~/.ssh/key
To avoid TOFU, edite the `~/.ssh/known_hosts` file and add a new line (all in one line):
- @cert-authority \*example.net
- the "CHAIN" content
Now you can connect to server with (here 'lemur' is the principal name and must exists on the server):
.. code-block:: bash
ssh lemur@server.example.net -i ~/.ssh/key
With this configuration you don't have any line like::
Warning: Permanently added 'server.example.net,192.168.0.1' (RSA) to the list of known hosts.
And you don't have to enter any password.

View File

@ -7,7 +7,7 @@
"""
from flask import current_app
from marshmallow import fields, validates_schema, pre_load
from marshmallow import fields, validates_schema, pre_load, post_dump
from marshmallow import validate
from marshmallow.exceptions import ValidationError
@ -24,6 +24,7 @@ from lemur.common import validators, missing
from lemur.common.fields import ArrowDateTime
from lemur.constants import CERTIFICATE_KEY_TYPES
from lemur.plugins.base import plugins
class AuthorityInputSchema(LemurInputSchema):
@ -129,6 +130,12 @@ class AuthorityOutputSchema(LemurOutputSchema):
default_validity_days = fields.Integer()
authority_certificate = fields.Nested(RootAuthorityCertificateOutputSchema)
@post_dump
def handle_auth_certificate(self, cert):
# Plugins may need to modify the cert object before returning it to the user
plugin = plugins.get(cert['plugin']['slug'])
plugin.wrap_auth_certificate(cert['authority_certificate'])
class AuthorityNestedOutputSchema(LemurOutputSchema):
__envelope__ = False

View File

@ -117,6 +117,12 @@ def create(**kwargs):
"""
Creates a new authority.
"""
ca_name = kwargs.get("name")
if get_by_name(ca_name):
raise Exception(f"Authority with name {ca_name} already exists")
if role_service.get_by_name(f"{ca_name}_admin") or role_service.get_by_name(f"{ca_name}_operator"):
raise Exception(f"Admin and/or operator roles for authority {ca_name} already exist")
body, private_key, chain, roles = mint(**kwargs)
kwargs["creator"].roles = list(set(list(kwargs["creator"].roles) + roles))

View File

@ -38,6 +38,7 @@ from lemur.schemas import (
AssociatedRotationPolicySchema,
)
from lemur.users.schemas import UserNestedOutputSchema
from lemur.plugins.base import plugins
class CertificateSchema(LemurInputSchema):
@ -324,6 +325,8 @@ class CertificateOutputSchema(LemurOutputSchema):
notifications = fields.Nested(NotificationNestedOutputSchema, many=True)
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
authority = fields.Nested(AuthorityNestedOutputSchema)
# if this certificate is an authority, the authority informations are in root_authority
root_authority = fields.Nested(AuthorityNestedOutputSchema)
dns_provider = fields.Nested(DnsProvidersNestedOutputSchema)
roles = fields.Nested(RoleNestedOutputSchema, many=True)
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
@ -357,6 +360,22 @@ class CertificateOutputSchema(LemurOutputSchema):
if field in data and data[field] is None:
data.pop(field)
@post_dump
def handle_certificate(self, cert):
# Plugins may need to modify the cert object before returning it to the user
if cert['authority'] is None:
if cert['root_authority'] is None:
plugin = None
else:
# this certificate is an authority
plugin = plugins.get(cert['root_authority']['plugin']['slug'])
else:
plugin = plugins.get(cert['authority']['plugin']['slug'])
if plugin:
plugin.wrap_certificate(cert)
if 'root_authority' in cert:
del cert['root_authority']
class CertificateShortOutputSchema(LemurOutputSchema):
id = fields.Integer()

View File

@ -88,6 +88,16 @@ def get_by_attributes(conditions):
return database.find_all(query, Certificate, conditions).all()
def get_by_root_authority(id):
"""
Retrieves certificate by its root_authority's id.
:param id:
:return:
"""
return database.get(Certificate, id, field="root_authority_id")
def delete(cert_id):
"""
Delete's a certificate.
@ -679,7 +689,16 @@ def stats(**kwargs):
:param kwargs:
:return:
"""
if kwargs.get("metric") == "not_after":
# Verify requested metric
allow_list = ["bits", "issuer", "not_after", "signing_algorithm"]
req_metric = kwargs.get("metric")
if req_metric not in allow_list:
raise Exception(
f"Stats not available for requested metric: {req_metric}"
)
if req_metric == "not_after":
start = arrow.utcnow()
end = start.shift(weeks=+32)
items = (
@ -691,7 +710,7 @@ def stats(**kwargs):
)
else:
attr = getattr(Certificate, kwargs.get("metric"))
attr = getattr(Certificate, req_metric)
query = database.db.session.query(attr, func.count(attr))
items = query.group_by(attr).all()

View File

@ -33,6 +33,7 @@ from lemur.certificates.schemas import (
from lemur.roles import service as role_service
from lemur.logs import service as log_service
from lemur.plugins.base import plugins
mod = Blueprint("certificates", __name__)
@ -635,7 +636,12 @@ class CertificatesStats(AuthenticatedResource):
args = self.reqparse.parse_args()
try:
items = service.stats(**args)
except Exception as e:
sentry.captureException()
return dict(message=f"Failed to retrieve stats: {str(e)}"), 400
return dict(items=items, total=len(items))
@ -686,6 +692,16 @@ class CertificatePrivateKey(AuthenticatedResource):
return dict(message="You are not authorized to view this key"), 403
log_service.create(g.current_user, "key_view", certificate=cert)
# Plugins may need to modify the cert object before returning it to the user
if cert.root_authority:
# this certificate is an authority
plugin_name = cert.root_authority.plugin_name
else:
plugin_name = cert.authority.plugin_name
plugin = plugins.get(plugin_name)
plugin.wrap_private_key(cert)
response = make_response(jsonify(key=cert.private_key), 200)
response.headers["cache-control"] = "private, max-age=0, no-cache, no-store"
response.headers["pragma"] = "no-cache"

View File

@ -425,7 +425,7 @@ class CertificateDestinations(AuthenticatedResource):
class DestinationsStats(AuthenticatedResource):
""" Defines the 'certificates' stats endpoint """
""" Defines the 'destinations' stats endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()

View File

@ -10,9 +10,9 @@ class DnsProvidersNestedOutputSchema(LemurOutputSchema):
name = fields.String()
provider_type = fields.String()
description = fields.String()
credentials = fields.String()
api_endpoint = fields.String()
date_created = ArrowDateTime()
# credentials are intentionally omitted (they are input-only)
class DnsProvidersNestedInputSchema(LemurInputSchema):

View File

@ -31,3 +31,12 @@ class IssuerPlugin(Plugin):
def cancel_ordered_certificate(self, pending_cert, **kwargs):
raise NotImplementedError
def wrap_certificate(self, cert):
pass
def wrap_auth_certificate(self, cert):
pass
def wrap_private_key(self, cert):
pass

View File

@ -0,0 +1,4 @@
try:
VERSION = __import__("pkg_resources").get_distribution(__name__).version
except Exception as e:
VERSION = "unknown"

View File

@ -0,0 +1,159 @@
"""
.. module: lemur.plugins.lemur_openssh.plugin
:platform: Unix
:copyright: (c) 2020 by Emmanuel Garette, see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Emmanuel Garette <gnunux@gnunux.info>
"""
import subprocess
from os import unlink
from flask import current_app
from cryptography.hazmat.primitives import serialization
from datetime import datetime
from lemur.utils import mktempfile
from lemur.plugins import lemur_openssh as openssh
from lemur.common.utils import parse_private_key, parse_certificate
from lemur.plugins.lemur_cryptography.plugin import CryptographyIssuerPlugin
from lemur.certificates.service import get_by_root_authority
def run_process(command):
"""
Runs a given command with pOpen and wraps some
error handling around it.
:param command:
:return:
"""
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
current_app.logger.debug(" ".join(command))
stdout, stderr = p.communicate()
if p.returncode != 0:
current_app.logger.error(stderr.decode())
raise Exception(stderr.decode())
def split_cert(body):
"""
To display certificate in Lemur website, we have to split
certificate in several line
:param body: certificate
:retur: splitted certificate
"""
length = 65
return '\n'.join([body[i:i + length] for i in range(0, len(body), length)])
def sign_certificate(common_name, public_key, authority_private_key, user, extensions, not_before, not_after):
private_key = parse_private_key(authority_private_key).private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.OpenSSH,
encryption_algorithm=serialization.NoEncryption(),
).decode()
with mktempfile() as issuer_tmp:
cmd = ['ssh-keygen', '-s', issuer_tmp]
with open(issuer_tmp, 'w') as i:
i.writelines(private_key)
if 'extendedKeyUsage' in extensions and extensions['extendedKeyUsage'].get('useClientAuthentication'):
cmd.extend(['-I', user['username'] + ' user key',
'-n', user['username']])
else:
domains = {common_name}
for name in extensions['subAltNames']['names']:
if name['nameType'] == 'DNSName':
domains.add(name['value'])
cmd.extend(['-I', common_name + ' host key',
'-n', ','.join(domains),
'-h'])
# something like 20201024102030
ssh_not_before = datetime.fromisoformat(not_before).strftime("%Y%m%d%H%M%S")
ssh_not_after = datetime.fromisoformat(not_after).strftime("%Y%m%d%H%M%S")
cmd.extend(['-V', ssh_not_before + ':' + ssh_not_after])
with mktempfile() as cert_tmp:
with open(cert_tmp, 'w') as f:
f.write(public_key)
cmd.append(cert_tmp)
run_process(cmd)
pub = cert_tmp + '-cert.pub'
with open(pub, 'r') as p:
body = split_cert(p.read())
unlink(pub)
return body
class OpenSSHIssuerPlugin(CryptographyIssuerPlugin):
"""This issuer plugins is base in Cryptography plugin
Certificates and authorities are x509 certificates created by Cryptography plugin.
Those certificates are converted to OpenSSH format when people get them.
"""
title = "OpenSSH"
slug = "openssh-issuer"
description = "Enables the creation and signing OpenSSH keys"
version = openssh.VERSION
author = "Emmanuel Garette"
author_url = "http://gnunux.info"
def create_authority(self, options):
# OpenSSH do not support parent's authoriy
if options.get("parent"):
raise Exception('cannot create authority with a parent for OpenSSH plugin')
# create a x509 certificat
cert_pem, private_key, chain_cert_pem, roles = super().create_authority(options)
return cert_pem, private_key, chain_cert_pem, roles
def wrap_certificate(self, cert):
if 'body' not in cert:
return
# get public_key in OpenSSH format
public_key = parse_certificate(cert['body']).public_key().public_bytes(
encoding=serialization.Encoding.OpenSSH,
format=serialization.PublicFormat.OpenSSH,
).decode()
public_key += ' ' + cert['user']['email']
# sign it with authority private key
if 'root_authority' in cert and cert['root_authority']:
authority = cert['root_authority']
else:
authority = cert['authority']
root_authority = get_by_root_authority(authority['id'])
authority_private_key = root_authority.private_key
cert['body'] = sign_certificate(
cert['common_name'],
public_key,
authority_private_key,
cert['user'],
cert['extensions'],
cert['not_before'],
cert['not_after']
)
# convert chain in OpenSSH format
if cert['chain']:
chain_cert = {'body': cert['chain'], 'cn': root_authority.cn}
self.wrap_auth_certificate(chain_cert)
cert['chain'] = chain_cert['body']
# OpenSSH do not support csr
cert['csr'] = None
@staticmethod
def wrap_auth_certificate(auth_cert):
# convert chain in OpenSSH format
chain_key = parse_certificate(auth_cert['body']).public_key().public_bytes(
encoding=serialization.Encoding.OpenSSH,
format=serialization.PublicFormat.OpenSSH,
).decode()
chain_key += ' root@' + auth_cert['cn']
auth_cert['body'] = split_cert(chain_key)
@staticmethod
def wrap_private_key(cert):
# convert private_key in OpenSSH format
cert.private_key = parse_private_key(cert.private_key).private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.OpenSSH,
encryption_algorithm=serialization.NoEncryption(),
)

View File

@ -36,6 +36,7 @@ from .factories import (
InvalidCertificateFactory,
CryptoAuthorityFactory,
CACertificateFactory,
DnsProviderFactory,
)
@ -183,6 +184,13 @@ def user(session):
return {"user": u, "token": token}
@pytest.fixture
def dns_provider(session):
d = DnsProviderFactory()
session.commit()
return d
@pytest.fixture
def pending_certificate(session):
u = UserFactory()

View File

@ -1,14 +1,15 @@
import json
from datetime import date
from factory import Sequence, post_generation, SubFactory
from factory.alchemy import SQLAlchemyModelFactory
from factory.fuzzy import FuzzyChoice, FuzzyText, FuzzyDate, FuzzyInteger
from lemur.database import db
from lemur.authorities.models import Authority
from lemur.certificates.models import Certificate
from lemur.destinations.models import Destination
from lemur.dns_providers.models import DnsProvider
from lemur.sources.models import Source
from lemur.notifications.models import Notification
from lemur.pending_certificates.models import PendingCertificate
@ -435,3 +436,17 @@ class PendingCertificateFactory(BaseFactory):
if extracted:
for domain in extracted:
self.roles.append(domain)
class DnsProviderFactory(BaseFactory):
"""DnsProvider Factory."""
name = Sequence(lambda n: f"dnsProvider{n}")
description = FuzzyText(length=128)
provider_type = FuzzyText(length=128)
credentials = json.dumps({"account_id": f"{FuzzyInteger(100000, 999999).fuzz()}"})
class Meta:
"""Factory Configuration."""
model = DnsProvider

View File

@ -1,5 +1,7 @@
import json
import unittest
from lemur.dns_providers import util as dnsutil
from lemur.dns_providers.schemas import dns_provider_output_schema
class TestDNSProvider(unittest.TestCase):
@ -21,3 +23,17 @@ class TestDNSProvider(unittest.TestCase):
self.assertFalse(dnsutil.is_valid_domain('example..io'))
self.assertFalse(dnsutil.is_valid_domain('exa mple.io'))
self.assertFalse(dnsutil.is_valid_domain('-'))
def test_output_schema(dns_provider):
# no credentials using the output schema dump
assert dns_provider.credentials
assert json.loads(dns_provider.credentials)["account_id"]
dump = dns_provider_output_schema.dump(dns_provider).data
assert 'name' in dump
assert 'credentials' not in dump
def test_json(dns_provider):
# we can still get credentials using json.load
assert 'account_id' in json.loads(dns_provider.credentials)

View File

@ -50,7 +50,7 @@ packaging==20.9
# via bleach
pkginfo==1.5.0.1
# via twine
pre-commit==2.11.0
pre-commit==2.11.1
# via -r requirements-dev.in
pycodestyle==2.6.0
# via flake8

View File

@ -5,7 +5,10 @@
# pip-compile --no-index --output-file=requirements-docs.txt requirements-docs.in
#
acme==1.13.0
# via -r requirements-docs.in
# via
# -r requirements-docs.in
# -r requirements-tests.txt
# certbot
alabaster==0.7.12
# via sphinx
alembic==1.5.5
@ -48,7 +51,7 @@ blinker==1.4
# flask-mail
# flask-principal
# raven
boto3==1.17.22
boto3==1.17.27
# via
# -r requirements-docs.in
# -r requirements-tests.txt
@ -58,7 +61,7 @@ boto==2.49.0
# via
# -r requirements-tests.txt
# moto
botocore==1.20.22
botocore==1.20.27
# via
# -r requirements-docs.in
# -r requirements-tests.txt
@ -67,6 +70,9 @@ botocore==1.20.22
# moto
# s3transfer
certbot==1.13.0
# via
# -r requirements-docs.in
# -r requirements-tests.txt
certifi==2020.12.5
# via
# -r requirements-tests.txt
@ -94,6 +100,14 @@ click==7.1.2
# flask
cloudflare==2.8.15
# via -r requirements-docs.in
configargparse==1.4
# via
# -r requirements-tests.txt
# certbot
configobj==5.0.6
# via
# -r requirements-tests.txt
# certbot
coverage==5.5
# via -r requirements-tests.txt
cryptography==3.4.6
@ -101,6 +115,7 @@ cryptography==3.4.6
# -r requirements-docs.in
# -r requirements-tests.txt
# acme
# certbot
# josepy
# moto
# paramiko
@ -111,6 +126,10 @@ decorator==4.4.2
# via
# -r requirements-tests.txt
# networkx
distro==1.5.0
# via
# -r requirements-tests.txt
# certbot
dnspython3==1.15.0
# via -r requirements-docs.in
dnspython==1.15.0
@ -226,7 +245,9 @@ jmespath==0.9.5
josepy==1.7.0
# via
# -r requirements-docs.in
# -r requirements-tests.txt
# acme
# certbot
jsondiff==1.1.2
# via
# -r requirements-tests.txt
@ -293,6 +314,10 @@ packaging==20.3
# sphinx
paramiko==2.7.2
# via -r requirements-docs.in
parsedatetime==2.6
# via
# -r requirements-tests.txt
# certbot
pathspec==0.8.0
# via
# -r requirements-tests.txt
@ -339,6 +364,7 @@ pynacl==1.4.0
pyopenssl==20.0.1
# via
# -r requirements-docs.in
# -r requirements-tests.txt
# acme
# josepy
pyparsing==2.4.7
@ -346,7 +372,10 @@ pyparsing==2.4.7
# -r requirements-tests.txt
# packaging
pyrfc3339==1.1
# via acme
# via
# -r requirements-tests.txt
# acme
# certbot
pyrsistent==0.16.0
# via
# -r requirements-tests.txt
@ -382,6 +411,7 @@ pytz==2019.3
# -r requirements-tests.txt
# acme
# babel
# certbot
# flask-restful
# moto
# pyrfc3339
@ -406,7 +436,9 @@ regex==2020.4.4
requests-mock==1.8.0
# via -r requirements-tests.txt
requests-toolbelt==0.9.1
# via acme
# via
# -r requirements-tests.txt
# acme
requests==2.25.1
# via
# -r requirements-tests.txt
@ -441,6 +473,7 @@ six==1.15.0
# bandit
# bcrypt
# cfn-lint
# configobj
# docker
# ecdsa
# fakeredis
@ -564,6 +597,36 @@ zipp==3.1.0
# -r requirements-tests.txt
# importlib-metadata
# moto
zope.component==4.6.2
# via
# -r requirements-tests.txt
# certbot
zope.deferredimport==4.3.1
# via
# -r requirements-tests.txt
# zope.component
zope.deprecation==4.4.0
# via
# -r requirements-tests.txt
# zope.component
zope.event==4.5.0
# via
# -r requirements-tests.txt
# zope.component
zope.hookable==5.0.1
# via
# -r requirements-tests.txt
# zope.component
zope.interface==5.2.0
# via
# -r requirements-tests.txt
# certbot
# zope.component
# zope.proxy
zope.proxy==4.3.5
# via
# -r requirements-tests.txt
# zope.deferredimport
# The following packages are considered to be unsafe in a requirements file:
# setuptools

View File

@ -4,6 +4,8 @@
#
# pip-compile --no-index --output-file=requirements-tests.txt requirements-tests.in
#
acme==1.13.0
# via certbot
appdirs==1.4.3
# via black
attrs==19.3.0
@ -18,19 +20,20 @@ bandit==1.7.0
# via -r requirements-tests.in
black==20.8b1
# via -r requirements-tests.in
boto3==1.17.22
boto3==1.17.27
# via
# aws-sam-translator
# moto
boto==2.49.0
# via moto
botocore==1.20.22
botocore==1.20.27
# via
# aws-xray-sdk
# boto3
# moto
# s3transfer
certbot==1.13.0
# via -r requirements-tests.in
certifi==2020.12.5
# via requests
cffi==1.14.0
@ -43,15 +46,25 @@ click==7.1.2
# via
# black
# flask
configargparse==1.4
# via certbot
configobj==5.0.6
# via certbot
coverage==5.5
# via -r requirements-tests.in
cryptography==3.4.6
# via
# acme
# certbot
# josepy
# moto
# pyopenssl
# python-jose
# sshpubkeys
decorator==4.4.2
# via networkx
distro==1.5.0
# via certbot
docker==4.2.0
# via moto
ecdsa==0.14.1
@ -95,6 +108,10 @@ jmespath==0.9.5
# via
# boto3
# botocore
josepy==1.7.0
# via
# acme
# certbot
jsondiff==1.1.2
# via moto
jsonpatch==1.25
@ -125,6 +142,8 @@ nose==1.3.7
# via -r requirements-tests.in
packaging==20.3
# via pytest
parsedatetime==2.6
# via certbot
pathspec==0.8.0
# via black
pbr==5.4.5
@ -141,8 +160,16 @@ pycparser==2.20
# via cffi
pyflakes==2.2.0
# via -r requirements-tests.in
pyopenssl==20.0.1
# via
# acme
# josepy
pyparsing==2.4.7
# via packaging
pyrfc3339==1.1
# via
# acme
# certbot
pyrsistent==0.16.0
# via jsonschema
pytest-flask==1.2.0
@ -163,7 +190,11 @@ python-dateutil==2.8.1
python-jose[cryptography]==3.1.0
# via moto
pytz==2019.3
# via moto
# via
# acme
# certbot
# moto
# pyrfc3339
pyyaml==5.4.1
# via
# -r requirements-tests.in
@ -176,11 +207,15 @@ regex==2020.4.4
# via black
requests-mock==1.8.0
# via -r requirements-tests.in
requests-toolbelt==0.9.1
# via acme
requests==2.25.1
# via
# acme
# docker
# moto
# requests-mock
# requests-toolbelt
# responses
responses==0.10.12
# via moto
@ -193,12 +228,15 @@ six==1.15.0
# aws-sam-translator
# bandit
# cfn-lint
# configobj
# docker
# ecdsa
# fakeredis
# josepy
# jsonschema
# moto
# packaging
# pyopenssl
# pyrsistent
# python-dateutil
# python-jose
@ -243,6 +281,23 @@ zipp==3.1.0
# via
# importlib-metadata
# moto
zope.component==4.6.2
# via certbot
zope.deferredimport==4.3.1
# via zope.component
zope.deprecation==4.4.0
# via zope.component
zope.event==4.5.0
# via zope.component
zope.hookable==5.0.1
# via zope.component
zope.interface==5.2.0
# via
# certbot
# zope.component
# zope.proxy
zope.proxy==4.3.5
# via zope.deferredimport
# The following packages are considered to be unsafe in a requirements file:
# setuptools

View File

@ -5,7 +5,9 @@
# pip-compile --no-index --output-file=requirements.txt requirements.in
#
acme==1.13.0
# via -r requirements.in
# via
# -r requirements.in
# certbot
alembic-autogenerate-enums==0.0.2
# via -r requirements.in
alembic==1.4.2
@ -31,9 +33,9 @@ blinker==1.4
# flask-mail
# flask-principal
# raven
boto3==1.17.22
boto3==1.17.27
# via -r requirements.in
botocore==1.20.22
botocore==1.20.27
# via
# -r requirements.in
# boto3
@ -41,6 +43,7 @@ botocore==1.20.22
celery[redis]==4.4.2
# via -r requirements.in
certbot==1.13.0
# via -r requirements.in
certifi==2020.12.5
# via
# -r requirements.in
@ -58,13 +61,20 @@ click==7.1.2
# via flask
cloudflare==2.8.15
# via -r requirements.in
configargparse==1.4
# via certbot
configobj==5.0.6
# via certbot
cryptography==3.4.6
# via
# -r requirements.in
# acme
# certbot
# josepy
# paramiko
# pyopenssl
distro==1.5.0
# via certbot
dnspython3==1.15.0
# via -r requirements.in
dnspython==1.15.0
@ -126,7 +136,9 @@ jmespath==0.9.5
# boto3
# botocore
josepy==1.7.0
# via acme
# via
# acme
# certbot
jsonlines==1.2.0
# via cloudflare
kombu==4.6.8
@ -151,6 +163,8 @@ ndg-httpsclient==0.5.1
# via -r requirements.in
paramiko==2.7.2
# via -r requirements.in
parsedatetime==2.6
# via certbot
pem==21.1.0
# via -r requirements.in
psycopg2==2.8.6
@ -182,7 +196,9 @@ pyopenssl==20.0.1
# josepy
# ndg-httpsclient
pyrfc3339==1.1
# via acme
# via
# acme
# certbot
python-dateutil==2.8.1
# via
# alembic
@ -198,6 +214,7 @@ pytz==2019.3
# via
# acme
# celery
# certbot
# flask-restful
# pyrfc3339
pyyaml==5.4.1
@ -228,6 +245,7 @@ six==1.15.0
# via
# -r requirements.in
# bcrypt
# configobj
# flask-cors
# flask-restful
# hvac
@ -264,6 +282,22 @@ werkzeug==1.0.1
# via flask
xmltodict==0.12.0
# via -r requirements.in
zope.component==4.6.2
# via certbot
zope.deferredimport==4.3.1
# via zope.component
zope.deprecation==4.4.0
# via zope.component
zope.event==4.5.0
# via zope.component
zope.hookable==5.0.1
# via zope.component
zope.interface==5.2.0
# via
# certbot
# zope.component
zope.proxy==4.3.5
# via zope.deferredimport
# The following packages are considered to be unsafe in a requirements file:
# setuptools

View File

@ -158,6 +158,7 @@ setup(
'adcs_source = lemur.plugins.lemur_adcs.plugin:ADCSSourcePlugin',
'entrust_issuer = lemur.plugins.lemur_entrust.plugin:EntrustIssuerPlugin',
'entrust_source = lemur.plugins.lemur_entrust.plugin:EntrustSourcePlugin',
'openssh_issuer = lemur.plugins.lemur_openssh.plugin:OpenSSHIssuerPlugin',
'azure_destination = lemur.plugins.lemur_azure_dest.plugin:AzureDestinationPlugin'
],
},