Skip to content

Commit 3e1c6ef

Browse files
committed
Initial code
1 parent 3e42e84 commit 3e1c6ef

File tree

121 files changed

+11056
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+11056
-0
lines changed

cryptoOps.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright 2022 Keyfactor
2+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
3+
# not use this file except in compliance with the License. You may obtain a
4+
# copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless
5+
# required by applicable law or agreed to in writing, software distributed
6+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
7+
# OR CONDITIONS OF ANY KIND, either express or implied. See the License for
8+
# thespecific language governing permissions and limitations under the
9+
# License.
10+
from cryptography import x509
11+
from cryptography.x509.oid import NameOID
12+
from cryptography.hazmat.primitives.asymmetric import rsa
13+
from cryptography.hazmat.primitives import serialization, hashes
14+
import cryptography.hazmat.backends as backends
15+
16+
oids = {"C":NameOID.COUNTRY_NAME, "CN":NameOID.COMMON_NAME, "E":NameOID.EMAIL_ADDRESS, "L":NameOID.LOCALITY_NAME, "O":NameOID.ORGANIZATION_NAME, "OU":NameOID.ORGANIZATIONAL_UNIT_NAME, "S":NameOID.STATE_OR_PROVINCE_NAME}
17+
backend = backends.default_backend()
18+
19+
def genRSAKeypair(keySize):
20+
return rsa.generate_private_key(public_exponent=65537,key_size=keySize,backend=backend)
21+
22+
def getRSAPrivateKey(keypair):
23+
bstr = keypair.private_bytes(serialization.Encoding.PEM, encryption_algorithm = serialization.NoEncryption(), format=serialization.PrivateFormat.PKCS8)
24+
return str(bstr.decode("UTF-8")).replace("\\n","\n")
25+
26+
def buildSubject(DN):
27+
return x509.Name([x509.NameAttribute(oids[rdn[0]],rdn[1]) for rdn in map(lambda x: x.split('='),DN.split(','))])
28+
29+
def buildDNSSans(domains):
30+
return x509.SubjectAlternativeName([x509.DNSName(d) for d in domains])
31+
32+
def getPublicBytes(CSR):
33+
return str(CSR.public_bytes(serialization.Encoding.PEM).decode("UTF-8")).replace("\\n","\n")
34+
35+
def getThumbprint(pem):
36+
return x509.load_pem_x509_certificate(pem.encode("UTF-8"),backend=backend).fingerprint(hashes.SHA1()).hex()
37+
38+
def createCSR(DN,domainSANs=[],keySize=2048):
39+
csrBuilder = x509.CertificateSigningRequestBuilder().subject_name(buildSubject(DN)).add_extension(buildDNSSans(domainSANs),critical=False)
40+
key = genRSAKeypair(keySize)
41+
csr = csrBuilder.sign(key, hashes.SHA256(),backend=backend)
42+
return (getPublicBytes(csr),getRSAPrivateKey(key))
43+

ejbca/.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
__pycache__/
2+
build/
3+
dist/
4+
*.egg-info/
5+
.pytest_cache/
6+
7+
# pyenv
8+
.python-version
9+
10+
# Environments
11+
.env
12+
.venv
13+
14+
# mypy
15+
.mypy_cache/
16+
.dmypy.json
17+
dmypy.json
18+
19+
# JetBrains
20+
.idea/
21+
22+
/coverage.xml
23+
/.coverage

ejbca/README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# ejbca-rest-interface-client
2+
A client library for accessing EJBCA REST Interface
3+
4+
## Usage
5+
First, create a client:
6+
7+
```python
8+
from ejbca_rest_interface_client import Client
9+
10+
client = Client(base_url="https://api.example.com")
11+
```
12+
13+
If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead:
14+
15+
```python
16+
from ejbca_rest_interface_client import AuthenticatedClient
17+
18+
client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken")
19+
```
20+
21+
Now call your endpoint and use your models:
22+
23+
```python
24+
from ejbca_rest_interface_client.models import MyDataModel
25+
from ejbca_rest_interface_client.api.my_tag import get_my_data_model
26+
from ejbca_rest_interface_client.types import Response
27+
28+
my_data: MyDataModel = get_my_data_model.sync(client=client)
29+
# or if you need more info (e.g. status_code)
30+
response: Response[MyDataModel] = get_my_data_model.sync_detailed(client=client)
31+
```
32+
33+
Or do the same thing with an async version:
34+
35+
```python
36+
from ejbca_rest_interface_client.models import MyDataModel
37+
from ejbca_rest_interface_client.api.my_tag import get_my_data_model
38+
from ejbca_rest_interface_client.types import Response
39+
40+
my_data: MyDataModel = await get_my_data_model.asyncio(client=client)
41+
response: Response[MyDataModel] = await get_my_data_model.asyncio_detailed(client=client)
42+
```
43+
44+
By default, when you're calling an HTTPS API it will attempt to verify that SSL is working correctly. Using certificate verification is highly recommended most of the time, but sometimes you may need to authenticate to a server (especially an internal server) using a custom certificate bundle.
45+
46+
```python
47+
client = AuthenticatedClient(
48+
base_url="https://internal_api.example.com",
49+
token="SuperSecretToken",
50+
verify_ssl="/path/to/certificate_bundle.pem",
51+
)
52+
```
53+
54+
You can also disable certificate validation altogether, but beware that **this is a security risk**.
55+
56+
```python
57+
client = AuthenticatedClient(
58+
base_url="https://internal_api.example.com",
59+
token="SuperSecretToken",
60+
verify_ssl=False
61+
)
62+
```
63+
64+
Things to know:
65+
1. Every path/method combo becomes a Python module with four functions:
66+
1. `sync`: Blocking request that returns parsed data (if successful) or `None`
67+
1. `sync_detailed`: Blocking request that always returns a `Request`, optionally with `parsed` set if the request was successful.
68+
1. `asyncio`: Like `sync` but async instead of blocking
69+
1. `asyncio_detailed`: Like `sync_detailed` but async instead of blocking
70+
71+
1. All path/query params, and bodies become method arguments.
72+
1. If your endpoint had any tags on it, the first tag will be used as a module name for the function (my_tag above)
73+
1. Any endpoint which did not have a tag will be in `ejbca_rest_interface_client.api.default`
74+
75+
## Building / publishing this Client
76+
This project uses [Poetry](https://python-poetry.org/) to manage dependencies and packaging. Here are the basics:
77+
1. Update the metadata in pyproject.toml (e.g. authors, version)
78+
1. If you're using a private repository, configure it with Poetry
79+
1. `poetry config repositories.<your-repository-name> <url-to-your-repository>`
80+
1. `poetry config http-basic.<your-repository-name> <username> <password>`
81+
1. Publish the client with `poetry publish --build -r <your-repository-name>` or, if for public PyPI, just `poetry publish --build`
82+
83+
If you want to install this client into another project without publishing it (e.g. for development) then:
84+
1. If that project **is using Poetry**, you can simply do `poetry add <path-to-this-client>` from that project
85+
1. If that project is not using Poetry:
86+
1. Build a wheel with `poetry build -f wheel`
87+
1. Install that wheel from the other project `pip install <path-to-wheel>`

ejbca/pyproject.toml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[tool.poetry]
2+
name = "ejbca-rest-interface-client"
3+
version = "1.0"
4+
description = "A client library for accessing EJBCA REST Interface"
5+
6+
authors = []
7+
8+
readme = "README.md"
9+
packages = [
10+
{include = "ejbca_rest_interface_client"},
11+
]
12+
include = ["CHANGELOG.md", "ejbca_rest_interface_client/py.typed"]
13+
14+
[tool.poetry.dependencies]
15+
python = "^3.7"
16+
httpx = ">=0.15.4,<0.24.0"
17+
attrs = ">=21.3.0"
18+
python-dateutil = "^2.8.0"
19+
20+
[build-system]
21+
requires = ["poetry-core>=1.0.0"]
22+
build-backend = "poetry.core.masonry.api"
23+
24+
[tool.black]
25+
line-length = 120
26+
target_version = ['py37', 'py38', 'py39']
27+
exclude = '''
28+
(
29+
/(
30+
| \.git
31+
| \.venv
32+
| \.mypy_cache
33+
)/
34+
)
35+
'''
36+
37+
[tool.isort]
38+
line_length = 120
39+
profile = "black"

ejbca/rest_client/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright 2022 Keyfactor
2+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
3+
# not use this file except in compliance with the License. You may obtain a
4+
# copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless
5+
# required by applicable law or agreed to in writing, software distributed
6+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
7+
# OR CONDITIONS OF ANY KIND, either express or implied. See the License for
8+
# thespecific language governing permissions and limitations under the
9+
# License.
10+
""" A client library for accessing EJBCA REST Interface """
11+
from .client import AuthenticatedClient, Client

ejbca/rest_client/api/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright 2022 Keyfactor
2+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
3+
# not use this file except in compliance with the License. You may obtain a
4+
# copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless
5+
# required by applicable law or agreed to in writing, software distributed
6+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
7+
# OR CONDITIONS OF ANY KIND, either express or implied. See the License for
8+
# thespecific language governing permissions and limitations under the
9+
# License.
10+
""" Contains methods for accessing the API """
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copyright 2022 Keyfactor
2+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
3+
# not use this file except in compliance with the License. You may obtain a
4+
# copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless
5+
# required by applicable law or agreed to in writing, software distributed
6+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
7+
# OR CONDITIONS OF ANY KIND, either express or implied. See the License for
8+
# thespecific language governing permissions and limitations under the
9+
# License.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Copyright 2022 Keyfactor
2+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
3+
# not use this file except in compliance with the License. You may obtain a
4+
# copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless
5+
# required by applicable law or agreed to in writing, software distributed
6+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
7+
# OR CONDITIONS OF ANY KIND, either express or implied. See the License for
8+
# thespecific language governing permissions and limitations under the
9+
# License.
10+
from typing import Any, Dict, Optional, Union
11+
12+
import httpx
13+
14+
from ...client import Client
15+
from ...models.create_crl_rest_response import CreateCrlRestResponse
16+
from ...types import UNSET, Response, Unset
17+
18+
19+
def _get_kwargs(
20+
issuer_dn: str,
21+
*,
22+
client: Client,
23+
deltacrl: Union[Unset, None, bool] = False,
24+
) -> Dict[str, Any]:
25+
url = "{}/v1/ca/{issuer_dn}/createcrl".format(client.base_url, issuer_dn=issuer_dn)
26+
27+
headers: Dict[str, str] = client.get_headers()
28+
cookies: Dict[str, Any] = client.get_cookies()
29+
30+
params: Dict[str, Any] = {}
31+
params["deltacrl"] = deltacrl
32+
33+
params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
34+
35+
return {
36+
"method": "post",
37+
"url": url,
38+
"headers": headers,
39+
"cookies": cookies,
40+
"timeout": client.get_timeout(),
41+
"params": params,
42+
}
43+
44+
45+
def _parse_response(*, response: httpx.Response) -> Optional[CreateCrlRestResponse]:
46+
if response.status_code == 200:
47+
response_200 = CreateCrlRestResponse.from_dict(response.json())
48+
49+
return response_200
50+
return None
51+
52+
53+
def _build_response(*, response: httpx.Response) -> Response[CreateCrlRestResponse]:
54+
return Response(
55+
status_code=response.status_code,
56+
content=response.content,
57+
headers=response.headers,
58+
parsed=_parse_response(response=response),
59+
)
60+
61+
62+
def sync_detailed(
63+
issuer_dn: str,
64+
*,
65+
client: Client,
66+
deltacrl: Union[Unset, None, bool] = False,
67+
) -> Response[CreateCrlRestResponse]:
68+
"""Create CRL(main, partition and delta) issued by this CA
69+
70+
Args:
71+
issuer_dn (str):
72+
deltacrl (Union[Unset, None, bool]):
73+
74+
Returns:
75+
Response[CreateCrlRestResponse]
76+
"""
77+
78+
kwargs = _get_kwargs(
79+
issuer_dn=issuer_dn,
80+
client=client,
81+
deltacrl=deltacrl,
82+
)
83+
84+
response = httpx.request(
85+
cert=client.cert,
86+
verify=client.verify_ssl,
87+
**kwargs,
88+
)
89+
90+
return _build_response(response=response)
91+
92+
93+
def sync(
94+
issuer_dn: str,
95+
*,
96+
client: Client,
97+
deltacrl: Union[Unset, None, bool] = False,
98+
) -> Optional[CreateCrlRestResponse]:
99+
"""Create CRL(main, partition and delta) issued by this CA
100+
101+
Args:
102+
issuer_dn (str):
103+
deltacrl (Union[Unset, None, bool]):
104+
105+
Returns:
106+
Response[CreateCrlRestResponse]
107+
"""
108+
109+
return sync_detailed(
110+
issuer_dn=issuer_dn,
111+
client=client,
112+
deltacrl=deltacrl,
113+
).parsed
114+
115+
116+
async def asyncio_detailed(
117+
issuer_dn: str,
118+
*,
119+
client: Client,
120+
deltacrl: Union[Unset, None, bool] = False,
121+
) -> Response[CreateCrlRestResponse]:
122+
"""Create CRL(main, partition and delta) issued by this CA
123+
124+
Args:
125+
issuer_dn (str):
126+
deltacrl (Union[Unset, None, bool]):
127+
128+
Returns:
129+
Response[CreateCrlRestResponse]
130+
"""
131+
132+
kwargs = _get_kwargs(
133+
issuer_dn=issuer_dn,
134+
client=client,
135+
deltacrl=deltacrl,
136+
)
137+
138+
async with httpx.AsyncClient(verify=client.verify_ssl) as _client:
139+
response = await _client.request(**kwargs)
140+
141+
return _build_response(response=response)
142+
143+
144+
async def asyncio(
145+
issuer_dn: str,
146+
*,
147+
client: Client,
148+
deltacrl: Union[Unset, None, bool] = False,
149+
) -> Optional[CreateCrlRestResponse]:
150+
"""Create CRL(main, partition and delta) issued by this CA
151+
152+
Args:
153+
issuer_dn (str):
154+
deltacrl (Union[Unset, None, bool]):
155+
156+
Returns:
157+
Response[CreateCrlRestResponse]
158+
"""
159+
160+
return (
161+
await asyncio_detailed(
162+
issuer_dn=issuer_dn,
163+
client=client,
164+
deltacrl=deltacrl,
165+
)
166+
).parsed

0 commit comments

Comments
 (0)