Mini Shell
# Majority of code shamelessly stolen from
# http://www.v13.gr/blog/?p=303
"""
Authenticate via a PKI certificate.
.. note::
This module is Experimental and should be used with caution
Provides an authenticate function that will allow the caller to authenticate
a user via their public cert against a pre-defined Certificate Authority.
TODO: Add a 'ca_dir' option to configure a directory of CA files, a la Apache.
:depends: - pyOpenSSL module
"""
import logging
import salt.utils.files
# pylint: disable=import-error
try:
try:
from M2Crypto import X509
HAS_M2 = True
except ImportError:
HAS_M2 = False
try:
from Cryptodome.Util import asn1
except ImportError:
from Crypto.Util import asn1 # nosec
import OpenSSL
HAS_DEPS = True
except ImportError:
HAS_DEPS = False
# pylint: enable=import-error
log = logging.getLogger(__name__)
def __virtual__():
"""
Requires newer pycrypto and pyOpenSSL
"""
if HAS_DEPS:
return True
return False
def auth(username, password, **kwargs):
"""
Returns True if the given user cert (password is the cert contents)
was issued by the CA and if cert's Common Name is equal to username.
Returns False otherwise.
``username``: we need it to run the auth function from CLI/API;
it should be in master config auth/acl
``password``: contents of user certificate (pem-encoded user public key);
why "password"? For CLI, it's the only available name
Configure the CA cert in the master config file:
.. code-block:: yaml
external_auth:
pki:
ca_file: /etc/pki/tls/ca_certs/trusted-ca.crt
your_user:
- .*
"""
pem = password
cacert_file = __salt__["config.get"]("external_auth:pki:ca_file")
log.debug("Attempting to authenticate via pki.")
log.debug("Using CA file: %s", cacert_file)
log.debug("Certificate contents: %s", pem)
if HAS_M2:
cert = X509.load_cert_string(pem, X509.FORMAT_PEM)
cacert = X509.load_cert(cacert_file, X509.FORMAT_PEM)
if cert.verify(cacert.get_pubkey()):
log.info("Successfully authenticated certificate: %s", pem)
return True
log.info("Failed to authenticate certificate: %s", pem)
return False
c = OpenSSL.crypto # pylint: disable=used-before-assignment
cert = c.load_certificate(c.FILETYPE_PEM, pem)
with salt.utils.files.fopen(cacert_file) as f:
cacert = c.load_certificate(c.FILETYPE_PEM, f.read())
# Get the signing algorithm
algo = cert.get_signature_algorithm()
# Get the ASN1 format of the certificate
cert_asn1 = c.dump_certificate(c.FILETYPE_ASN1, cert)
# Decode the certificate
der = asn1.DerSequence() # pylint: disable=used-before-assignment
der.decode(cert_asn1)
# The certificate has three parts:
# - certificate
# - signature algorithm
# - signature
# http://usefulfor.com/nothing/2009/06/10/x509-certificate-basics/
der_cert = der[0]
# der_algo = der[1]
der_sig = der[2]
# The signature is a BIT STRING (Type 3)
# Decode that as well
der_sig_in = asn1.DerObject()
der_sig_in.decode(der_sig)
# Get the payload
sig0 = der_sig_in.payload
# Do the following to see a validation error for tests
# der_cert=der_cert[:20]+'1'+der_cert[21:]
# First byte is the number of unused bits. This should be 0
# http://msdn.microsoft.com/en-us/library/windows/desktop/bb540792(v=vs.85).aspx
if sig0[0] != "\x00":
raise Exception("Number of unused bits is strange")
# Now get the signature itself
sig = sig0[1:]
# And verify the certificate
try:
c.verify(cacert, sig, der_cert, algo)
assert (
dict(cert.get_subject().get_components())["CN"] == username
), "Certificate's CN should match the username"
log.info("Successfully authenticated certificate: %s", pem)
return True
except (OpenSSL.crypto.Error, AssertionError):
log.info("Failed to authenticate certificate: %s", pem)
return False
Zerion Mini Shell 1.0