Free tool
module-sign-scan — which mode refuses the module you signed
Whether a module is signed is the easy half. The half that decides whether it installs is which of four verification modes the controller is in, and what each of those modes refuses. module-sign-scan reads nre.jar, baja.jar and platform-rt.jar with javap and prints the modes, the signature states each one accepts, and the log lines a station writes when it turns a jar away.
- Module signing
- Reads the shipped jars
- No station needed
- Read-only
- Python 3
- MIT
Install and run
One file, standard library only, and no station involved: it reads jars on disk. It
needs a Niagara installation and a javap on the path, which the JDK
that ships with Niagara already provides. Point it at the installation or set
NIAGARA_HOME.
# download it next to wherever you are working
curl -O https://plantroomlabs.com/tools/module-sign-scan.py
# read the verification code out of an installation
python3 module-sign-scan.py /opt/Niagara/Niagara-4.15.5.22
What it printed here
This is the whole of one run against the installation on the machine that built this
page, pasted by the script that publishes it rather than retyped. Three things in it
are worth the read on their own: medium is the shipped default from
4.10 and it accepts a self-signed signing certificate with a warning, while
high refuses the same jar outright; an un-timestamped signature is
accepted in every mode, so a jar that installs today stops validating the day its
certificate expires; and niagara.moduleVerificationMode is the first of
eight properties that cannot be set at the station command line, so the mode is
configuration you have to be told rather than a switch you can read remotely.
$ python3 module-sign-scan.py $NIAGARA_HOME
What a station does with a module signature
======================================================================
read from /opt/Niagara/Niagara-4.15.5.22
javap: 1.8.0_504
The verification mode is an enum of four, read out of nre.jar:
noPreference ordinal 0
low ordinal 1
medium ordinal 2 <- DEFAULT
high ordinal 3
Below version 4.8 (MIN_VERIFICATION_VERSION) make() returns low
regardless of what is asked for. At or above it, a mode of
noPreference (or null) defers to getDefaultVerificationMode.
When no mode is asked for, getDefaultVerificationMode picks by
version: below 4.9 -> low; below 4.10 -> medium; otherwise -> medium (the
shipped default). So a 4.14 or 4.15 controller lands on medium.
Where the mode actually comes from (Nre, in baja.jar):
The field moduleVerificationMode is seeded with low in Nre's static
initialiser. It is replaced during Nre.init(), which reads the
niagara.moduleVerificationMode property, loads the baja module and
passes make() baja's OWN vendorVersion - so the version that chooses
the default is the framework's, not the module's. An unparseable
property value logs a warning and leaves the requested mode null,
which make() then turns into the version default.
niagara.moduleVerificationMode is the first of 8 entries on
DEFAULT_COMMAND_LINE_DENYLIST, so it cannot be overridden at the
station command line - it has to be set in configuration:
niagara.moduleVerificationMode
program.requireSigning
niagara.export.preventCSVInjection
niagara.webbrowser.disable
niagara.webbrowser.urlWhitelist
niagara.baja.formatBlacklist
niagara.baja.formatBlacklistExclusions
jdk.tls.rejectClientInitiatedRenegotiation
Which signature states each mode accepts (ModuleSignatureStatusEnum
.isAcceptable, switch map resolved from the synthetic $1 class):
noPreference refuses: nothing
accepts: OK, NOT_TIMESTAMPED, UNKNOWN, SIGNER_SELF_SIGNED, TIMESTAMP_SELF_SIGNED, CERT_PATH_VALIDATION_FAILURE, CERT_PATH_VALIDATION_WARNING, UNSIGNED, INVALID_SIGNATURE
low refuses: INVALID_SIGNATURE
accepts: OK, NOT_TIMESTAMPED, UNKNOWN, SIGNER_SELF_SIGNED, TIMESTAMP_SELF_SIGNED, CERT_PATH_VALIDATION_FAILURE, CERT_PATH_VALIDATION_WARNING, UNSIGNED
medium refuses: UNSIGNED, CERT_PATH_VALIDATION_FAILURE, UNKNOWN, INVALID_SIGNATURE
accepts: OK, NOT_TIMESTAMPED, SIGNER_SELF_SIGNED, TIMESTAMP_SELF_SIGNED, CERT_PATH_VALIDATION_WARNING
high refuses: SIGNER_SELF_SIGNED, TIMESTAMP_SELF_SIGNED, UNSIGNED, CERT_PATH_VALIDATION_FAILURE, UNKNOWN, INVALID_SIGNATURE
accepts: OK, NOT_TIMESTAMPED, CERT_PATH_VALIDATION_WARNING
Note which line each mode draws. medium - the shipped default on
4.10 and up - accepts a self-signed signing certificate and an
un-timestamped signature; high refuses the self-signed one. No
mode below high ever refuses NOT_TIMESTAMPED on its own.
What ModuleClassLoader.verifyJarEntrySignature does, per entry:
Directories and META-INF entries return true without a check. The
cert chain is validated for an entry under com/tridium/ or
javax/baja/, or when the module sets checkTpk - and at mode low the
check is relaxed unless the module requested permissions.
No code signers on an entry that must be validated throws
"Error validating cert path: No code signers found."
otherwise it only logs, and returns true:
"No code signers for entry %s in module %s. Signed modules will be required in a future release."
A self-signed signing certificate is REFUSED only at mode high:
"Self signed signing certificate not permitted by current
module verification mode."
and likewise a self-signed timestamp certificate. At medium and
low each is a warning only, worded "... will not be allowed by
default in a future release."
An un-timestamped signature is accepted at every mode, with:
"Signature for entry %s in module %s is not timestamped. This signature will fail to validate when the signing certificate expires."
which is the one that bites an OEM later rather than now.
Turning validation off entirely:
The property niagara.classLoader.skipModuleValidation is read, but
it only takes effect behind a licensed feature - checkFeature('tridium',
'developer') with the 'skipModuleValidation' attribute. Without it the request
throws FeatureNotLicensedException, is caught, and logs a warning:
"A request to disable module validation was made, but the system
is not licensed for it: ..."
When it does take effect, a three-line banner is logged at WARNING:
"**** Module validation has been DISABLED ****" between two rows
of asterisks. There is no quiet way to switch it off.
What this means if you ship a signed module
----------------------------------------------------------------------
1. On a 4.10+ controller the default is medium, which accepts a
self-signed certificate with only a warning. The same jar on a
site that has set the mode to high is refused outright. The mode
is not on the command line, so you find that out from the station
log, not from a platform switch you can read remotely.
2. NOT_TIMESTAMPED is acceptable in every mode - so a signed but
un-timestamped jar installs and runs today, and stops validating
the day the signing certificate expires. A timestamp is what
decouples 'signed then' from 'trusted now'.
3. Below 4.8 the mode is forced to low and almost everything is
accepted; an older controller is not evidence your signing is
right, only that nothing checked it hard.
4. Nothing here is a defect. It is the shipped default and the shape
of the code that reads your signature - the parts an integrator
sees as a log line, and an OEM can get ahead of.
What it reads, and what that does not cover
It reads three shipped jars with javap and nothing else. No station is
contacted, no jar of yours is verified, and nothing is installed anywhere —
which is also the limit of what the output is worth. It is a reading of the code
that checks a signature, so it tells you what that code is built to refuse rather
than what a particular controller did with a particular jar.
The numbers above came from one installation, 4.15.5.22, and the
output names it. The controller this work usually targets runs
4.14.0.162. A different Niagara version is a different answer, so run
it against yours rather than trusting this page. Nothing in the output is a defect:
it is the shipped default and the shape of the code that reads your signature.
Niagara module signing: what a station checks is the reasoning behind the output, and building and loading a custom module is where the signed jar has to land.
The repository
The same file, MIT licensed, at github.com/UsamaIqbal0304/module-sign-scan. Its README carries the finding and a run of the output, so the repository is checkable without downloading anything. Issues and pull requests are read.
The file you are downloading
Published here so the download is checkable rather than trusted. Both figures
are read off the file served at
/tools/module-sign-scan.py when this page is built, so they cannot
disagree with it.
| Property | Value |
|---|---|
| File | module-sign-scan.py |
| Size | 21,186 bytes |
| SHA-256 | 661323250a71ea527fda6cdbd2facb4ff394f64f961ba0dd1bd0d2e3988e8157 |
| Licence | MIT — LICENSE.txt |
| Source | github.com/UsamaIqbal0304/module-sign-scan |
To check it, on Linux sha256sum module-sign-scan.py, on macOS
shasum -a 256 module-sign-scan.py, on Windows
certutil -hashfile module-sign-scan.py SHA256. A different digest means a
different file — not necessarily a hostile one, but not this one.
The repository holds the same file, byte for byte, together with everything needed to re-run the checks this page's claims rest on — so they can be run rather than read about. Issues and pull requests there are read.
bacnet-sweep
Broadcasts a BACnet/IP Who-Is, tables the devices that answer, and dumps a named device's object list — object name, present value and units — to a table or to CSV. It can encode two BACnet services and no others: Who-Is and ReadProperty.
- BACnet/IP
- Who-Is
- CSV out
mqtt-tap
Subscribes to a broker and prints what is actually on it: the topic tree with a count, a rate, a payload-type guess and the last value per topic, plus the retained topics that stopped updating. It sends five packet types and none of them is PUBLISH.
- MQTT
- Topic tree
- Retained
decoder-check
Runs a LoRaWAN device vendor's payload decoder against your frames in a sealed vm context and reports what a station would actually get back: crashes on a short frame, types that change between uplinks, units glued into values, keys a station has to escape. It reads frames and nothing else - no network, no broker, no network server.
- LoRaWAN
- Decoder
- Sandboxed
modbus-address-scan
Reads modbusCore-rt.jar out of a Niagara installation with javap and prints the register a point of each address format actually asks for — including the four band boundaries that all resolve to the same one.
- Modbus
- Shipped jars
- No station
ede-check
Reads the EDE import rules out of bacnetEDE-wb.jar with javap, then reports line by line what the shipped parser would reject in your point file and what it would silently default.
- EDE
- Point lists
- Line by line
bacnet-priority-scan
Reads bacnet-rt.jar with javap and prints the object types Niagara writes through the priority array without asking, the ones it probes with a single ReadProperty, and what a failed probe does to the point for the life of that configuration.
- BACnet
- Shipped jars
- No station
alarm-route-scan
Reads alarm-rt.jar and baja.jar with javap and prints what happens to an alarm between the source and the recipient: one queue, one worker thread, the coalesce key that decides which duplicate is dropped, and why the invocation that lost that collision still reports success.
- Alarms
- Shipped jars
- No station
alarm-recipient-scan
Reads the recipient side of alarm-rt.jar with javap and prints why returning false from sendAlarm drops the alarm silently, what throwing does instead, how long the retry loop runs, and which four properties are the only evidence a site can send you.
- Alarms
- Retry
- No station
schedule-scan
Reads schedule-rt.jar with javap and prints the 90-day scanLimit horizon that turns a far-off change into no change at all, why nextCov steps over a boundary whose value matches, and the one serial uncapped queue every control schedule shares.
- Schedules
- Shipped jars
- No station
workbook-scan
Opens an .xlsx as the zip of XML it is and reports what is in the bytes: formulas saved holding an error, links into files that may be gone, saved queries to one person's mapped drive, approximate VLOOKUPs, hidden sheets, and rules nothing protects. It reads the old binary .xls too.
- Excel
- No install
- JSON out
poll-scheduler-scan
Reads the poll scheduler out of driver-rt.jar and prints the arithmetic: three rate defaults, one point polled per pass, and the bucket size at which the thread stops sleeping and the real cycle time stretches.
- Niagara
- javap
- Read-only
tuning-stale-scan
Reads the tuning policy and the stale branch out of the shipped jars: the default staleTime of zero, the clock it measures, and why an unsolicited device can sit dead with an ok status for as long as the station runs.
- Niagara
- javap
- Read-only
Next step
Send the log line that refused the jar.
The station says which state it objected to, and that state maps to one of four modes and one of two certificates. Which half is wrong is usually clear from the line itself, and that read costs nothing either way.