Allow uploading a signed cert for a pending certificate.
This commit is contained in:
parent
4c7bf917f2
commit
7dd9268ca7
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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');
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,41 @@
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">×</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>
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in New Issue