acme_issuing_service.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2019 New Vector Ltd
  3. # Copyright 2019 The Matrix.org Foundation C.I.C.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. """
  17. Utility function to create an ACME issuing service.
  18. This file contains the unconditional imports on the acme and cryptography bits that we
  19. only need (and may only have available) if we are doing ACME, so is designed to be
  20. imported conditionally.
  21. """
  22. import logging
  23. import attr
  24. from cryptography.hazmat.backends import default_backend
  25. from cryptography.hazmat.primitives import serialization
  26. from josepy import JWKRSA
  27. from josepy.jwa import RS256
  28. from txacme.challenges import HTTP01Responder
  29. from txacme.client import Client
  30. from txacme.interfaces import ICertificateStore
  31. from txacme.service import AcmeIssuingService
  32. from txacme.util import generate_private_key
  33. from zope.interface import implementer
  34. from twisted.internet import defer
  35. from twisted.python.filepath import FilePath
  36. from twisted.python.url import URL
  37. logger = logging.getLogger(__name__)
  38. def create_issuing_service(reactor, acme_url, account_key_file, well_known_resource):
  39. """Create an ACME issuing service, and attach it to a web Resource
  40. Args:
  41. reactor: twisted reactor
  42. acme_url (str): URL to use to request certificates
  43. account_key_file (str): where to store the account key
  44. well_known_resource (twisted.web.IResource): web resource for .well-known.
  45. we will attach a child resource for "acme-challenge".
  46. Returns:
  47. AcmeIssuingService
  48. """
  49. responder = HTTP01Responder()
  50. well_known_resource.putChild(b"acme-challenge", responder.resource)
  51. store = ErsatzStore()
  52. return AcmeIssuingService(
  53. cert_store=store,
  54. client_creator=(
  55. lambda: Client.from_url(
  56. reactor=reactor,
  57. url=URL.from_text(acme_url),
  58. key=load_or_create_client_key(account_key_file),
  59. alg=RS256,
  60. )
  61. ),
  62. clock=reactor,
  63. responders=[responder],
  64. )
  65. @attr.s
  66. @implementer(ICertificateStore)
  67. class ErsatzStore(object):
  68. """
  69. A store that only stores in memory.
  70. """
  71. certs = attr.ib(default=attr.Factory(dict))
  72. def store(self, server_name, pem_objects):
  73. self.certs[server_name] = [o.as_bytes() for o in pem_objects]
  74. return defer.succeed(None)
  75. def load_or_create_client_key(key_file):
  76. """Load the ACME account key from a file, creating it if it does not exist.
  77. Args:
  78. key_file (str): name of the file to use as the account key
  79. """
  80. # this is based on txacme.endpoint.load_or_create_client_key, but doesn't
  81. # hardcode the 'client.key' filename
  82. acme_key_file = FilePath(key_file)
  83. if acme_key_file.exists():
  84. logger.info("Loading ACME account key from '%s'", acme_key_file)
  85. key = serialization.load_pem_private_key(
  86. acme_key_file.getContent(), password=None, backend=default_backend()
  87. )
  88. else:
  89. logger.info("Saving new ACME account key to '%s'", acme_key_file)
  90. key = generate_private_key("rsa")
  91. acme_key_file.setContent(
  92. key.private_bytes(
  93. encoding=serialization.Encoding.PEM,
  94. format=serialization.PrivateFormat.TraditionalOpenSSL,
  95. encryption_algorithm=serialization.NoEncryption(),
  96. )
  97. )
  98. return JWKRSA(key=key)