Always pin the server, let the registry own pins, and survive a revoke mid-upgrade
Found in the Windows VM re-test. - A CA-flagged server certificate (common with a plain openssl self-signed cert) produced an installer package with no pin at all, so agent 1.20 refused the server. The package now always carries the SPKI pin of the certificate PCs receive; the CA flag only skips the trusted-root import. When an uploaded CA-flagged certificate differs from the server's own, both pins are included. The build response carries X-ShopDB-Build-Warning and the installer logs what older agents need. - Pins written into appsettings.json by the installer were merged with the registry, so removing a pin from HKLM never took effect. When the ServerPins value exists in the registry it is now authoritative, even when empty; appsettings pins apply only when it is absent. Set-ShopDBServerPins.ps1 -Clear deletes the value; new -None writes an empty value (no pins). - A script or package download sent with a credential revoked moments earlier got 401 CREDENTIAL_INVALID and failed the command. The agent now retries that download once with the shared key and records the fallback in the result.
This commit is contained in:
@@ -150,17 +150,42 @@ def _leaf_certificate_facts(pembytes):
|
||||
return _spki_sha256(block), isca
|
||||
|
||||
|
||||
def _current_server_pin():
|
||||
"""Pin of this server's own leaf certificate, or None when not found/CA."""
|
||||
def _current_server_facts():
|
||||
"""(spkisha256, isca) of this server's own leaf certificate, else (None, None)."""
|
||||
path = _server_cert_path()
|
||||
if not path:
|
||||
return None
|
||||
return None, None
|
||||
try:
|
||||
with open(path, 'rb') as handle:
|
||||
pin, isca = _leaf_certificate_facts(handle.read())
|
||||
return _leaf_certificate_facts(handle.read())
|
||||
except OSError:
|
||||
return None
|
||||
return None if isca else pin
|
||||
return None, None
|
||||
|
||||
|
||||
def _current_server_pin():
|
||||
"""Pin of this server's own leaf certificate, or None when not found.
|
||||
|
||||
The SPKI pin does not depend on basicConstraints, so a CA-flagged
|
||||
self-signed certificate is pinned like any other leaf.
|
||||
"""
|
||||
return _current_server_facts()[0]
|
||||
|
||||
|
||||
# Shown to the admin at build time (response header and UI) when the pinned
|
||||
# certificate carries basicConstraints CA:TRUE.
|
||||
CA_CERT_BUILD_WARNING = (
|
||||
'The server certificate has basicConstraints CA:TRUE, so install.ps1 will '
|
||||
'not import it into LocalMachine\\Root. The package still pins it: agents '
|
||||
'1.20 and newer trust the server by pin alone. Agents older than 1.20 and '
|
||||
'PowerShell 5.1 downloads without pin support will not trust this server. '
|
||||
'Regenerate the server certificate in Settings > Endpoints > Server '
|
||||
'certificate (the generator does not set CA), or trust a non-CA '
|
||||
'certificate on those PCs with Install-ShopDBServerCertificate.ps1.')
|
||||
CA_CERT_BOTH_PINS_WARNING = (
|
||||
'The uploaded CA-flagged certificate does not match this server\'s own '
|
||||
'certificate, so both pins were included in ServerPins: the uploaded '
|
||||
'certificate and the server\'s own certificate.')
|
||||
CA_WARNING_HEADER = 'X-ShopDB-Build-Warning'
|
||||
|
||||
|
||||
# The non-secret runtime config shipped in the package. baseurl and apikey stay
|
||||
@@ -485,6 +510,12 @@ if (-not [string]::IsNullOrWhiteSpace($CertFileName)) {{
|
||||
}}
|
||||
if ($certIsCa) {{
|
||||
Write-Warning "server certificate $CertFileName is a CA certificate; not importing it into LocalMachine\\Root."
|
||||
if ($pinList.Count -gt 0) {{
|
||||
Write-Warning "ServerPins were written: agents 1.20 and newer trust the server by pin alone."
|
||||
}}
|
||||
Write-Warning "Agents older than 1.20, and PowerShell 5.1 downloads without pin support, will NOT trust this server."
|
||||
Write-Warning "Fix: regenerate the server certificate with the ShopDB generator (Settings > Endpoints > Server certificate; it does not set CA),"
|
||||
Write-Warning "or have an admin trust a non-CA server certificate on this PC with Install-ShopDBServerCertificate.ps1."
|
||||
}}
|
||||
else {{
|
||||
Write-Host " trusting bundled server certificate $CertFileName ..."
|
||||
@@ -889,7 +920,9 @@ def package_siteagent():
|
||||
Returns an in-memory ZIP with the staged shopdbagent.exe (and tray when
|
||||
present), a non-secret appsettings.json, install.ps1, and README.txt. The
|
||||
collector key is embedded only in install.ps1 as its default parameter and
|
||||
is never stored or logged.
|
||||
is never stored or logged. When the pinned certificate is CA-flagged the
|
||||
response carries an X-ShopDB-Build-Warning header explaining that only
|
||||
pinning (agents 1.20 and newer) will trust the server.
|
||||
"""
|
||||
if request.form or request.files:
|
||||
source = request.form
|
||||
@@ -959,29 +992,42 @@ def package_siteagent():
|
||||
|
||||
certfilename = 'server.crt' if (cert_pem or certpath) else None
|
||||
|
||||
# Pin the bundled leaf when there is one; a CA bundle cannot be pinned (the
|
||||
# agent hashes the leaf) so fall back to this server's own certificate.
|
||||
# Pin the leaf the server presents: the bundled/uploaded leaf, else this
|
||||
# server's own certificate. The SPKI pin ignores basicConstraints, so a
|
||||
# CA-flagged self-signed cert is still pinned; the CA flag only gates the
|
||||
# Root import.
|
||||
trustcertinroot = True
|
||||
currentpin = None
|
||||
pinisca = False
|
||||
if certfilename:
|
||||
if cert_pem:
|
||||
bundledbytes = cert_pem.encode('ascii')
|
||||
else:
|
||||
with open(certpath, 'rb') as handle:
|
||||
bundledbytes = handle.read()
|
||||
bundledpin, bundledisca = _leaf_certificate_facts(bundledbytes)
|
||||
currentpin, bundledisca = _leaf_certificate_facts(bundledbytes)
|
||||
trustcertinroot = not bundledisca
|
||||
if not bundledisca:
|
||||
currentpin = bundledpin
|
||||
pinisca = bool(bundledisca)
|
||||
# An uploaded CA-flagged cert may be the server's own self-signed cert or an
|
||||
# internal CA root sent by mistake. Ship the server's own pin beside it so
|
||||
# the agent (which accepts any listed pin) works either way.
|
||||
serverownpin = None
|
||||
if cert_pem and pinisca:
|
||||
serverownpin = _current_server_pin()
|
||||
if serverownpin == currentpin:
|
||||
serverownpin = None
|
||||
if currentpin is None:
|
||||
currentpin = _current_server_pin()
|
||||
currentpin, pinisca = _current_server_facts()
|
||||
warnings = [CA_CERT_BUILD_WARNING] if (currentpin and pinisca) else []
|
||||
if serverownpin:
|
||||
warnings.append(CA_CERT_BOTH_PINS_WARNING)
|
||||
if nextserverpin and currentpin is None:
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
'nextserverpin needs a current server pin; bundle or upload the '
|
||||
'server leaf certificate',
|
||||
http_code=400)
|
||||
serverpins = [pin for pin in (currentpin, nextserverpin) if pin]
|
||||
serverpins = [pin for pin in (currentpin, serverownpin, nextserverpin) if pin]
|
||||
serverpins = list(dict.fromkeys(serverpins))
|
||||
appsettings['serverpins'] = serverpins
|
||||
|
||||
@@ -1020,8 +1066,11 @@ def package_siteagent():
|
||||
agentversion = agentstore.current_version('agent')
|
||||
download_name = ('shopdb-endpoint-agent-{0}.zip'.format(agentversion)
|
||||
if agentversion else 'shopdb-endpoint-agent.zip')
|
||||
return send_file(
|
||||
response = send_file(
|
||||
buffer,
|
||||
mimetype='application/zip',
|
||||
as_attachment=True,
|
||||
download_name=download_name)
|
||||
if warnings:
|
||||
response.headers[CA_WARNING_HEADER] = ' '.join(warnings)
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user