Skip to content

GH-177 - Adds support for DER formatted certficates #336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,26 @@
.DS_Store
.coverage
.vagrant
/.vscode/
/*.egg
/*.egg-info
/*.eggs
/.conda/
/.idea/
/.jupyter/
/.local/
/.venv/
/.pipenv-requires
/.venv/
/.vscode/
/build/
/dist/
/docs/docs/changelog.md
/docs/docs/index.md
/node_modules/
/notebooks*/
/rsconnect-build
/rsconnect-build-test
/rsconnect/version.py
htmlcov
/tests/testdata/**/rsconnect-python/
htmlcov
test-home/
/docs/docs/index.md
/docs/docs/changelog.md
/rsconnect-build
/rsconnect-build-test
venv
14 changes: 7 additions & 7 deletions rsconnect/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@

import re
from warnings import warn
from six import text_type
import gc

from . import validation
from .certificates import read_certificate_file
from .http_support import HTTPResponse, HTTPServer, append_to_path, CookieJar
from .log import logger, connect_logger, cls_logged, console_logger
from .models import AppModes
Expand Down Expand Up @@ -360,7 +360,7 @@ def __init__(
url: str = None,
api_key: str = None,
insecure: bool = False,
cacert: IO = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cacert should be handled as a file, hence IO; whereas cadata is text, hence str

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. Here the cacert variable is a string representing the file path, instead of a file pointer. The call on line 436 opens the file pointer, which was originally handled by Click), and reads the file contents in either text mode or byte mode depending on the file-type.

The existing implementation assumes that the file pointer has been opened in text mode, which doesn't work when using DER files since they are encoded in binary format. L436 onwards, cadata is either a str or bytes, both of which are accepted when creating an SSLContext. This is the same as the existing implementation.

The Click documentation goes into scenario a little further: https://click.palletsprojects.com/en/8.1.x/arguments/#file-path-arguments

cacert: str = None,
ca_data: str = None,
cookies=None,
account=None,
Expand Down Expand Up @@ -415,7 +415,7 @@ def setup_remote_server(
url: str = None,
api_key: str = None,
insecure: bool = False,
cacert: IO = None,
cacert: str = None,
ca_data: str = None,
account_name: str = None,
token: str = None,
Expand All @@ -433,7 +433,7 @@ def setup_remote_server(
)

if cacert and not ca_data:
ca_data = text_type(cacert.read())
ca_data = read_certificate_file(cacert)

server_data = ServerStore().resolve(name, url)
if server_data.from_store:
Expand Down Expand Up @@ -507,7 +507,7 @@ def validate_server(
url: str = None,
api_key: str = None,
insecure: bool = False,
cacert: IO = None,
cacert: str = None,
api_key_is_required: bool = False,
account_name: str = None,
token: str = None,
Expand All @@ -528,7 +528,7 @@ def validate_connect_server(
url: str = None,
api_key: str = None,
insecure: bool = False,
cacert: IO = None,
cacert: str = None,
api_key_is_required: bool = False,
**kwargs
):
Expand All @@ -551,7 +551,7 @@ def validate_connect_server(

ca_data = None
if cacert:
ca_data = text_type(cacert.read())
ca_data = read_certificate_file(cacert)
api_key = api_key or self.remote_server.api_key
insecure = insecure or self.remote_server.insecure
if not ca_data:
Expand Down
37 changes: 37 additions & 0 deletions rsconnect/certificates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from pathlib import Path

BINARY_ENCODED_FILETYPES = [".cer", ".der"]
TEXT_ENCODED_FILETYPES = [".ca-bundle", ".crt", ".key", ".pem"]


def read_certificate_file(location: str):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are changing cacert to location then we need to be internally consistent and change existing cacert to something like cacert_location.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going down that route can potentially break backwards compatibility so that's an important aspect to consider.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @bcwu - could you help clarify your comment. Are you suggesting that I rename the variables named cacert to cacert_location so that the variable type is more obvious?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a little more digging. It looks like cafile is the typical nomenclature for file name references. https://docs.python.org/3/library/ssl.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about a couple of things:

  1. Whether changing the type hints of cacert (from IO to str) will break any existing behavior
  • In this case either way (type hint set to IO or str) is probably all right. In general it helps to think through whether changes are backwards compatible.
  1. Possibly rename cacert if we do change the type hint
  • this will probably cause more confusion than it clarifies. It will certainly break existing behavior; so, on second thought not a good idea, please disregard.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the usage of cacert in each of the methods where the type signature is changed. In each case the variable is only used in combination with read_certificate_file. So, this shouldn't break any exisiting behavior.

"""Reads a certificate file from disk.

The file type (suffix) is used to determine the file encoding.
Assumption are made based on standard SSL practices.

Files ending in '.cer' and '.der' are assumed DER (Distinguished
Encoding Rules) files encoded in binary format.

Files ending in '.ca-bundle', '.crt', '.key', and '.pem' are PEM
(Privacy Enhanced Mail) files encoded in plain-text format.
"""

path = Path(location)
suffix = path.suffix
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking the file type by examining its magic number is a safer way of inference than checking the suffix. This especially pertains to security related files like certificates.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. I'll look into this a bit.

Do you happen to know of any documentation that shows how to accomplish this across each of the certificate types?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use mimetypes to infer some file types. It may not have full coverage of certificates, but it's a good starting point.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't appear that certificates contain standard header blocks, but PEM files and DER files both adhere to a standard. https://www.openssl.org/docs/man3.0/man1/openssl-format-options.html

I can add additional validation that inspects the contents to check expecatations based on this documentation. But, I'm not sure if that is necessary.

@mmarchetti - do you have any additional insights into this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mimetypes determines types by file suffix, so it's probably equivalent to what we're doing. I don't think we need to inspect file contents - if an invalid certificate file is provided, it will fail with an SSL error when we try to connect.


if suffix in BINARY_ENCODED_FILETYPES:
with open(path, "rb") as file:
return file.read()

if suffix in TEXT_ENCODED_FILETYPES:
with open(path, "r") as file:
return file.read()

types = BINARY_ENCODED_FILETYPES + TEXT_ENCODED_FILETYPES
types = sorted(types)
types = [f"'{_}'" for _ in types]
human_readable_string = ", ".join(types[:-1]) + ", or " + types[-1]
raise RuntimeError(
f"The certificate file type is not recognized. Expected {human_readable_string}. Found '{suffix}'."
)
35 changes: 20 additions & 15 deletions rsconnect/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import typing
import textwrap
import click
from six import text_type
from os.path import abspath, dirname, exists, isdir, join
from functools import wraps

from rsconnect.certificates import read_certificate_file

from .environment import EnvironmentException
from .exception import RSConnectException
from .actions import (
Expand Down Expand Up @@ -129,7 +131,7 @@ def server_args(func):
"--cacert",
"-c",
envvar="CONNECT_CA_CERTIFICATE",
type=click.File(),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
help="The path to trusted TLS CA certificates.",
)
@click.option("--verbose", "-v", is_flag=True, help="Print detailed messages.")
Expand Down Expand Up @@ -269,7 +271,7 @@ def _test_server_and_api(server, api_key, insecure, ca_cert):
:return: a tuple containing an appropriate ConnectServer object and the username
of the user the API key represents (or None, if no key was provided).
"""
ca_data = ca_cert and text_type(ca_cert.read())
ca_data = ca_cert and ca_cert.read()
me = None

with cli_feedback("Checking %s" % server):
Expand Down Expand Up @@ -312,7 +314,7 @@ def _test_rstudio_creds(server: api.PositServer):
"--cacert",
"-c",
envvar="CONNECT_CA_CERTIFICATE",
type=click.File(),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
help="The path to trusted TLS CA certificates.",
)
@click.option(
Expand Down Expand Up @@ -344,7 +346,10 @@ def bootstrap(
logger.debug("Generated JWT:\n" + bootstrap_token)

logger.debug("Insecure: " + str(insecure))
ca_data = cacert and text_type(cacert.read())

ca_data = None
if cacert:
ca_data = read_certificate_file(cacert)

with cli_feedback("", stderr=True):
connect_server = RSConnectServer(
Expand Down Expand Up @@ -398,7 +403,7 @@ def bootstrap(
"--cacert",
"-c",
envvar="CONNECT_CA_CERTIFICATE",
type=click.File(),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
help="The path to trusted TLS CA certificates.",
)
@click.option("--verbose", "-v", is_flag=True, help="Print detailed messages.")
Expand Down Expand Up @@ -1674,7 +1679,7 @@ def content():
"--cacert",
"-c",
envvar="CONNECT_CA_CERTIFICATE",
type=click.File(),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
help="The path to trusted TLS CA certificates.",
)
@click.option(
Expand Down Expand Up @@ -1768,7 +1773,7 @@ def content_search(
"--cacert",
"-c",
envvar="CONNECT_CA_CERTIFICATE",
type=click.File(),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
help="The path to trusted TLS CA certificates.",
)
@click.option(
Expand Down Expand Up @@ -1819,7 +1824,7 @@ def content_describe(name, server, api_key, insecure, cacert, guid, verbose):
"--cacert",
"-c",
envvar="CONNECT_CA_CERTIFICATE",
type=click.File(),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
help="The path to trusted TLS CA certificates.",
)
@click.option(
Expand Down Expand Up @@ -1888,7 +1893,7 @@ def build():
"--cacert",
"-c",
envvar="CONNECT_CA_CERTIFICATE",
type=click.File(),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
help="The path to trusted TLS CA certificates.",
)
@click.option(
Expand Down Expand Up @@ -1942,7 +1947,7 @@ def add_content_build(name, server, api_key, insecure, cacert, guid, verbose):
"--cacert",
"-c",
envvar="CONNECT_CA_CERTIFICATE",
type=click.File(),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
help="The path to trusted TLS CA certificates.",
)
@click.option(
Expand Down Expand Up @@ -2005,7 +2010,7 @@ def remove_content_build(name, server, api_key, insecure, cacert, guid, all, pur
"--cacert",
"-c",
envvar="CONNECT_CA_CERTIFICATE",
type=click.File(),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
help="The path to trusted TLS CA certificates.",
)
@click.option("--status", type=click.Choice(BuildStatus._all), help="Filter results by status of the build operation.")
Expand Down Expand Up @@ -2053,7 +2058,7 @@ def list_content_build(name, server, api_key, insecure, cacert, status, guid, ve
"--cacert",
"-c",
envvar="CONNECT_CA_CERTIFICATE",
type=click.File(),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
help="The path to trusted TLS CA certificates.",
)
@click.option(
Expand Down Expand Up @@ -2104,7 +2109,7 @@ def get_build_history(name, server, api_key, insecure, cacert, guid, verbose):
"--cacert",
"-c",
envvar="CONNECT_CA_CERTIFICATE",
type=click.File(),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
help="The path to trusted TLS CA certificates.",
)
@click.option(
Expand Down Expand Up @@ -2167,7 +2172,7 @@ def get_build_logs(name, server, api_key, insecure, cacert, guid, task_id, forma
"--cacert",
"-c",
envvar="CONNECT_CA_CERTIFICATE",
type=click.File(),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
help="The path to trusted TLS CA certificates.",
)
@click.option(
Expand Down
40 changes: 40 additions & 0 deletions tests/test_certificates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from tempfile import NamedTemporaryFile
from unittest import TestCase

from rsconnect.certificates import read_certificate_file


class ParseCertificateFileTestCase(TestCase):

def test_parse_certificate_file_ca_bundle(self):
res = read_certificate_file("tests/testdata/certificates/localhost.ca-bundle")
self.assertTrue(res)

def test_parse_certificate_file_cer(self):
res = read_certificate_file("tests/testdata/certificates/localhost.cer")
self.assertTrue(res)

def test_parse_certificate_file_crt(self):
res = read_certificate_file("tests/testdata/certificates/localhost.crt")
self.assertTrue(res)

def test_parse_certificate_file_der(self):
res = read_certificate_file("tests/testdata/certificates/localhost.der")
self.assertTrue(res)

def test_parse_certificate_file_key(self):
res = read_certificate_file("tests/testdata/certificates/localhost.key")
self.assertTrue(res)

def test_parse_certificate_file_pem(self):
res = read_certificate_file("tests/testdata/certificates/localhost.pem")
self.assertTrue(res)

def test_parse_certificate_file_csr(self):
with self.assertRaises(RuntimeError):
read_certificate_file("tests/testdata/certificates/localhost.csr")

def test_parse_certificate_file_invalid(self):
with NamedTemporaryFile() as tmpfile:
with self.assertRaises(RuntimeError):
read_certificate_file(tmpfile.name)
38 changes: 38 additions & 0 deletions tests/testdata/certificates/localhost.ca-bundle
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-----BEGIN CERTIFICATE-----
MIIDETCCAfkCFGouyhXhN5LZGv4+gVbr+IXMNIB+MA0GCSqGSIb3DQEBCwUAMEUx
CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl
cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjMwMTE4MTkwMTA0WhcNMjQwMTE4MTkw
MTA0WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE
CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAw8rWVc8S3nU86ybTRpjus3+AvSoQF7yf5yvFzcm8lnNptOsA
xP9l4NnnWwdJv87JtUAAdL0MxRrWuhsZFAJh5aCMOGL5mdib80dIy7gqNrg6H2lq
6ciM5yEpcJo29qtEjfExmRYHNQRbd5OjhggzAj2oCLXPQqSRNVfsmNsm/AxkdVXf
tJzOEEulb1yh9FMqn8skWxbFuuua/iVzxX5/r9R8BVdsFNlH69cwV2nI/OAOySHg
zHwWZKVUtQfOkv9jm6CCzpLRrnqarvXt6GmaQFGuqFaAebjPMlp/53csMWQCsrp7
Psy/JXKUJ3Dogk2PvziEgzGg2Nf2f2lKXr3WZQIDAQABMA0GCSqGSIb3DQEBCwUA
A4IBAQB3UsUO5XWlzaO6LsGFCsNbHxH+LxJsejGmnABQt6qzwFF1fs6ixl0RkUsE
6/wKKGZdZw9fDotQeDB7zYfQmOqVJtBh/yrmBsW9qzBIJTp/0RSlNsYueyrnGMi5
+3g+KHLhnOD9tvnPiz8Haoln2XaGM8iZ+HlVUHxHViWqKTDRcBOmtjglFHmsNy+S
UFYqgjVbRZNLWOhkC+7LMQutOzfPbO/5zrCSc4nUUhWEYa4AsmYKKMu10VH/EXtB
QgkX8ZIRa1P8iIe1YDghyjEKBU1WCR7bvbryMTp0HjbSGCuNiiddMesPdwkmPH5/
Y/tWij/h+jS8s6uPtyz4KQUcP7gg
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDETCCAfkCFAro99muVq+KLo92JIXJVyMtLnmBMA0GCSqGSIb3DQEBCwUAMEUx
CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl
cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjMwMTE4MTkwMzQwWhcNMjQwMTE4MTkw
MzQwWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE
CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAw8rWVc8S3nU86ybTRpjus3+AvSoQF7yf5yvFzcm8lnNptOsA
xP9l4NnnWwdJv87JtUAAdL0MxRrWuhsZFAJh5aCMOGL5mdib80dIy7gqNrg6H2lq
6ciM5yEpcJo29qtEjfExmRYHNQRbd5OjhggzAj2oCLXPQqSRNVfsmNsm/AxkdVXf
tJzOEEulb1yh9FMqn8skWxbFuuua/iVzxX5/r9R8BVdsFNlH69cwV2nI/OAOySHg
zHwWZKVUtQfOkv9jm6CCzpLRrnqarvXt6GmaQFGuqFaAebjPMlp/53csMWQCsrp7
Psy/JXKUJ3Dogk2PvziEgzGg2Nf2f2lKXr3WZQIDAQABMA0GCSqGSIb3DQEBCwUA
A4IBAQC+Fx+w7kWmvbwrduzYqKW4k8oFFrjolp1x9Hw7cbpx0qoF5sQadOaYMSt2
sBbdZ0qGqlYbRSOYlK9CbXWfzb2Q6ibi6JqkWU+05NYQecj/p1uEwHOxbsvyV2dt
SEGYkJzyRhxS0gI97A4ati7uQ57ptM2LcsVK2Fu+C3DlpU9FrIXnvZj3I+S+WZ7s
sYmqv2/Rf8z+Sy0qhxVwbHslKWuQJpXoUpXwOpZNiXgfV1VnjMTaWaagF0MobsHT
MzE6Dj6f6v2NkfQj7gNQs9ueg4uaACRlUzM/E6Xx1eCL2VXl5nX1ytqW4gKFvWF7
xeuMzMhz0EsvN4hhQKEMWDH7p31q
-----END CERTIFICATE-----
Binary file added tests/testdata/certificates/localhost.cer
Binary file not shown.
19 changes: 19 additions & 0 deletions tests/testdata/certificates/localhost.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDETCCAfkCFGouyhXhN5LZGv4+gVbr+IXMNIB+MA0GCSqGSIb3DQEBCwUAMEUx
CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl
cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjMwMTE4MTkwMTA0WhcNMjQwMTE4MTkw
MTA0WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE
CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAw8rWVc8S3nU86ybTRpjus3+AvSoQF7yf5yvFzcm8lnNptOsA
xP9l4NnnWwdJv87JtUAAdL0MxRrWuhsZFAJh5aCMOGL5mdib80dIy7gqNrg6H2lq
6ciM5yEpcJo29qtEjfExmRYHNQRbd5OjhggzAj2oCLXPQqSRNVfsmNsm/AxkdVXf
tJzOEEulb1yh9FMqn8skWxbFuuua/iVzxX5/r9R8BVdsFNlH69cwV2nI/OAOySHg
zHwWZKVUtQfOkv9jm6CCzpLRrnqarvXt6GmaQFGuqFaAebjPMlp/53csMWQCsrp7
Psy/JXKUJ3Dogk2PvziEgzGg2Nf2f2lKXr3WZQIDAQABMA0GCSqGSIb3DQEBCwUA
A4IBAQB3UsUO5XWlzaO6LsGFCsNbHxH+LxJsejGmnABQt6qzwFF1fs6ixl0RkUsE
6/wKKGZdZw9fDotQeDB7zYfQmOqVJtBh/yrmBsW9qzBIJTp/0RSlNsYueyrnGMi5
+3g+KHLhnOD9tvnPiz8Haoln2XaGM8iZ+HlVUHxHViWqKTDRcBOmtjglFHmsNy+S
UFYqgjVbRZNLWOhkC+7LMQutOzfPbO/5zrCSc4nUUhWEYa4AsmYKKMu10VH/EXtB
QgkX8ZIRa1P8iIe1YDghyjEKBU1WCR7bvbryMTp0HjbSGCuNiiddMesPdwkmPH5/
Y/tWij/h+jS8s6uPtyz4KQUcP7gg
-----END CERTIFICATE-----
16 changes: 16 additions & 0 deletions tests/testdata/certificates/localhost.csr
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICijCCAXICAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAMPK1lXPEt51POsm00aY7rN/gL0qEBe8n+crxc3J
vJZzabTrAMT/ZeDZ51sHSb/OybVAAHS9DMUa1robGRQCYeWgjDhi+ZnYm/NHSMu4
Kja4Oh9paunIjOchKXCaNvarRI3xMZkWBzUEW3eTo4YIMwI9qAi1z0KkkTVX7Jjb
JvwMZHVV37SczhBLpW9cofRTKp/LJFsWxbrrmv4lc8V+f6/UfAVXbBTZR+vXMFdp
yPzgDskh4Mx8FmSlVLUHzpL/Y5uggs6S0a56mq717ehpmkBRrqhWgHm4zzJaf+d3
LDFkArK6ez7MvyVylCdw6IJNj784hIMxoNjX9n9pSl691mUCAwEAAaAAMA0GCSqG
SIb3DQEBCwUAA4IBAQCbHHGn119/PjC7gd18PEGYGss/7rSylpo8FlHT28SI/YjJ
HTO2ebD0jU4nHcJo8ihOTBUaGRhzURXMX81ernRnFMTy87XKA7FDt5HXLX3i5EYC
89S3S8ncaDHC99D1vG4hGAAFuUYi8JWipKaCBKgoHm6VsYqCQeVOPll7PSC3Szyc
aWR/emEcz/tHXG5a6it6PsWo3jKDPeSPDUtlzZcUz4ssbQih8uEdoTBKwk4mnaCc
XVndRRfPnhHMOr9BdCjd+LYU0PRtJpsv7pqbHPTQEPEYh7GD99d5jDKtff6J4YkM
ljLCS+SOPMMTJTEaGuR7NvuIcoj4vODOSEkSUxv9
-----END CERTIFICATE REQUEST-----
Binary file added tests/testdata/certificates/localhost.der
Binary file not shown.
Loading