Skip to content

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.

PropertyValue
Filemodule-sign-scan.py
Size21,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.

Also free

The others

Same idea, a different protocol or a different file. Every free tool.

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.