pan-chainguard 0.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pan-chainguard-0.0.0/LICENSE.txt +16 -0
- pan-chainguard-0.0.0/PKG-INFO +77 -0
- pan-chainguard-0.0.0/README.rst +61 -0
- pan-chainguard-0.0.0/bin/cert-fingerprints.sh +29 -0
- pan-chainguard-0.0.0/bin/chain.py +466 -0
- pan-chainguard-0.0.0/bin/fling.py +206 -0
- pan-chainguard-0.0.0/bin/guard.py +373 -0
- pan-chainguard-0.0.0/pan_chainguard/__init__.py +18 -0
- pan-chainguard-0.0.0/pan_chainguard/crtsh.py +87 -0
- pan-chainguard-0.0.0/pan_chainguard.egg-info/PKG-INFO +77 -0
- pan-chainguard-0.0.0/pan_chainguard.egg-info/SOURCES.txt +15 -0
- pan-chainguard-0.0.0/pan_chainguard.egg-info/dependency_links.txt +1 -0
- pan-chainguard-0.0.0/pan_chainguard.egg-info/requires.txt +2 -0
- pan-chainguard-0.0.0/pan_chainguard.egg-info/top_level.txt +1 -0
- pan-chainguard-0.0.0/pyproject.toml +3 -0
- pan-chainguard-0.0.0/setup.cfg +31 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
pan-chainguard is distributed with an ISC license
|
|
2
|
+
(https://cvsweb.openbsd.org/src/share/misc/license.template?rev=HEAD):
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2024 Palo Alto Networks, Inc.
|
|
5
|
+
|
|
6
|
+
Permission to use, copy, modify, and distribute this software for any
|
|
7
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
8
|
+
copyright notice and this permission notice appear in all copies.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
11
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
12
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
13
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
14
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
15
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
16
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pan-chainguard
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Preload Trusted CA Intermediate Certificate Chains on PAN-OS
|
|
5
|
+
Home-page: https://github.com/PaloAltoNetworks/pan-chainguard
|
|
6
|
+
Author: Palo Alto Networks, Inc.
|
|
7
|
+
Author-email: devrel@paloaltonetworks.com
|
|
8
|
+
License: ISC
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: ISC License (ISCL)
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/x-rst
|
|
13
|
+
License-File: LICENSE.txt
|
|
14
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
15
|
+
Requires-Dist: pan-python>=0.25.0
|
|
16
|
+
|
|
17
|
+
pan-chainguard - Preload Trusted CA Intermediate Certificate Chains on PAN-OS
|
|
18
|
+
=============================================================================
|
|
19
|
+
|
|
20
|
+
``pan-chainguard`` is a Python3 application which uses
|
|
21
|
+
`CCADB data
|
|
22
|
+
<https://www.ccadb.org/resources>`_
|
|
23
|
+
to derive intermediate certificate chains for trusted
|
|
24
|
+
certificate authorities in PAN-OS so they can be
|
|
25
|
+
`preloaded
|
|
26
|
+
<https://wiki.mozilla.org/Security/CryptoEngineering/Intermediate_Preloading>`_
|
|
27
|
+
as device certificates.
|
|
28
|
+
|
|
29
|
+
Problem
|
|
30
|
+
-------
|
|
31
|
+
|
|
32
|
+
Many TLS enabled origin servers suffer from a misconfiguration in
|
|
33
|
+
which they:
|
|
34
|
+
|
|
35
|
+
#. Do not return intermediate CA certificates.
|
|
36
|
+
#. Return certificates out of order.
|
|
37
|
+
#. Return intermediate certificates which are not related to the CA
|
|
38
|
+
which signed the server certificate.
|
|
39
|
+
|
|
40
|
+
The impact for PAN-OS SSL decryption administrators is end users will
|
|
41
|
+
see errors such as *unable to get local issuer certificate* until the
|
|
42
|
+
sites that are misconfigured are
|
|
43
|
+
`identified
|
|
44
|
+
<https://docs.paloaltonetworks.com/pan-os/11-1/pan-os-admin/decryption/troubleshoot-and-monitor-decryption/decryption-logs/repair-incomplete-certificate-chains>`_,
|
|
45
|
+
the required intermediate certificates are obtained, and the
|
|
46
|
+
certificates are imported into PAN-OS.
|
|
47
|
+
|
|
48
|
+
Solution: Intermediate CA Preloading
|
|
49
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
50
|
+
|
|
51
|
+
``pan-chainguard`` uses the PAN-OS default trusted CA store and the
|
|
52
|
+
*All Certificate Information* CCADB data file as input, and determines
|
|
53
|
+
the intermediate certificate chains, if available, for each root CA
|
|
54
|
+
certificate. These can then be added to PAN-OS as trusted CA device
|
|
55
|
+
certificates.
|
|
56
|
+
|
|
57
|
+
By preloading known intermediates for the trusted CAs, the number of
|
|
58
|
+
TLS connection errors that users encounter for misconfigured servers
|
|
59
|
+
can be reduced, without reactive actions by an administrator.
|
|
60
|
+
|
|
61
|
+
Documentation
|
|
62
|
+
-------------
|
|
63
|
+
|
|
64
|
+
- Administrator's Guide:
|
|
65
|
+
|
|
66
|
+
https://github.com/PaloAltoNetworks/pan-chainguard/blob/main/doc/admin-guide.rst
|
|
67
|
+
|
|
68
|
+
Install ``pan-chainguard``
|
|
69
|
+
--------------------------
|
|
70
|
+
|
|
71
|
+
``pan-chainguard`` is available as a
|
|
72
|
+
`release
|
|
73
|
+
<https://github.com/PaloAltoNetworks/pan-chainguard/releases/>`_
|
|
74
|
+
on GitHub and as a
|
|
75
|
+
`package
|
|
76
|
+
<https://pypi.org/project/pan-chainguard/>`_
|
|
77
|
+
on PyPi.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
pan-chainguard - Preload Trusted CA Intermediate Certificate Chains on PAN-OS
|
|
2
|
+
=============================================================================
|
|
3
|
+
|
|
4
|
+
``pan-chainguard`` is a Python3 application which uses
|
|
5
|
+
`CCADB data
|
|
6
|
+
<https://www.ccadb.org/resources>`_
|
|
7
|
+
to derive intermediate certificate chains for trusted
|
|
8
|
+
certificate authorities in PAN-OS so they can be
|
|
9
|
+
`preloaded
|
|
10
|
+
<https://wiki.mozilla.org/Security/CryptoEngineering/Intermediate_Preloading>`_
|
|
11
|
+
as device certificates.
|
|
12
|
+
|
|
13
|
+
Problem
|
|
14
|
+
-------
|
|
15
|
+
|
|
16
|
+
Many TLS enabled origin servers suffer from a misconfiguration in
|
|
17
|
+
which they:
|
|
18
|
+
|
|
19
|
+
#. Do not return intermediate CA certificates.
|
|
20
|
+
#. Return certificates out of order.
|
|
21
|
+
#. Return intermediate certificates which are not related to the CA
|
|
22
|
+
which signed the server certificate.
|
|
23
|
+
|
|
24
|
+
The impact for PAN-OS SSL decryption administrators is end users will
|
|
25
|
+
see errors such as *unable to get local issuer certificate* until the
|
|
26
|
+
sites that are misconfigured are
|
|
27
|
+
`identified
|
|
28
|
+
<https://docs.paloaltonetworks.com/pan-os/11-1/pan-os-admin/decryption/troubleshoot-and-monitor-decryption/decryption-logs/repair-incomplete-certificate-chains>`_,
|
|
29
|
+
the required intermediate certificates are obtained, and the
|
|
30
|
+
certificates are imported into PAN-OS.
|
|
31
|
+
|
|
32
|
+
Solution: Intermediate CA Preloading
|
|
33
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
34
|
+
|
|
35
|
+
``pan-chainguard`` uses the PAN-OS default trusted CA store and the
|
|
36
|
+
*All Certificate Information* CCADB data file as input, and determines
|
|
37
|
+
the intermediate certificate chains, if available, for each root CA
|
|
38
|
+
certificate. These can then be added to PAN-OS as trusted CA device
|
|
39
|
+
certificates.
|
|
40
|
+
|
|
41
|
+
By preloading known intermediates for the trusted CAs, the number of
|
|
42
|
+
TLS connection errors that users encounter for misconfigured servers
|
|
43
|
+
can be reduced, without reactive actions by an administrator.
|
|
44
|
+
|
|
45
|
+
Documentation
|
|
46
|
+
-------------
|
|
47
|
+
|
|
48
|
+
- Administrator's Guide:
|
|
49
|
+
|
|
50
|
+
https://github.com/PaloAltoNetworks/pan-chainguard/blob/main/doc/admin-guide.rst
|
|
51
|
+
|
|
52
|
+
Install ``pan-chainguard``
|
|
53
|
+
--------------------------
|
|
54
|
+
|
|
55
|
+
``pan-chainguard`` is available as a
|
|
56
|
+
`release
|
|
57
|
+
<https://github.com/PaloAltoNetworks/pan-chainguard/releases/>`_
|
|
58
|
+
on GitHub and as a
|
|
59
|
+
`package
|
|
60
|
+
<https://pypi.org/project/pan-chainguard/>`_
|
|
61
|
+
on PyPi.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
usage() {
|
|
4
|
+
echo "usage: $(basename $0) cert-directory"
|
|
5
|
+
exit 1
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
# Usage: fingerprints directory
|
|
9
|
+
fingerprints() {
|
|
10
|
+
dir=$1
|
|
11
|
+
|
|
12
|
+
echo '"filename","sha256"'
|
|
13
|
+
for file in $(ls $dir/*.cer); do
|
|
14
|
+
fp=$(openssl x509 -noout -fingerprint -sha256 -in $file)
|
|
15
|
+
fp=$(echo $fp | sed -e 's/.*=//')
|
|
16
|
+
fp=$(echo $fp | sed -e 's/://g')
|
|
17
|
+
echo \"$(basename $file)\",\"$fp\"
|
|
18
|
+
done
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if [ $# != 1 ] || [ $1 = '--help' ]; then
|
|
22
|
+
usage
|
|
23
|
+
fi
|
|
24
|
+
if [ ! -d $1 ]; then
|
|
25
|
+
echo "$1: not directory"
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
fingerprints $1
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2024 Palo Alto Networks, Inc.
|
|
5
|
+
#
|
|
6
|
+
# Permission to use, copy, modify, and distribute this software for any
|
|
7
|
+
# purpose with or without fee is hereby granted, provided that the above
|
|
8
|
+
# copyright notice and this permission notice appear in all copies.
|
|
9
|
+
#
|
|
10
|
+
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
11
|
+
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
12
|
+
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
13
|
+
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
14
|
+
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
15
|
+
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
16
|
+
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
17
|
+
#
|
|
18
|
+
|
|
19
|
+
import aiohttp
|
|
20
|
+
import argparse
|
|
21
|
+
import asyncio
|
|
22
|
+
from collections import defaultdict
|
|
23
|
+
import csv
|
|
24
|
+
import datetime
|
|
25
|
+
import io
|
|
26
|
+
import os
|
|
27
|
+
import pprint
|
|
28
|
+
import sys
|
|
29
|
+
import tarfile
|
|
30
|
+
import time
|
|
31
|
+
|
|
32
|
+
libpath = os.path.dirname(os.path.abspath(__file__))
|
|
33
|
+
sys.path[:0] = [os.path.join(libpath, os.pardir)]
|
|
34
|
+
|
|
35
|
+
from pan_chainguard import (title, __version__)
|
|
36
|
+
from pan_chainguard.crtsh import (ArgsError, CrtShApi)
|
|
37
|
+
|
|
38
|
+
ROOT = 'root'
|
|
39
|
+
INTERMEDIATE = 'intermediate'
|
|
40
|
+
MAX_TASKS = 3 # concurrent crt.sh API requests
|
|
41
|
+
CRT_SH_TIMEOUT = 60
|
|
42
|
+
|
|
43
|
+
args = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main():
|
|
47
|
+
global args
|
|
48
|
+
args = parse_args()
|
|
49
|
+
|
|
50
|
+
asyncio.run(main_loop())
|
|
51
|
+
|
|
52
|
+
sys.exit(0)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def main_loop():
|
|
56
|
+
create_archive(test=True)
|
|
57
|
+
|
|
58
|
+
invalid, roots, intermediates, parents = get_certs()
|
|
59
|
+
if args.verbose:
|
|
60
|
+
print('roots %d, intermediates %d, parents %d' %
|
|
61
|
+
(len(roots), len(intermediates), len(parents)))
|
|
62
|
+
|
|
63
|
+
chains = get_cert_chains(roots, intermediates, parents)
|
|
64
|
+
if args.verbose:
|
|
65
|
+
print('chains %d (root=>intermediates)' % len(chains),
|
|
66
|
+
pprint.pformat(chains),
|
|
67
|
+
file=sys.stderr)
|
|
68
|
+
|
|
69
|
+
total, total_invalid, panos = await get_panos_intermediates(
|
|
70
|
+
chains, invalid)
|
|
71
|
+
print('%d intermediate chains found for %d PAN-OS trusted CAs' % (
|
|
72
|
+
len(panos), total))
|
|
73
|
+
if args.verbose:
|
|
74
|
+
print('PAN-OS intermediate chains', pprint.pformat(panos),
|
|
75
|
+
file=sys.stderr)
|
|
76
|
+
|
|
77
|
+
errors, certificates = await get_cert_files(panos)
|
|
78
|
+
if errors:
|
|
79
|
+
print('Warning: %d certificates were not downloaded')
|
|
80
|
+
|
|
81
|
+
if False and args.verbose:
|
|
82
|
+
print('PAN-OS certificates', pprint.pformat(certificates),
|
|
83
|
+
file=sys.stderr)
|
|
84
|
+
|
|
85
|
+
create_archive(certificates)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_certs():
|
|
89
|
+
invalid = {}
|
|
90
|
+
roots = []
|
|
91
|
+
intermediates = []
|
|
92
|
+
parents = defaultdict(list)
|
|
93
|
+
certs = {}
|
|
94
|
+
duplicates = defaultdict(list)
|
|
95
|
+
|
|
96
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
with open(args.ccadb, 'r', newline='') as csvfile:
|
|
100
|
+
reader = csv.DictReader(csvfile,
|
|
101
|
+
dialect='unix')
|
|
102
|
+
for row in reader:
|
|
103
|
+
sha256 = row['SHA-256 Fingerprint']
|
|
104
|
+
|
|
105
|
+
if row['Revocation Status'] not in ['', 'Not Revoked']:
|
|
106
|
+
x = '%s %s %s' % (row['Revocation Status'],
|
|
107
|
+
sha256,
|
|
108
|
+
row['Certificate Name'])
|
|
109
|
+
if args.verbose:
|
|
110
|
+
print(x, file=sys.stderr)
|
|
111
|
+
invalid[sha256] = x
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
fmt = '%Y.%m.%d %z'
|
|
115
|
+
valid_from = datetime.datetime.strptime(
|
|
116
|
+
row['Valid From (GMT)'] + ' +0000', fmt)
|
|
117
|
+
valid_to = datetime.datetime.strptime(
|
|
118
|
+
row['Valid To (GMT)'] + ' +0000', fmt)
|
|
119
|
+
if now < valid_from:
|
|
120
|
+
x = 'Not yet valid (valid from %s) %s %s' % (
|
|
121
|
+
row['Valid From (GMT)'],
|
|
122
|
+
sha256,
|
|
123
|
+
row['Certificate Name'])
|
|
124
|
+
if args.verbose:
|
|
125
|
+
print(x, file=sys.stderr)
|
|
126
|
+
invalid[sha256] = x
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
if valid_to < now:
|
|
130
|
+
x = 'Expired (valid to %s) %s %s' % (
|
|
131
|
+
row['Valid To (GMT)'],
|
|
132
|
+
sha256,
|
|
133
|
+
row['Certificate Name'])
|
|
134
|
+
if args.verbose:
|
|
135
|
+
print(x, file=sys.stderr)
|
|
136
|
+
invalid[sha256] = x
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
derived_trust_bits = row['Derived Trust Bits']
|
|
140
|
+
if (derived_trust_bits and 'Server Authentication' not in
|
|
141
|
+
derived_trust_bits.split(';')):
|
|
142
|
+
x = 'Missing Server Authentication %s' % sha256
|
|
143
|
+
invalid[sha256] = x
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
cert_type = row['Certificate Record Type']
|
|
147
|
+
|
|
148
|
+
if sha256 in certs:
|
|
149
|
+
if sha256 not in duplicates:
|
|
150
|
+
duplicates[sha256].append(certs[sha256])
|
|
151
|
+
duplicates[sha256].append(row)
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
certs[sha256] = row
|
|
155
|
+
|
|
156
|
+
if cert_type == 'Root Certificate':
|
|
157
|
+
roots.append(sha256)
|
|
158
|
+
|
|
159
|
+
if cert_type == 'Intermediate Certificate':
|
|
160
|
+
parent_sha256 = row['Parent SHA-256 Fingerprint']
|
|
161
|
+
if not parent_sha256:
|
|
162
|
+
x = 'Intermediate with no parent: %s' % sha256
|
|
163
|
+
if args.verbose:
|
|
164
|
+
print(x, file=sys.stderr)
|
|
165
|
+
invalid[sha256] = x
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
intermediates.append(sha256)
|
|
169
|
+
parents[parent_sha256].append(sha256)
|
|
170
|
+
|
|
171
|
+
except OSError as e:
|
|
172
|
+
print('%s: %s' % (args.ccadb, e), file=sys.stderr)
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
|
|
175
|
+
if args.verbose:
|
|
176
|
+
for sha256 in duplicates:
|
|
177
|
+
print('Duplicate certificates %s' % sha256, file=sys.stderr)
|
|
178
|
+
for x in duplicates[sha256]:
|
|
179
|
+
print(' %s %s %s' % (x['Certificate Name'],
|
|
180
|
+
x['Certificate Record Type'],
|
|
181
|
+
x['Salesforce Record ID']),
|
|
182
|
+
file=sys.stderr)
|
|
183
|
+
|
|
184
|
+
return invalid, roots, intermediates, parents
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_cert_chains(roots, intermediates, parents):
|
|
188
|
+
chains = {}
|
|
189
|
+
|
|
190
|
+
for k in roots:
|
|
191
|
+
if k not in parents:
|
|
192
|
+
if args.verbose:
|
|
193
|
+
print('Root with no child %s' % k,
|
|
194
|
+
file=sys.stderr)
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
for child in parents[k]:
|
|
198
|
+
chain = [k]
|
|
199
|
+
follow(chain, child, parents)
|
|
200
|
+
if args.verbose:
|
|
201
|
+
print('chain[%d]:' % len(chain),
|
|
202
|
+
pprint.pformat(chain), file=sys.stderr)
|
|
203
|
+
|
|
204
|
+
chains[chain[0]] = chain
|
|
205
|
+
|
|
206
|
+
return chains
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def follow(chain, k, parents):
|
|
210
|
+
if args.verbose:
|
|
211
|
+
print('follow[%d]:' % len(chain), k, file=sys.stderr)
|
|
212
|
+
|
|
213
|
+
chain.append(k)
|
|
214
|
+
if k in parents:
|
|
215
|
+
for child in parents[k]:
|
|
216
|
+
follow(chain, child, parents)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def get_panos_intermediates(chains, invalid):
|
|
220
|
+
intermediates = {}
|
|
221
|
+
total = 0
|
|
222
|
+
total_invalid = 0
|
|
223
|
+
|
|
224
|
+
if args.fingerprints:
|
|
225
|
+
try:
|
|
226
|
+
with open(args.fingerprints, 'r', newline='') as csvfile:
|
|
227
|
+
reader = csv.DictReader(csvfile,
|
|
228
|
+
dialect='unix')
|
|
229
|
+
for row in reader:
|
|
230
|
+
sha256 = row['sha256']
|
|
231
|
+
if sha256 in invalid:
|
|
232
|
+
print('Invalid PAN-OS certificate %s: %s' % (
|
|
233
|
+
row['filename'],
|
|
234
|
+
invalid[sha256]))
|
|
235
|
+
total_invalid += 1
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
total += 1
|
|
239
|
+
if sha256 in chains:
|
|
240
|
+
intermediates[row['filename']] = [(ROOT, sha256)]
|
|
241
|
+
for x in chains[sha256][1:]:
|
|
242
|
+
intermediates[row['filename']].append(
|
|
243
|
+
(INTERMEDIATE, x))
|
|
244
|
+
|
|
245
|
+
except OSError as e:
|
|
246
|
+
print('%s: %s' % (args.ccadb, e), file=sys.stderr)
|
|
247
|
+
sys.exit(1)
|
|
248
|
+
|
|
249
|
+
return total, total_invalid, intermediates
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
async def download(api, sequence, sha256):
|
|
253
|
+
tries = 0
|
|
254
|
+
MAX_TRIES = 5
|
|
255
|
+
RETRY_SLEEP = 5.0
|
|
256
|
+
|
|
257
|
+
while True:
|
|
258
|
+
tries += 1
|
|
259
|
+
try:
|
|
260
|
+
resp = await api.download(id=sha256[1])
|
|
261
|
+
|
|
262
|
+
if resp.status == 429:
|
|
263
|
+
x = '%d %s' % (resp.status, resp.reason)
|
|
264
|
+
if tries == MAX_TRIES:
|
|
265
|
+
print('no retry after try %d, %s' % (tries, x),
|
|
266
|
+
file=sys.stderr)
|
|
267
|
+
break
|
|
268
|
+
print('retry after try %d, %s' % (tries, x),
|
|
269
|
+
file=sys.stderr)
|
|
270
|
+
await asyncio.sleep(RETRY_SLEEP)
|
|
271
|
+
elif resp.status != 200:
|
|
272
|
+
x = 'download %d %s %s %s' % (
|
|
273
|
+
resp.status, resp.reason, sequence, sha256[1])
|
|
274
|
+
break
|
|
275
|
+
else:
|
|
276
|
+
filename, content = await api.content(resp=resp)
|
|
277
|
+
if filename is None:
|
|
278
|
+
filename = sha256[1] + '.crt'
|
|
279
|
+
|
|
280
|
+
start = '-----BEGIN CERTIFICATE-----'
|
|
281
|
+
end = '-----END CERTIFICATE-----\n'
|
|
282
|
+
if (content.startswith(start) and
|
|
283
|
+
content.endswith(end)):
|
|
284
|
+
return filename, content, sequence, sha256[0]
|
|
285
|
+
else:
|
|
286
|
+
# XXX ephemeral?
|
|
287
|
+
x = 'content malformed %s %s %s' % (
|
|
288
|
+
sequence, sha256[1], content)
|
|
289
|
+
if tries == MAX_TRIES:
|
|
290
|
+
print('no retry after try %d, %s' % (tries, x),
|
|
291
|
+
file=sys.stderr)
|
|
292
|
+
break
|
|
293
|
+
print('retry after try %d, %s' % (tries, x),
|
|
294
|
+
file=sys.stderr)
|
|
295
|
+
await asyncio.sleep(RETRY_SLEEP)
|
|
296
|
+
|
|
297
|
+
except (asyncio.TimeoutError,
|
|
298
|
+
asyncio.CancelledError, # XXX
|
|
299
|
+
aiohttp.ClientError) as e:
|
|
300
|
+
msg = e if str(e) else type(e).__name__
|
|
301
|
+
x = 'CrtShApi: %s: %s %s' % (msg, sequence, sha256[1])
|
|
302
|
+
if tries == MAX_TRIES:
|
|
303
|
+
print('no retry after try %d, %s' % (tries, x),
|
|
304
|
+
file=sys.stderr)
|
|
305
|
+
break
|
|
306
|
+
print('retry after try %d, %s' % (tries, x),
|
|
307
|
+
file=sys.stderr)
|
|
308
|
+
# no sleep
|
|
309
|
+
|
|
310
|
+
except ArgsError as e:
|
|
311
|
+
x = 'CrtShApi: %s' % e
|
|
312
|
+
break
|
|
313
|
+
|
|
314
|
+
return None, x, None, None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
async def get_cert_files(panos):
|
|
318
|
+
try:
|
|
319
|
+
api = CrtShApi(timeout=CRT_SH_TIMEOUT)
|
|
320
|
+
except ArgsError as e:
|
|
321
|
+
print('CrtShApi: %s' % e, file=sys.stderr)
|
|
322
|
+
sys.exit(1)
|
|
323
|
+
else:
|
|
324
|
+
errors, certificates = await process_roots(api, panos)
|
|
325
|
+
finally:
|
|
326
|
+
await api.session.close()
|
|
327
|
+
|
|
328
|
+
return errors, certificates
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
async def process_roots(api, roots):
|
|
332
|
+
certificates = defaultdict(list)
|
|
333
|
+
tasks = []
|
|
334
|
+
start = time.time()
|
|
335
|
+
total = 0
|
|
336
|
+
errors = 0
|
|
337
|
+
|
|
338
|
+
for sequence in roots:
|
|
339
|
+
sequence_friendly = sequence
|
|
340
|
+
if sequence.endswith('.cer'):
|
|
341
|
+
sequence_friendly = sequence[:-4]
|
|
342
|
+
print('download CA %s intermediates %d' % (sequence_friendly,
|
|
343
|
+
len(roots[sequence][1:])),
|
|
344
|
+
flush=True)
|
|
345
|
+
|
|
346
|
+
for sha256 in roots[sequence]:
|
|
347
|
+
if not args.roots and sha256[0] == ROOT:
|
|
348
|
+
continue
|
|
349
|
+
total += 1
|
|
350
|
+
tasks.append(download(api, sequence_friendly, sha256))
|
|
351
|
+
|
|
352
|
+
if len(tasks) == MAX_TASKS:
|
|
353
|
+
errors += await run_tasks(tasks, certificates)
|
|
354
|
+
tasks = []
|
|
355
|
+
|
|
356
|
+
if len(tasks):
|
|
357
|
+
errors += await run_tasks(tasks, certificates)
|
|
358
|
+
|
|
359
|
+
if args.verbose:
|
|
360
|
+
end = time.time()
|
|
361
|
+
elapsed = end - start
|
|
362
|
+
mins = elapsed // 60
|
|
363
|
+
secs = elapsed % 60
|
|
364
|
+
avg = elapsed / total if total > 0 else 0
|
|
365
|
+
print('%d tasks, elapsed %.2f seconds, %dm%ds, avg %.2fs' %
|
|
366
|
+
(total, elapsed, mins, secs, avg),
|
|
367
|
+
file=sys.stderr)
|
|
368
|
+
|
|
369
|
+
return errors, certificates
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
async def run_tasks(tasks, certificates):
|
|
373
|
+
INTERVAL_WAIT = 3.3 # random throttle sweet spot to avoid TimeoutError
|
|
374
|
+
errors = 0
|
|
375
|
+
|
|
376
|
+
if args.verbose:
|
|
377
|
+
print('running %d tasks' % len(tasks),
|
|
378
|
+
file=sys.stderr)
|
|
379
|
+
start = time.time()
|
|
380
|
+
|
|
381
|
+
for coro in asyncio.as_completed(tasks):
|
|
382
|
+
filename, content, sequence, cert_type = await coro
|
|
383
|
+
if filename is None:
|
|
384
|
+
# error
|
|
385
|
+
print(content)
|
|
386
|
+
errors += 1
|
|
387
|
+
else:
|
|
388
|
+
certificates[sequence].append((cert_type, filename, content))
|
|
389
|
+
|
|
390
|
+
if args.verbose:
|
|
391
|
+
end = time.time()
|
|
392
|
+
elapsed = end - start
|
|
393
|
+
rate = len(tasks) / elapsed
|
|
394
|
+
rate2 = elapsed / len(tasks)
|
|
395
|
+
print('%d tasks complete, elapsed %.2f seconds, '
|
|
396
|
+
'%.2f tasks/sec, %.2f secs/task' %
|
|
397
|
+
(len(tasks), elapsed, rate, rate2), file=sys.stderr)
|
|
398
|
+
print('sleeping %.2fs' % INTERVAL_WAIT, file=sys.stderr)
|
|
399
|
+
await asyncio.sleep(INTERVAL_WAIT)
|
|
400
|
+
|
|
401
|
+
return errors
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def create_archive(certificates=None, test=False):
|
|
405
|
+
if not certificates:
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
with tarfile.open(name=args.certs, mode='w:gz') as tar:
|
|
410
|
+
if test:
|
|
411
|
+
os.unlink(args.certs)
|
|
412
|
+
return
|
|
413
|
+
for sequence in sorted(certificates.keys()):
|
|
414
|
+
for cert in certificates[sequence]:
|
|
415
|
+
name = os.path.join(sequence, cert[0], cert[1])
|
|
416
|
+
member = tarfile.TarInfo(name=name)
|
|
417
|
+
member.size = len(cert[2])
|
|
418
|
+
member.mtime = time.time()
|
|
419
|
+
f = io.BytesIO(cert[2].encode())
|
|
420
|
+
tar.addfile(member, fileobj=f)
|
|
421
|
+
except (tarfile.TarError, OSError) as e:
|
|
422
|
+
print("tarfile %s: %s" % (args.certs, e), file=sys.stderr)
|
|
423
|
+
sys.exit(1)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def parse_args():
|
|
427
|
+
parser = argparse.ArgumentParser(
|
|
428
|
+
usage='%(prog)s [options]',
|
|
429
|
+
description='generate PAN-OS intermediate CAs to preload')
|
|
430
|
+
# curl -OJ \
|
|
431
|
+
# https://ccadb.my.salesforce-sites.com/ccadb/AllCertificateRecordsCSVFormatv2
|
|
432
|
+
parser.add_argument('-c', '--ccadb',
|
|
433
|
+
required=True,
|
|
434
|
+
metavar='PATH',
|
|
435
|
+
help='CCADB AllCertificateRecordsCSVFormatv2 CSV path')
|
|
436
|
+
# sh cert-fingerprints.sh cert-dir
|
|
437
|
+
parser.add_argument('-f', '--fingerprints',
|
|
438
|
+
metavar='PATH',
|
|
439
|
+
help='PAN-OS trusted CAs fingerprints CSV path')
|
|
440
|
+
x = 'certificates.tgz'
|
|
441
|
+
parser.add_argument('--certs',
|
|
442
|
+
default=x,
|
|
443
|
+
metavar='PATH',
|
|
444
|
+
help='certificate archive path'
|
|
445
|
+
' (default: %s)' % x)
|
|
446
|
+
parser.add_argument('--roots',
|
|
447
|
+
action='store_true',
|
|
448
|
+
help='also download root CAs (experimental)')
|
|
449
|
+
parser.add_argument('--verbose',
|
|
450
|
+
action='store_true',
|
|
451
|
+
help='enable verbosity')
|
|
452
|
+
x = '%s %s' % (title, __version__)
|
|
453
|
+
parser.add_argument('--version',
|
|
454
|
+
action='version',
|
|
455
|
+
help='display version',
|
|
456
|
+
version=x)
|
|
457
|
+
args = parser.parse_args()
|
|
458
|
+
|
|
459
|
+
if args.verbose:
|
|
460
|
+
print(args, file=sys.stderr)
|
|
461
|
+
|
|
462
|
+
return args
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
if __name__ == '__main__':
|
|
466
|
+
main()
|