Merge pull request #2757 from jplana/add-pending-certificate-upload

Allow uploading a signed cert for a pending certificate.
This commit is contained in:
Curtis 2019-04-24 09:53:50 -07:00 committed by GitHub
commit ccd0bde0bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 326 additions and 4 deletions

View File

@ -1,5 +1,7 @@
from marshmallow import fields, post_load from marshmallow import fields, validates_schema, post_load
from marshmallow.exceptions import ValidationError
from lemur.common import utils, validators
from lemur.authorities.schemas import AuthorityNestedOutputSchema from lemur.authorities.schemas import AuthorityNestedOutputSchema
from lemur.certificates.schemas import CertificateNestedOutputSchema from lemur.certificates.schemas import CertificateNestedOutputSchema
from lemur.common.schema import LemurInputSchema, LemurOutputSchema from lemur.common.schema import LemurInputSchema, LemurOutputSchema
@ -98,6 +100,31 @@ class PendingCertificateCancelSchema(LemurInputSchema):
note = fields.String() note = fields.String()
class PendingCertificateUploadInputSchema(LemurInputSchema):
external_id = fields.String(missing=None, allow_none=True)
body = fields.String(required=True)
chain = fields.String(missing=None, allow_none=True)
@validates_schema
def validate_cert_chain(self, data):
cert = None
if data.get('body'):
try:
cert = utils.parse_certificate(data['body'])
except ValueError:
raise ValidationError("Public certificate presented is not valid.", field_names=['body'])
if data.get('chain'):
try:
chain = utils.parse_cert_chain(data['chain'])
except ValueError:
raise ValidationError("Invalid certificate in certificate chain.", field_names=['chain'])
# Throws ValidationError
validators.verify_cert_chain([cert] + chain)
pending_certificate_output_schema = PendingCertificateOutputSchema() pending_certificate_output_schema = PendingCertificateOutputSchema()
pending_certificate_edit_input_schema = PendingCertificateEditInputSchema() pending_certificate_edit_input_schema = PendingCertificateEditInputSchema()
pending_certificate_cancel_schema = PendingCertificateCancelSchema() pending_certificate_cancel_schema = PendingCertificateCancelSchema()
pending_certificate_upload_input_schema = PendingCertificateUploadInputSchema()

View File

@ -8,9 +8,11 @@ from sqlalchemy import or_, cast, Integer
from lemur import database from lemur import database
from lemur.authorities.models import Authority from lemur.authorities.models import Authority
from lemur.authorities import service as authorities_service
from lemur.certificates import service as certificate_service from lemur.certificates import service as certificate_service
from lemur.certificates.schemas import CertificateUploadInputSchema from lemur.certificates.schemas import CertificateUploadInputSchema
from lemur.common.utils import truthiness from lemur.common.utils import truthiness, parse_cert_chain, parse_certificate
from lemur.common import validators
from lemur.destinations.models import Destination from lemur.destinations.models import Destination
from lemur.domains.models import Domain from lemur.domains.models import Domain
from lemur.notifications.models import Notification from lemur.notifications.models import Notification
@ -230,3 +232,40 @@ def render(args):
# Only show unresolved certificates in the UI # Only show unresolved certificates in the UI
query = query.filter(PendingCertificate.resolved.is_(False)) query = query.filter(PendingCertificate.resolved.is_(False))
return database.sort_and_page(query, PendingCertificate, args) return database.sort_and_page(query, PendingCertificate, args)
def upload(pending_certificate_id, **kwargs):
"""
Uploads a (signed) pending certificate. The allowed fields are validated by
PendingCertificateUploadInputSchema. The certificate is also validated to be
signed by the correct authoritity.
"""
pending_cert = get(pending_certificate_id)
partial_cert = kwargs
uploaded_chain = partial_cert['chain']
authority = authorities_service.get(pending_cert.authority.id)
# Construct the chain for cert validation
if uploaded_chain:
chain = uploaded_chain + '\n' + authority.authority_certificate.body
else:
chain = authority.authority_certificate.body
parsed_chain = parse_cert_chain(chain)
# Check that the certificate is actually signed by the CA to avoid incorrect cert pasting
validators.verify_cert_chain([parse_certificate(partial_cert['body'])] + parsed_chain)
final_cert = create_certificate(pending_cert, partial_cert, pending_cert.user)
update(
pending_cert.id,
resolved=True
)
pending_cert_final_result = update(
pending_cert.id,
resolved_cert_id=final_cert.id
)
return pending_cert_final_result

View File

@ -20,6 +20,7 @@ from lemur.pending_certificates.schemas import (
pending_certificate_output_schema, pending_certificate_output_schema,
pending_certificate_edit_input_schema, pending_certificate_edit_input_schema,
pending_certificate_cancel_schema, pending_certificate_cancel_schema,
pending_certificate_upload_input_schema,
) )
mod = Blueprint('pending_certificates', __name__) mod = Blueprint('pending_certificates', __name__)
@ -419,6 +420,101 @@ class PendingCertificatePrivateKey(AuthenticatedResource):
return response return response
class PendingCertificatesUpload(AuthenticatedResource):
""" Defines the 'pending_certificates' upload endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(PendingCertificatesUpload, self).__init__()
@validate_schema(pending_certificate_upload_input_schema, pending_certificate_output_schema)
def post(self, pending_certificate_id, data=None):
"""
.. http:post:: /pending_certificates/1/upload
Upload the body for a (signed) pending_certificate
**Example request**:
.. sourcecode:: http
POST /certificates/1/upload HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"body": "-----BEGIN CERTIFICATE-----...",
"chain": "-----BEGIN CERTIFICATE-----...",
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
"dateCreated": "2016-06-03T06:09:42.133769+00:00",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}],
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
"rotation": true,
"rotationPolicy": {"name": "default"},
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 403: unauthenticated
:statuscode 200: no error
"""
return service.upload(pending_certificate_id, **data)
api.add_resource(PendingCertificatesList, '/pending_certificates', endpoint='pending_certificates') api.add_resource(PendingCertificatesList, '/pending_certificates', endpoint='pending_certificates')
api.add_resource(PendingCertificates, '/pending_certificates/<int:pending_certificate_id>', endpoint='pending_certificate') api.add_resource(PendingCertificates, '/pending_certificates/<int:pending_certificate_id>', endpoint='pending_certificate')
api.add_resource(PendingCertificatesUpload, '/pending_certificates/<int:pending_certificate_id>/upload', endpoint='pendingCertificateUpload')
api.add_resource(PendingCertificatePrivateKey, '/pending_certificates/<int:pending_certificate_id>/key', endpoint='privateKeyPendingCertificates') api.add_resource(PendingCertificatePrivateKey, '/pending_certificates/<int:pending_certificate_id>/key', endpoint='privateKeyPendingCertificates')

View File

@ -0,0 +1,34 @@
'use strict';
angular.module('lemur')
.controller('PendingCertificateUploadController', function ($scope, $uibModalInstance, PendingCertificateApi, PendingCertificateService, toaster, uploadId) {
PendingCertificateApi.get(uploadId).then(function (pendingCertificate) {
$scope.pendingCertificate = pendingCertificate;
});
$scope.upload = PendingCertificateService.upload;
$scope.save = function (pendingCertificate) {
PendingCertificateService.upload(pendingCertificate).then(
function () {
toaster.pop({
type: 'success',
title: pendingCertificate.name,
body: 'Successfully uploaded!'
});
$uibModalInstance.close();
},
function (response) {
toaster.pop({
type: 'error',
title: pendingCertificate.name,
body: 'Failed to upload ' + response.data.message,
timeout: 100000
});
});
};
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
};
});

View File

@ -0,0 +1,41 @@
<div class="modal-header">
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h3 class="modal-title">Import certificate <span class="text-muted"><small>{{ pendingCertificate.name }}</small></span></h3>
</div>
<div class="modal-body">
<form name="uploadForm" class="form-horizontal" role="form" novalidate>
<div class="form-group"
ng-class="{'has-error': uploadForm.publicCert.$invalid, 'has-success': !uploadForm.publicCert.$invalid&&uploadForm.publicCert.$dirty}">
<label class="control-label col-sm-2">
Public Certificate
</label>
<div class="col-sm-10">
<textarea name="publicCert" ng-model="pendingCertificate.body" placeholder="PEM encoded string..."
class="form-control" ng-pattern="/^-----BEGIN CERTIFICATE-----/" required></textarea>
<p ng-show="uploadForm.publicCert.$invalid && !uploadForm.publicCert.$pristine" class="help-block">Enter
a valid certificate.</p>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': uploadForm.owner.$invalid&&uploadform.intermediateCert.$dirty, 'has-success': !uploadForm.intermediateCert.$invalid&&uploadForm.intermediateCert.$dirty}">
<label class="control-label col-sm-2">
Intermediate Certificate
</label>
<div class="col-sm-10">
<textarea name="intermediateCert" ng-model="pendingCertificate.chain"
placeholder="PEM encoded string..." class="form-control"
ng-pattern="/^-----BEGIN CERTIFICATE-----/"></textarea>
<p ng-show="uploadForm.intermediateCert.$invalid && !uploadForm.intemediateCert.$pristine"
class="help-block">Enter a valid certificate.</p>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="submit" ng-click="save(pendingCertificate)" ng-disabled="uploadForm.$invalid" class="btn btn-success">
Import
</button>
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div>
</div>

View File

@ -245,5 +245,9 @@ angular.module('lemur')
return pending_certificate.customOperation('remove', null, {}, {'Content-Type': 'application/json'}, options); return pending_certificate.customOperation('remove', null, {}, {'Content-Type': 'application/json'}, options);
}; };
PendingCertificateService.upload = function (pending_certificate) {
return pending_certificate.customPOST({'body': pending_certificate.body, 'chain': pending_certificate.chain}, 'upload');
};
return PendingCertificateService; return PendingCertificateService;
}); });

View File

@ -99,4 +99,23 @@ angular.module('lemur')
$scope.pendingCertificateTable.reload(); $scope.pendingCertificateTable.reload();
}); });
}; };
$scope.upload = function (pendingCertificateId) {
var uibModalInstance = $uibModal.open({
animation: true,
controller: 'PendingCertificateUploadController',
templateUrl: '/angular/pending_certificates/pending_certificate/upload.tpl.html',
size: 'lg',
backdrop: 'static',
resolve: {
uploadId: function () {
return pendingCertificateId;
}
}
});
uibModalInstance.result.then(function () {
$scope.pendingCertificateTable.reload();
});
};
}); });

View File

@ -51,6 +51,7 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href ng-click="edit(pendingCertificate.id)">Edit</a></li> <li><a href ng-click="edit(pendingCertificate.id)">Edit</a></li>
<li><a href ng-click="cancel(pendingCertificate.id)">Cancel</a></li> <li><a href ng-click="cancel(pendingCertificate.id)">Cancel</a></li>
<li><a href ng-click="upload(pendingCertificate.id)">Upload</a></li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -13,12 +13,12 @@ from lemur import create_app
from lemur.common.utils import parse_private_key from lemur.common.utils import parse_private_key
from lemur.database import db as _db from lemur.database import db as _db
from lemur.auth.service import create_token from lemur.auth.service import create_token
from lemur.tests.vectors import SAN_CERT_KEY, INTERMEDIATE_KEY from lemur.tests.vectors import SAN_CERT_KEY, INTERMEDIATE_KEY, ROOTCA_CERT_STR, ROOTCA_KEY
from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \ from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \
CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, \ CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, \
RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory, InvalidCertificateFactory, \ RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory, InvalidCertificateFactory, \
CryptoAuthorityFactory CryptoAuthorityFactory, CACertificateFactory
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
@ -172,6 +172,25 @@ def pending_certificate(session):
return p return p
@pytest.fixture
def pending_certificate_from_full_chain_ca(session):
u = UserFactory()
a = AuthorityFactory()
p = PendingCertificateFactory(user=u, authority=a)
session.commit()
return p
@pytest.fixture
def pending_certificate_from_partial_chain_ca(session):
u = UserFactory()
c = CACertificateFactory(body=ROOTCA_CERT_STR, private_key=ROOTCA_KEY, chain=None)
a = AuthorityFactory(authority_certificate=c)
p = PendingCertificateFactory(user=u, authority=a)
session.commit()
return p
@pytest.fixture @pytest.fixture
def invalid_certificate(session): def invalid_certificate(session):
u = UserFactory() u = UserFactory()

View File

@ -2,6 +2,7 @@ import json
import pytest import pytest
from marshmallow import ValidationError
from lemur.pending_certificates.views import * # noqa from lemur.pending_certificates.views import * # noqa
from .vectors import CSR_STR, INTERMEDIATE_CERT_STR, VALID_ADMIN_API_TOKEN, VALID_ADMIN_HEADER_TOKEN, \ from .vectors import CSR_STR, INTERMEDIATE_CERT_STR, VALID_ADMIN_API_TOKEN, VALID_ADMIN_HEADER_TOKEN, \
VALID_USER_HEADER_TOKEN, WILDCARD_CERT_STR VALID_USER_HEADER_TOKEN, WILDCARD_CERT_STR
@ -50,3 +51,44 @@ def test_pending_cancel(client, pending_certificate, token, status):
assert client.delete(api.url_for(PendingCertificates, pending_certificate_id=pending_certificate.id), assert client.delete(api.url_for(PendingCertificates, pending_certificate_id=pending_certificate.id),
data=json.dumps({'note': "unit test", 'send_email': False}), data=json.dumps({'note': "unit test", 'send_email': False}),
headers=token).status_code == status headers=token).status_code == status
def test_pending_upload(pending_certificate_from_full_chain_ca):
from lemur.pending_certificates.service import upload
from lemur.certificates.service import get
cert = {'body': WILDCARD_CERT_STR,
'chain': None,
'external_id': None
}
pending_cert = upload(pending_certificate_from_full_chain_ca.id, **cert)
assert pending_cert.resolved
assert get(pending_cert.resolved_cert_id)
def test_pending_upload_with_chain(pending_certificate_from_partial_chain_ca):
from lemur.pending_certificates.service import upload
from lemur.certificates.service import get
cert = {'body': WILDCARD_CERT_STR,
'chain': INTERMEDIATE_CERT_STR,
'external_id': None
}
pending_cert = upload(pending_certificate_from_partial_chain_ca.id, **cert)
assert pending_cert.resolved
assert get(pending_cert.resolved_cert_id)
def test_invalid_pending_upload_with_chain(pending_certificate_from_partial_chain_ca):
from lemur.pending_certificates.service import upload
cert = {'body': WILDCARD_CERT_STR,
'chain': None,
'external_id': None
}
with pytest.raises(ValidationError) as err:
upload(pending_certificate_from_partial_chain_ca.id, **cert)
assert str(err.value).startswith(
'Incorrect chain certificate(s) provided: \'*.wild.example.org\' is not signed by \'LemurTrust Unittests Root CA 2018')