mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-23 00:25:18 +00:00
api: Add preliminary SCEP support
This commit is contained in:
parent
a5ad9238a1
commit
5ae872e1ea
4
.gitignore
vendored
4
.gitignore
vendored
@ -64,3 +64,7 @@ node_modules/
|
|||||||
# Ignore autogenerated files
|
# Ignore autogenerated files
|
||||||
certidude/static/js/nunjucks*
|
certidude/static/js/nunjucks*
|
||||||
certidude/static/js/templates.js
|
certidude/static/js/templates.js
|
||||||
|
|
||||||
|
# Ignore patch
|
||||||
|
*.orig
|
||||||
|
*.rej
|
||||||
|
@ -210,6 +210,11 @@ def certidude_app(log_handlers=[]):
|
|||||||
# Bootstrap resource
|
# Bootstrap resource
|
||||||
app.add_route("/api/bootstrap/", BootstrapResource())
|
app.add_route("/api/bootstrap/", BootstrapResource())
|
||||||
|
|
||||||
|
# Add SCEP handler if we have any whitelisted subnets
|
||||||
|
if config.SCEP_SUBNETS:
|
||||||
|
from .scep import SCEPResource
|
||||||
|
app.add_route("/api/scep/", SCEPResource())
|
||||||
|
|
||||||
# Add sink for serving static files
|
# Add sink for serving static files
|
||||||
app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static")))
|
app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static")))
|
||||||
|
|
||||||
|
273
certidude/api/scep.py
Normal file
273
certidude/api/scep.py
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
from __future__ import unicode_literals, division, absolute_import, print_function
|
||||||
|
import click
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
from asn1crypto import cms, algos, x509
|
||||||
|
from asn1crypto.core import ObjectIdentifier, SetOf, PrintableString
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
from certbuilder import pem_armor_certificate
|
||||||
|
from certidude import authority, push, config
|
||||||
|
from certidude.firewall import whitelist_subnets
|
||||||
|
from oscrypto import keys, asymmetric, symmetric
|
||||||
|
from oscrypto.errors import SignatureError
|
||||||
|
|
||||||
|
# Monkey patch asn1crypto
|
||||||
|
|
||||||
|
class SetOfPrintableString(SetOf):
|
||||||
|
_child_spec = PrintableString
|
||||||
|
|
||||||
|
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.2'] = "message_type"
|
||||||
|
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.3'] = "pki_status"
|
||||||
|
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.4'] = "fail_info"
|
||||||
|
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.5'] = "sender_nonce"
|
||||||
|
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.6'] = "recipient_nonce"
|
||||||
|
cms.CMSAttributeType._map['2.16.840.1.113733.1.9.7'] = "trans_id"
|
||||||
|
|
||||||
|
cms.CMSAttribute._oid_specs['message_type'] = SetOfPrintableString
|
||||||
|
cms.CMSAttribute._oid_specs['pki_status'] = SetOfPrintableString
|
||||||
|
cms.CMSAttribute._oid_specs['fail_info'] = SetOfPrintableString
|
||||||
|
cms.CMSAttribute._oid_specs['sender_nonce'] = cms.SetOfOctetString
|
||||||
|
cms.CMSAttribute._oid_specs['recipient_nonce'] = cms.SetOfOctetString
|
||||||
|
cms.CMSAttribute._oid_specs['trans_id'] = SetOfPrintableString
|
||||||
|
|
||||||
|
class SCEPError(Exception): code = 25 # system failure
|
||||||
|
class SCEPBadAlg(SCEPError): code = 0
|
||||||
|
class SCEPBadMessageCheck(SCEPError): code = 1
|
||||||
|
class SCEPBadRequest(SCEPError): code = 2
|
||||||
|
class SCEPBadTime(SCEPError): code = 3
|
||||||
|
class SCEPBadCertId(SCEPError): code = 4
|
||||||
|
|
||||||
|
class SCEPResource(object):
|
||||||
|
@whitelist_subnets(config.SCEP_SUBNETS)
|
||||||
|
def on_get(self, req, resp):
|
||||||
|
operation = req.get_param("operation")
|
||||||
|
if operation.lower() == "getcacert":
|
||||||
|
resp.stream = keys.parse_certificate(authority.ca_buf).dump()
|
||||||
|
resp.append_header("Content-Type", "application/x-x509-ca-cert")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Parse CA certificate
|
||||||
|
fh = open(config.AUTHORITY_CERTIFICATE_PATH)
|
||||||
|
server_certificate = asymmetric.load_certificate(fh.read())
|
||||||
|
fh.close()
|
||||||
|
|
||||||
|
# If we bump into exceptions later
|
||||||
|
encrypted_container = b""
|
||||||
|
attr_list = [
|
||||||
|
cms.CMSAttribute({
|
||||||
|
'type': "message_type",
|
||||||
|
'values': ["3"]
|
||||||
|
}),
|
||||||
|
cms.CMSAttribute({
|
||||||
|
'type': "pki_status",
|
||||||
|
'values': ["2"] # rejected
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = cms.ContentInfo.load(b64decode(req.get_param("message", required=True)))
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
### Verify signature of the outer container ###
|
||||||
|
###############################################
|
||||||
|
|
||||||
|
signed_envelope = info['content']
|
||||||
|
encap_content_info = signed_envelope['encap_content_info']
|
||||||
|
encap_content = encap_content_info['content']
|
||||||
|
|
||||||
|
# TODO: try except
|
||||||
|
current_certificate, = signed_envelope["certificates"]
|
||||||
|
signer, = signed_envelope["signer_infos"]
|
||||||
|
|
||||||
|
# TODO: compare cert to current one if we are renewing
|
||||||
|
|
||||||
|
assert signer["digest_algorithm"]["algorithm"].native == "md5"
|
||||||
|
assert signer["signature_algorithm"]["algorithm"].native == "rsassa_pkcs1v15"
|
||||||
|
message_digest = None
|
||||||
|
transaction_id = None
|
||||||
|
sender_nonce = None
|
||||||
|
|
||||||
|
for attr in signer["signed_attrs"]:
|
||||||
|
if attr["type"].native == "sender_nonce":
|
||||||
|
sender_nonce, = attr["values"]
|
||||||
|
elif attr["type"].native == "trans_id":
|
||||||
|
transaction_id, = attr["values"]
|
||||||
|
elif attr["type"].native == "message_digest":
|
||||||
|
message_digest, = attr["values"]
|
||||||
|
if hashlib.md5(encap_content.native).digest() != message_digest.native:
|
||||||
|
raise SCEPBadMessageCheck()
|
||||||
|
|
||||||
|
assert message_digest
|
||||||
|
msg = signer["signed_attrs"].dump(force=True)
|
||||||
|
assert msg[0] == b"\xa0", repr(msg[0])
|
||||||
|
|
||||||
|
# Verify signature
|
||||||
|
try:
|
||||||
|
asymmetric.rsa_pkcs1v15_verify(
|
||||||
|
asymmetric.load_certificate(current_certificate.dump()),
|
||||||
|
signer["signature"].native,
|
||||||
|
b"\x31" + msg[1:], # wtf?!
|
||||||
|
"md5")
|
||||||
|
except SignatureError:
|
||||||
|
raise SCEPBadMessageCheck()
|
||||||
|
|
||||||
|
###############################
|
||||||
|
### Decrypt inner container ###
|
||||||
|
###############################
|
||||||
|
|
||||||
|
info = cms.ContentInfo.load(encap_content.native)
|
||||||
|
encrypted_envelope = info['content']
|
||||||
|
encrypted_content_info = encrypted_envelope['encrypted_content_info']
|
||||||
|
iv = encrypted_content_info['content_encryption_algorithm']['parameters'].native
|
||||||
|
|
||||||
|
if encrypted_content_info['content_encryption_algorithm']["algorithm"].native != "des":
|
||||||
|
raise SCEPBadAlgo()
|
||||||
|
|
||||||
|
encrypted_content = encrypted_content_info['encrypted_content'].native
|
||||||
|
recipient, = encrypted_envelope['recipient_infos']
|
||||||
|
|
||||||
|
if recipient.native["rid"]["serial_number"] != authority.ca_cert.serial:
|
||||||
|
raise SCEPBadCertId()
|
||||||
|
|
||||||
|
# Since CA private key is not directly readable here, we'll redirect it to signer socket
|
||||||
|
key = b64decode(authority.signer_exec("decrypt-pkcs7", b64encode(recipient.native["encrypted_key"])))
|
||||||
|
if len(key) == 8: key = key * 3 # Convert DES to 3DES
|
||||||
|
buf = symmetric.tripledes_cbc_pkcs5_decrypt(key, encrypted_content, iv)
|
||||||
|
_, common_name = authority.store_request(buf, overwrite=True)
|
||||||
|
cert, buf = authority.sign(common_name, overwrite=True)
|
||||||
|
signed_certificate = asymmetric.load_certificate(buf)
|
||||||
|
content = signed_certificate.asn1.dump()
|
||||||
|
|
||||||
|
except SCEPError, e:
|
||||||
|
attr_list.append(cms.CMSAttribute({
|
||||||
|
'type': "fail_info",
|
||||||
|
'values': ["%d" % e.code]
|
||||||
|
}))
|
||||||
|
else:
|
||||||
|
|
||||||
|
##################################
|
||||||
|
### Degenerate inner container ###
|
||||||
|
##################################
|
||||||
|
|
||||||
|
degenerate = cms.ContentInfo({
|
||||||
|
'content_type': 'signed_data',
|
||||||
|
'content': cms.SignedData({
|
||||||
|
'version': 'v1',
|
||||||
|
'certificates': [signed_certificate.asn1],
|
||||||
|
'digest_algorithms': [cms.DigestAlgorithm({
|
||||||
|
'algorithm':'md5'
|
||||||
|
})],
|
||||||
|
'encap_content_info': {
|
||||||
|
'content_type': 'data',
|
||||||
|
'content': cms.ContentInfo({
|
||||||
|
'content_type': 'signed_data',
|
||||||
|
'content': None
|
||||||
|
}).dump()
|
||||||
|
},
|
||||||
|
'signer_infos': []
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
################################
|
||||||
|
### Encrypt middle container ###
|
||||||
|
################################
|
||||||
|
|
||||||
|
key = os.urandom(8)
|
||||||
|
iv, encrypted_content = symmetric.des_cbc_pkcs5_encrypt(key, degenerate.dump(), os.urandom(8))
|
||||||
|
assert degenerate.dump() == symmetric.tripledes_cbc_pkcs5_decrypt(key*3, encrypted_content, iv)
|
||||||
|
|
||||||
|
ri = cms.RecipientInfo({
|
||||||
|
'ktri': cms.KeyTransRecipientInfo({
|
||||||
|
'version': 'v0',
|
||||||
|
'rid': cms.RecipientIdentifier({
|
||||||
|
'issuer_and_serial_number': cms.IssuerAndSerialNumber({
|
||||||
|
'issuer': current_certificate.chosen["tbs_certificate"]["issuer"],
|
||||||
|
'serial_number': current_certificate.chosen["tbs_certificate"]["serial_number"],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'key_encryption_algorithm': {
|
||||||
|
'algorithm': 'rsa'
|
||||||
|
},
|
||||||
|
'encrypted_key': asymmetric.rsa_pkcs1v15_encrypt(
|
||||||
|
asymmetric.load_certificate(current_certificate.chosen.dump()), key)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
encrypted_container = cms.ContentInfo({
|
||||||
|
'content_type': 'enveloped_data',
|
||||||
|
'content': cms.EnvelopedData({
|
||||||
|
'version': 'v1',
|
||||||
|
'recipient_infos': [ri],
|
||||||
|
'encrypted_content_info': {
|
||||||
|
'content_type': 'data',
|
||||||
|
'content_encryption_algorithm': {
|
||||||
|
'algorithm': 'des',
|
||||||
|
'parameters': iv
|
||||||
|
},
|
||||||
|
'encrypted_content': encrypted_content
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).dump()
|
||||||
|
|
||||||
|
attr_list = [
|
||||||
|
cms.CMSAttribute({
|
||||||
|
'type': 'message_digest',
|
||||||
|
'values': [hashlib.sha1(encrypted_container).digest()]
|
||||||
|
}),
|
||||||
|
cms.CMSAttribute({
|
||||||
|
'type': "message_type",
|
||||||
|
'values': ["3"]
|
||||||
|
}),
|
||||||
|
cms.CMSAttribute({
|
||||||
|
'type': "pki_status",
|
||||||
|
'values': ["0"] # ok
|
||||||
|
})
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
|
||||||
|
##############################
|
||||||
|
### Signed outer container ###
|
||||||
|
##############################
|
||||||
|
|
||||||
|
attrs = cms.CMSAttributes(attr_list + [
|
||||||
|
cms.CMSAttribute({
|
||||||
|
'type': "recipient_nonce",
|
||||||
|
'values': [sender_nonce]
|
||||||
|
}),
|
||||||
|
cms.CMSAttribute({
|
||||||
|
'type': 'trans_id',
|
||||||
|
'values': [transaction_id]
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
signer = cms.SignerInfo({
|
||||||
|
"signed_attrs": attrs,
|
||||||
|
'version':'v1',
|
||||||
|
'sid': cms.SignerIdentifier({
|
||||||
|
'issuer_and_serial_number': cms.IssuerAndSerialNumber({
|
||||||
|
'issuer': server_certificate.asn1["tbs_certificate"]["issuer"],
|
||||||
|
'serial_number': server_certificate.asn1["tbs_certificate"]["serial_number"],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'digest_algorithm': algos.DigestAlgorithm({'algorithm': 'sha1'}),
|
||||||
|
'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': 'rsassa_pkcs1v15'}),
|
||||||
|
'signature': b64decode(authority.signer_exec("sign-pkcs7", b64encode(b"\x31" + attrs.dump()[1:])))
|
||||||
|
})
|
||||||
|
|
||||||
|
resp.append_header("Content-Type", "application/x-pki-message")
|
||||||
|
resp.body = cms.ContentInfo({
|
||||||
|
'content_type': 'signed_data',
|
||||||
|
'content': cms.SignedData({
|
||||||
|
'version': 'v1',
|
||||||
|
'certificates': [x509.Certificate.load(server_certificate.asn1.dump())], # wat
|
||||||
|
'digest_algorithms': [cms.DigestAlgorithm({
|
||||||
|
'algorithm':'sha1'
|
||||||
|
})],
|
||||||
|
'encap_content_info': {
|
||||||
|
'content_type': 'data',
|
||||||
|
'content': encrypted_container
|
||||||
|
},
|
||||||
|
'signer_infos': [signer]
|
||||||
|
})
|
||||||
|
}).dump()
|
@ -12,6 +12,7 @@ from cryptography import x509
|
|||||||
from cryptography.x509.oid import NameOID, ExtensionOID, ExtendedKeyUsageOID
|
from cryptography.x509.oid import NameOID, ExtensionOID, ExtendedKeyUsageOID
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
from cryptography.hazmat.primitives import hashes, serialization
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.serialization import Encoding
|
||||||
from certidude import config, push, mailer, const
|
from certidude import config, push, mailer, const
|
||||||
from certidude import errors
|
from certidude import errors
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
@ -81,7 +82,13 @@ def store_request(buf, overwrite=False):
|
|||||||
if not buf:
|
if not buf:
|
||||||
raise ValueError("No signing request supplied")
|
raise ValueError("No signing request supplied")
|
||||||
|
|
||||||
|
if isinstance(buf, unicode):
|
||||||
csr = x509.load_pem_x509_csr(buf, backend=default_backend())
|
csr = x509.load_pem_x509_csr(buf, backend=default_backend())
|
||||||
|
elif isinstance(buf, str):
|
||||||
|
csr = x509.load_der_x509_csr(buf, backend=default_backend())
|
||||||
|
buf = csr.public_bytes(Encoding.PEM)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid type, expected str for PEM and bytes for DER")
|
||||||
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
||||||
# TODO: validate common name again
|
# TODO: validate common name again
|
||||||
|
|
||||||
@ -92,7 +99,7 @@ def store_request(buf, overwrite=False):
|
|||||||
|
|
||||||
|
|
||||||
# If there is cert, check if it's the same
|
# If there is cert, check if it's the same
|
||||||
if os.path.exists(request_path):
|
if os.path.exists(request_path) and not overwrite:
|
||||||
if open(request_path).read() == buf:
|
if open(request_path).read() == buf:
|
||||||
raise errors.RequestExists("Request already exists")
|
raise errors.RequestExists("Request already exists")
|
||||||
else:
|
else:
|
||||||
@ -106,7 +113,7 @@ def store_request(buf, overwrite=False):
|
|||||||
mailer.send("request-stored.md",
|
mailer.send("request-stored.md",
|
||||||
attachments=(attach_csr,),
|
attachments=(attach_csr,),
|
||||||
common_name=common_name.value)
|
common_name=common_name.value)
|
||||||
return csr
|
return csr, common_name.value
|
||||||
|
|
||||||
|
|
||||||
def signer_exec(cmd, *bits):
|
def signer_exec(cmd, *bits):
|
||||||
|
@ -32,6 +32,8 @@ AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in
|
|||||||
cp.get("authorization", "autosign subnets").split(" ") if j])
|
cp.get("authorization", "autosign subnets").split(" ") if j])
|
||||||
REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in
|
REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||||
cp.get("authorization", "request subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS)
|
cp.get("authorization", "request subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS)
|
||||||
|
SCEP_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||||
|
cp.get("authorization", "scep subnets").split(" ") if j])
|
||||||
|
|
||||||
AUTHORITY_DIR = "/var/lib/certidude"
|
AUTHORITY_DIR = "/var/lib/certidude"
|
||||||
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
|
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
|
||||||
|
@ -5,11 +5,13 @@ import socket
|
|||||||
import os
|
import os
|
||||||
import asyncore
|
import asyncore
|
||||||
import asynchat
|
import asynchat
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
from certidude import const, config
|
from certidude import const, config
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives import hashes, serialization
|
from cryptography.hazmat.primitives import hashes, padding, serialization
|
||||||
from cryptography.hazmat.primitives.serialization import Encoding
|
from cryptography.hazmat.primitives.serialization import Encoding
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, AuthorityInformationAccessOID
|
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, AuthorityInformationAccessOID
|
||||||
import random
|
import random
|
||||||
@ -64,6 +66,18 @@ class SignHandler(asynchat.async_chat):
|
|||||||
self.close()
|
self.close()
|
||||||
raise asyncore.ExitNow()
|
raise asyncore.ExitNow()
|
||||||
|
|
||||||
|
elif cmd == "sign-pkcs7":
|
||||||
|
signer = self.server.private_key.signer(
|
||||||
|
padding.PKCS1v15(),
|
||||||
|
hashes.SHA1()
|
||||||
|
)
|
||||||
|
signer.update(b64decode(body))
|
||||||
|
self.send(b64encode(signer.finalize()))
|
||||||
|
|
||||||
|
elif cmd == "decrypt-pkcs7":
|
||||||
|
self.send(b64encode(self.server.private_key.decrypt(b64decode(body), padding.PKCS1v15())))
|
||||||
|
self.close()
|
||||||
|
|
||||||
elif cmd == "sign-request":
|
elif cmd == "sign-request":
|
||||||
# Only common name and public key are used from request
|
# Only common name and public key are used from request
|
||||||
request = x509.load_pem_x509_csr(body, default_backend())
|
request = x509.load_pem_x509_csr(body, default_backend())
|
||||||
|
@ -56,6 +56,9 @@ request subnets = 0.0.0.0/0
|
|||||||
# Certificates are automatically signed for these subnets
|
# Certificates are automatically signed for these subnets
|
||||||
autosign subnets = 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
autosign subnets = 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
||||||
|
|
||||||
|
# Simple Certificate Enrollment Protocol enabled subnets
|
||||||
|
scep subnets =
|
||||||
|
|
||||||
[logging]
|
[logging]
|
||||||
# Disable logging
|
# Disable logging
|
||||||
;backend =
|
;backend =
|
||||||
|
Loading…
Reference in New Issue
Block a user