Internal Documentation

See also: Module Index

serles.create_app()

initialize web app

This function should be passed to the WSGI server.

class serles.backends.ejbca.EjbcaBackend(config)

Serles Backend for EJBCA (Community edition compatible)

Uses the EJBCA SOAP API to request certificates. Recommended setup on the EJBCA side:

  • A Certificate Profile for ACME-issued certificates, e.g. “ACMEServerProfile”. Must have an Extended Key Usage of “Server Authentication”. Should have relatively short Validity duration.

  • An End Entity Profile, e.g. “ACMEEndIdentityProfile”. Uses the Server Certificate Profile mentioned above and should allow for a number of DNS Name Subject Alternative Names.

To connect to the API, the following setup is used:

  • A Certificate Profile for authenticating EjbcaBackend with EJBCA’s SOAP API, e.g. “APIClientProfile”. Must have an Extended Key Usage of “Client Authentication”.

  • An End Entity Profile for client authentication, e.g. “APIClientIdentityProfile”. Uses the Client Certificate Profile mentioned above.

  • A concrete End Entity for EjbcaBackend, e.g. “client01”. Uses the Client Entity Profile mentioned above. Should have its common name same as its user name.

  • A certificate must be issued for this entity and its location stored in config.ini.

  • An Administrator Role for API clients, e.g. “ACMEUser”. From advanced mode, requires access to the Rules (<> denote variables)

    • /administrator

    • /ca_functionality/create_certificate

    • /ra_functionality/create_end_entity

    • /ra_functionality/edit_end_entity

    • /ca/<NAME_OF_CA> (e.g. /ca/ACMECA)

    • /endentityprofilesrules/<END_ENTITY_PROFILE>/create_end_entity (e.g. /endentityprofilesrules/ACMEEndIdentityProfile/create_end_entity/)

    • /endentityprofilesrules/<END_ENTITY_PROFILE>/edit_end_entity (e.g. /endentityprofilesrules/ACMEEndIdentityProfile/edit_end_entity/)

  • Ensure the client entity is in the correct Administrator Role (e.g. via CN).

serles.backends.ejbca.pkcs7_to_pem_chain(pkcs7_input)

Converts a PKCS#7 cert chain to PEM format.

Parameters:

pkcs7_input (bytes) – the PKCS#7 chain as stored in the database.

Returns:

PEM encoded certificate chain as expected by ACME clients.

Return type:

str

class serles.backends.base.Backend(config)

Abstract Base Backend

Inherit from this and implement sign(), and optionally __init__() to use this.

class serles.backends.certbot.Backend(config)

Serles Backend for certbot

Backend will pass the certificate signing request to an already configured certbot installation.

Example use cases:
  1. Want actual trusted certs without using an internal CA (you want it to work in browsers/etc. out of the box)

  2. Don’t want the services externally exposed (internal services/development only)

  3. Don’t want the internal services having control over your external DNS (random developers/users)

  4. Have an intermediate server running serles that you are willing to grant external DNS update rights to.

This allows any internal entity to transparently use serles ACME CA with http-01 validation, but the actual signing requests are delegated to external ACME CA.

Contributed by Nathan Neulinger.

class serles.models.Account(**kwargs)

To avoid having to send the large public key for each request, a client registers an Account and identifies itself using the key id. With Let’s Encrypt, accounts persist over a long time, and some clients will try to keep using it. Certbot is especially bad at this, as it fails when the account it expects doesn’t exist. This object also stores an optional contact email address, which we pass to the backend (e.g. for notifications regarding certificate expiry).

query: t.ClassVar[Query]

A SQLAlchemy query for a model. Equivalent to db.session.query(Model). Can be customized per-model by overriding query_class.

Warning

The query interface is considered legacy in SQLAlchemy. Prefer using session.execute(select()) instead.

class serles.models.AccountStatus(*values)

RFC 8555 § 7.1.6

class serles.models.Authorization(**kwargs)

For each Identifier (domain name) the client requested in an Order, there is a Authorization. To obtain a certificate, the client must satisfy all of them. To satisfy an Authorization, the client can solve any one of the Challenges within (i.e., only 1 Challenge per Authorization is required).

query: t.ClassVar[Query]

A SQLAlchemy query for a model. Equivalent to db.session.query(Model). Can be customized per-model by overriding query_class.

Warning

The query interface is considered legacy in SQLAlchemy. Prefer using session.execute(select()) instead.

class serles.models.AuthzStatus(*values)

RFC 8555 § 7.1.6 (Fig.2)

class serles.models.Certificate(**kwargs)

Pretty much what it says on the tin. Stored in the database for the short time between the client requesting finalization of their order and them fetching the cert from us.

query: t.ClassVar[Query]

A SQLAlchemy query for a model. Equivalent to db.session.query(Model). Can be customized per-model by overriding query_class.

Warning

The query interface is considered legacy in SQLAlchemy. Prefer using session.execute(select()) instead.

class serles.models.Challenge(**kwargs)

A completed Challenge satisfies an Authorization. We support the HTTP-01 challenge type, which requires the client to temporarily serve a short text string on a location we decide.

query: t.ClassVar[Query]

A SQLAlchemy query for a model. Equivalent to db.session.query(Model). Can be customized per-model by overriding query_class.

Warning

The query interface is considered legacy in SQLAlchemy. Prefer using session.execute(select()) instead.

class serles.models.ChallengeStatus(*values)

RFC 8555 § 7.1.6 (Fig.1)

class serles.models.ChallengeTypes(*values)
class serles.models.Identifier(**kwargs)

An Identifier is essentially a domain name and some metadata.

query: t.ClassVar[Query]

A SQLAlchemy query for a model. Equivalent to db.session.query(Model). Can be customized per-model by overriding query_class.

Warning

The query interface is considered legacy in SQLAlchemy. Prefer using session.execute(select()) instead.

class serles.models.IdentifierTypes(*values)
class serles.models.Nonces(**kwargs)

To avoid replay attacks, each HTTP POST request must come with a nonce we issued.

classmethod check(value)

returns True iff the nonce is valid (not yet used)

query: t.ClassVar[Query]

A SQLAlchemy query for a model. Equivalent to db.session.query(Model). Can be customized per-model by overriding query_class.

Warning

The query interface is considered legacy in SQLAlchemy. Prefer using session.execute(select()) instead.

class serles.models.Order(**kwargs)

In ACME lingo, an Order identifies a client’s request for a certificate. It keeps a list of Identifiers (domain names) requested from the client, a list of Authorizations (see below), and (once we decided to issue one) the certificate.

query: t.ClassVar[Query]

A SQLAlchemy query for a model. Equivalent to db.session.query(Model). Can be customized per-model by overriding query_class.

Warning

The query interface is considered legacy in SQLAlchemy. Prefer using session.execute(select()) instead.

class serles.models.OrderStatus(*values)

RFC 8555 § 7.1.6 (Fig.3)

class serles.models.UTCDateTime(*args: Any, **kwargs: Any)

SQLite stores datetimes without TZ info, and SQLAlchemy then returns TZ-less datetimes. This breaks calculating timedeltas. So this wrapper converts incoming timestamps to UTC before storing and adds the TZ (utc) back on retrieval.

cache_ok = True

Indicate if statements using this ExternalType are “safe to cache”.

The default value None will emit a warning and then not allow caching of a statement which includes this type. Set to False to disable statements using this type from being cached at all without a warning. When set to True, the object’s class and selected elements from its state will be used as part of the cache key. For example, using a TypeDecorator:

class MyType(TypeDecorator):
    impl = String

    cache_ok = True

    def __init__(self, choices):
        self.choices = tuple(choices)
        self.internal_only = True

The cache key for the above type would be equivalent to:

>>> MyType(["a", "b", "c"])._static_cache_key
(<class '__main__.MyType'>, ('choices', ('a', 'b', 'c')))

The caching scheme will extract attributes from the type that correspond to the names of parameters in the __init__() method. Above, the “choices” attribute becomes part of the cache key but “internal_only” does not, because there is no parameter named “internal_only”.

The requirements for cacheable elements is that they are hashable and also that they indicate the same SQL rendered for expressions using this type every time for a given cache value.

To accommodate for datatypes that refer to unhashable structures such as dictionaries, sets and lists, these objects can be made “cacheable” by assigning hashable structures to the attributes whose names correspond with the names of the arguments. For example, a datatype which accepts a dictionary of lookup values may publish this as a sorted series of tuples. Given a previously un-cacheable type as:

class LookupType(UserDefinedType):
    """a custom type that accepts a dictionary as a parameter.

    this is the non-cacheable version, as "self.lookup" is not
    hashable.

    """

    def __init__(self, lookup):
        self.lookup = lookup

    def get_col_spec(self, **kw):
        return "VARCHAR(255)"

    def bind_processor(self, dialect): ...  # works with "self.lookup" ...

Where “lookup” is a dictionary. The type will not be able to generate a cache key:

>>> type_ = LookupType({"a": 10, "b": 20})
>>> type_._static_cache_key
<stdin>:1: SAWarning: UserDefinedType LookupType({'a': 10, 'b': 20}) will not
produce a cache key because the ``cache_ok`` flag is not set to True.
Set this flag to True if this type object's state is safe to use
in a cache key, or False to disable this warning.
symbol('no_cache')

If we did set up such a cache key, it wouldn’t be usable. We would get a tuple structure that contains a dictionary inside of it, which cannot itself be used as a key in a “cache dictionary” such as SQLAlchemy’s statement cache, since Python dictionaries aren’t hashable:

>>> # set cache_ok = True
>>> type_.cache_ok = True

>>> # this is the cache key it would generate
>>> key = type_._static_cache_key
>>> key
(<class '__main__.LookupType'>, ('lookup', {'a': 10, 'b': 20}))

>>> # however this key is not hashable, will fail when used with
>>> # SQLAlchemy statement cache
>>> some_cache = {key: "some sql value"}
Traceback (most recent call last): File "<stdin>", line 1,
in <module> TypeError: unhashable type: 'dict'

The type may be made cacheable by assigning a sorted tuple of tuples to the “.lookup” attribute:

class LookupType(UserDefinedType):
    """a custom type that accepts a dictionary as a parameter.

    The dictionary is stored both as itself in a private variable,
    and published in a public variable as a sorted tuple of tuples,
    which is hashable and will also return the same value for any
    two equivalent dictionaries.  Note it assumes the keys and
    values of the dictionary are themselves hashable.

    """

    cache_ok = True

    def __init__(self, lookup):
        self._lookup = lookup

        # assume keys/values of "lookup" are hashable; otherwise
        # they would also need to be converted in some way here
        self.lookup = tuple((key, lookup[key]) for key in sorted(lookup))

    def get_col_spec(self, **kw):
        return "VARCHAR(255)"

    def bind_processor(self, dialect): ...  # works with "self._lookup" ...

Where above, the cache key for LookupType({"a": 10, "b": 20}) will be:

>>> LookupType({"a": 10, "b": 20})._static_cache_key
(<class '__main__.LookupType'>, ('lookup', (('a', 10), ('b', 20))))

Added in version 1.4.14: - added the cache_ok flag to allow some configurability of caching for TypeDecorator classes.

Added in version 1.4.28: - added the ExternalType mixin which generalizes the cache_ok flag to both the TypeDecorator and UserDefinedType classes.

See also

sql_caching

impl

alias of DateTime

process_bind_param(val, _)

Receive a bound parameter value to be converted.

Custom subclasses of _types.TypeDecorator should override this method to provide custom behaviors for incoming data values. This method is called at statement execution time and is passed the literal Python data value which is to be associated with a bound parameter in the statement.

The operation could be anything desired to perform custom behavior, such as transforming or serializing data. This could also be used as a hook for validating logic.

Parameters:
  • value – Data to operate upon, of any type expected by this method in the subclass. Can be None.

  • dialect – the Dialect in use.

See also

types_typedecorator

_types.TypeDecorator.process_result_value()

process_result_value(val, _)

Receive a result-row column value to be converted.

Custom subclasses of _types.TypeDecorator should override this method to provide custom behaviors for data values being received in result rows coming from the database. This method is called at result fetching time and is passed the literal Python data value that’s extracted from a database result row.

The operation could be anything desired to perform custom behavior, such as transforming or deserializing data.

Parameters:
  • value – Data to operate upon, of any type expected by this method in the subclass. Can be None.

  • dialect – the Dialect in use.

See also

types_typedecorator

_types.TypeDecorator.process_bind_param()

serles.challenge.additional_ip_address_checks(config, remote_ip, host, is_ipaddress=False)

perform additional checks on the remote IP address

These are useful in an enterprise setting, but not required by spec.

Parameters:
  • remote_ip (str) – the IP address which we connected to for challenge verification

  • host (str) – dNSname which we resolved to get remote_ip

  • is_ipaddress (bool) – whether host is an IP address (skips verifyPTR)

Returns:

An error, if one occured, or None.

Return type:

Optional[str]

serles.challenge.alpn_challenge(challenge)

verify a TLS-ALPN-01 Challenge

Parameters:

challenge (Challenge) – The TLS-ALPN-01 challenge to verify.

Returns:

problem detail type of the error and textual description, or (None,None).

Return type:

tuple(str,str)

serles.challenge.dns_challenge(challenge)

verify a DNS-01 Challenge

Parameters:

challenge (Challenge) – The DNS-01 challenge to verify.

Returns:

problem detail type of the error and textual description, or (None,None).

Return type:

tuple(str,str)

serles.challenge.http_challenge(challenge)

verify a HTTP Challenge

Parameters:

challenge (Challenge) – The HTTP challenge to verify.

Returns:

problem detail type of the error and textual description, or (None,None).

Return type:

tuple(str,str)

serles.challenge.key_authorization(challenge)

build key authorization string from challenge

Parameters:

challenge (models.Challenge) – a challenge object

Returns:

key authorization string

Return type:

str

serles.challenge.verify_challenge(challenge)

verify a challenge

Parameters:

challenge (Challenge) – The challenge to verify.

Returns:

True on success, False on error.

Return type:

bool

Raises:

ACMEError – Verification failed.

exception serles.configloader.ConfigError

This exception is raised when an error occurred while reading the config.

serles.configloader.get_config()

Reads the configuration from the environment variable or the default path.

Returns:

A tuple of config, backend.

Return type:

(dict, object)

serles.configloader.load_config_and_backend(filename)

Parses the config file given, or raises an exception. This is called directly on startup (as opposed to when a certificate request comes in) to alert the administrator to erros immediately.

Parameters:

filename – config file to load.

Returns:

A tuple containing the Backend class and the parsed config (dict-like)

Return type:

(object, configparser.ConfigParser)

Raises:

ConfigError – The config could not be loaded, is missing a required key or the specified Backend could not be loaded.

exception serles.exceptions.ACMEError(message, status, error_type)

Raise this exception on invalid API usage. The exception handler will return an Error Document to the client.

Parameters:
  • message (str) – returned to client as the “detail” field.

  • status (int) – HTTP status code for the response.

  • error_type (str) – type token from the ACME namespace.

serles.flask_handlers.exception_handler(error)

This function is called by Flask when an exception is raised. We use the our ACMEError to return errors for failed API requests. Other exceptions are caught and transformed into an Internal Server Error and we log the exception for later review. Responses are in Problem Details format (RFC7807), albeit only with minimal content.

serles.flask_handlers.parse_jws()

Verify that the signature is as specified by the RFC, and make the payload available to all POST views using Flask’s g object. Note that we aren’t fully checking every detail, just the security relevant ones. Note also that we don’t support key rollover (as required by spec).

This function is registered as a before_request handler and augments the g object with the following attributes:

  • g.payload: the actually interesting request data, JSON-decoded

  • g.kid: the JWK key id (in our case a uuid) of the user, if known

  • g.jwk: the JWK public key, when a user tries to register

Raises:

ACMEError – The request was not understood or not authorized.

serles.utils.background_job(interval)

executes the decorated function in an interval

A very simple scheduler: decorate a function to call it in its own thread in a given interval.

Parameters:

interval (int) – number of seconds between executions.

serles.utils.base64d(s)

padding-ignoring base64-decoder

ACME uses url-safe base64, but does not add padding. Python’s base64 lib throws an exception on wrong padding, so we add it for it.

Parameters:

s (str) – input to decode.

Returns:

decoded input.

Return type:

bytes

serles.utils.ber_parse(b)

parse X.690 type/length/value tuple to string

python-cryptography for some reason does not trim type info and the length off the value, so we do it by hand.

Parameters:

b (bytes) – byte sequence

Returns:

decoded string

Return type:

str

serles.utils.get_ptr(ipaddr)

resolve an IP address to a domain name.

Parameters:

ipaddr (str) – query.

Returns:

FQDN, or None.

Return type:

str

serles.utils.ip_in_ranges(ipaddr, ranges)

check whether the given IP is in any of the given subnets.

Parameters:
  • ipaddr (str) – IP to check.

  • ranges (list) – list of ipaddress.IPv4Network or ipaddress.IPv6Network.

Returns:

True if it is, False if it isn’t.

Return type:

bool

serles.utils.normalize(domain)

don’t differentiate between FQDN and PartiallyQDN, ignore case

Parameters:

domain (str) – domain name to normalize.

Returns:

normalized domain name.

Return type:

str

serles.utils.query(qname, rdtype)

Query DNS records without raising an exception.

Parameters:
  • qname (str) – query name.

  • rdtype (str) – query type.

Returns:

results, or the empty list on error.

Return type:

list

class serles.views.AccountMain
methods: ClassVar[Collection[str] | None] = {'POST'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

post(kid)

View or update the specified Account object.

Parameters:

kid – JSON Web Key ID that identifies the account

Returns:

JSON-serialized Account object (post-update).

class serles.views.AccountOrders

see AccountMain.

methods: ClassVar[Collection[str] | None] = {'POST'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

class serles.views.AuthorizationMain
methods: ClassVar[Collection[str] | None] = {'POST'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

post(authid)

View the specified Authorization object that contains challenges.

Parameters:

authid

Returns:

JSON-serialized Authorization object.

class serles.views.CertificateMain
methods: ClassVar[Collection[str] | None] = {'POST'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

post(certid)

Download the specified certificate. Only the client who requested the order may access it.

Parameters:

certid

Returns:

PEM encoded certificate chain.

class serles.views.ChallengeMain

Once the client calls this endpoint, we can start verifying the challenge.

methods: ClassVar[Collection[str] | None] = {'POST'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

class serles.views.Directory
get()

Displays the URLs for accessing certain functions, and some metadata.

methods: ClassVar[Collection[str] | None] = {'GET'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

class serles.views.KeyChange
methods: ClassVar[Collection[str] | None] = {'POST'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

class serles.views.LandingPage
get()

return a 200 OK message on / so Users know what this is.

methods: ClassVar[Collection[str] | None] = {'GET'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

class serles.views.NewAccount
methods: ClassVar[Collection[str] | None] = {'POST'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

post()

Request a new Account or get the Key ID associated with a JSON Web Key.

class serles.views.NewAuthz
methods: ClassVar[Collection[str] | None] = {'POST'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

class serles.views.NewNonce

Lets the client fetch a nonce, if they ran out of them.

methods: ClassVar[Collection[str] | None] = {'GET', 'HEAD'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

class serles.views.NewOrder
methods: ClassVar[Collection[str] | None] = {'POST'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

post()

Submit a new Order. The request will include a list of Identifers (domain names) the client wants on the certificate.

class serles.views.OrderFinalize
methods: ClassVar[Collection[str] | None] = {'POST'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

post(orderid)

Upload CSR and (if order is ready) start issuance process.

Returns:

JSON-serialized Order object, now including a certificate id.

class serles.views.OrderMain
methods: ClassVar[Collection[str] | None] = {'POST'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.

post(orderid)

View the specified Order object. :param orderid:

Returns:

JSON-serialized Order object.

class serles.views.RevokeCert
methods: ClassVar[Collection[str] | None] = {'POST'}

The methods this view is registered for. Uses the same default (["GET", "HEAD", "OPTIONS"]) as route and add_url_rule by default.