Configuring the Jar Enforcer

Introduction

The JarEnforcer is a Maven Enforcer that, subject to configuration, performs the following tests on a distribution's externally-provided (dependency) jars.

  • Tests that they are signed by a key which is in an appropriate keyRing, failing if any signatures are missing or not resolvable.

    • This is used to ensure that you are not imposing a supply chain issue upon your customers.

  • Tests that the version is the one specified in the pom file (because maven’s resolution of dependencies is non intuitive). This fails if versions mismatch, or if artifacts are missing.

    • As a part of this test it can also do a reverse lookup and provide a trace back to which pom-specified artifact caused a particular jar to become a part of the distribution

  • Finally it can check the signature of every jar in your local maven repository. This can be used to check for supply chain attacks via maven plugins.

Configuration

This is done by adding the following stanza to the pom file for the project distribution.

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <dependencies> <dependency> <groupId>net.shibboleth.maven.enforcer.rules</groupId> <artifactId>maven-dist-enforcer</artifactId> <version>2.1.0</version> </dependency> </dependencies> <executions> <execution> <id>idp-enforce</id> <phase>verify</phase> <goals> <goal>enforce</goal> </goals> <configuration> <rules> <jarEnforcer implementation="net.shibboleth.mvn.enforcer.impl.JarEnforcer"> <enforcerData>${basedir}/src/main/enforcer</enforcerData> <parentPomDir>${basedir}/../idp-parent</parentPomDir> <jarDirs>${project.build.directory}/${idp.finalName}/bin/lib ${project.build.directory}/${idp.finalName}/webapp/WEB-INF/lib</jarDirs> <checkSignatures>true</checkSignatures> <checkDependencies>true</checkDependencies> <listJarSources>true</listJarSources> <artifactMap>${basedir}/src/main/enforcer/artifactMap.properties</artifactMap> </jarEnforcer> </rules> </configuration> </execution> </executions> </plugin>

The supported parameters are:

Element Name

Required?

Default

Function

Element Name

Required?

Default

Function

parentPomDir

Yes

 

This is the absolute path to the directory where the parent pom for the project is stored. This is parsed and used to

  • Establish the groupId for all artifacts (to allow signature lookup)

  • Establish which versions of the different artifacts are expected

dataGroupId 3.0
dataArtifactId 3.0
dataVersion 3.0

Yes

 

Maven coordinates of the project which contains the keys (and if required) signatures for jars. See below. Supercedes enforcerData

dataKeyRing 3.0

Yes

 

Absolute path to a keyring with keys which will be used to check the validity of the above specified jar file

tgzFiles 3.0

One must be present

 

Space separated list of tar.gz files to be scanned. Supercedes jarFiles

zipFiles 3.0

 

Space separated list of zip files to be scanned. Supercedes jarFiles

checkSignatures

 

true

Whether to run signature checking on the contents

sigCheckReportPath 3.0

 

${project.build.directory}\signatureReport.txt

Where to write the report of the signature checking

checkDependencies

 

true

Whether to run dependency analysis and report if any versions mismatch

listJarSources

 

false

Whether, as part of the dependency check to do a reverse look up of artifact to source (this is a slow operation)

depCheckReportPath 3.0

 

${project.build.directory}\dependencyReport.txt

Where to write the report of the signature checking

checkM2

 

false

Whether all the non-source, non-test jar files in the users maven repository (~/.m2/repository) will be checked

versionExtensions 3.2

 

-SNAPSHOT -GA -jre -empty-to-avoid-conflict-with-guava

See below

classifiers 3.2

 

<empty string>

See below

m2ReportPath 3.0

 

${project.build.directory}\m2SignatureReport.txt

Where to write the report of the m2 checking

The checkDependencies test will fail for several reasons. In certain circumstances, some strange configurations are required. Whether these start configurations are fatal or not can be controlled by four further Elements. Each element is a boolean (true/false) and defaults to true

  • compileRuntimeArtifactFatal. Setting this to false allows an artifact to be declared as runtime scope in some pom files and as compile scope in others. As an example, the IdP sets this element to false to cope with the logback artifacts (logback-classic and logback-core)

  • multipleJarVersionsFatal. If the same artifact (with the same or different versions) is found in multiple places in the supplied distribution the enforcer will fail unless this element is set to false. For example until V4.1.5 the IdP shipped with jcommander in the war\WEB_INF\lib folder and the bin\lib folder.

  • pomVersionMismatchFatal. If the a declaration of the same artifact carries different versions across the pom files then the enforcer will fail unless this element is set to false. For example in the later V4.1 distributions the version of spring (set in the spring-bim file) is overridden in the idp-parent pom.

  • distVersionMismatchFatal. If the artifact is the distribution has a difference version to that specified in the pom files then the enforcer will fail unless this element is set to false. I can think of no reason why this might be anything but a misconfiguration.

Decomposing file names, version extensions, classifiers and group lookups

Versions and Classifiers

When the enforcer is traversing the distribution (the tgz or zip file) as part of the signature check, it needs to infer the maven artifact coordinates from the file name. These coordinates allow the enforcer to download the signature (asc file) for the jar.

Thus, given a name such as bcutil-jdk15on-1.70.jar listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar or netty-transport-native-epoll-4.1.75.Final-linux-x86_64.jar it needs to be able to infer the groupId, artifactId, version and classifier. Since the enforcer has already read the project pom (and its parents) it can simply do this for artifacts which are explicitly mentioned. However the enforce does not follow dependencies and so it has to infer the coordinates which are in the distribution as a result of a dependency. To do this it treats a jar name as being made up of 4 parts

  1. A non-optional artifactId, followed by

  2. A non-optional version which starts with a '-' followed by

  3. An optional ‘extension garnish' which starts with a '-’ followed by

  4. An optional classifier which starts with a '-'

The maven coordindates are then defined as being

  • The artifactId is (1)

  • The version is (2) concatenated to (3) with the initial hyphen removed.

  • The classifier is (3) with the initial hyphen removed.

Thus in the example above a correct split would be

Jar file name

artifactId

version

classifier

Jar file name

artifactId

version

classifier

bcutil-jdk15on-1.70.jar

bcutil-jdk15on

1.70

 

listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar

listenablefuture

9999.0-empty-to-avoid-conflict-with-guava

 

netty-transport-native-epoll-4.1.75.Final-linux-x86_64.jar

netty-transport-native-epoll

4.1.75.Final

linux-x86_64

Given that all identifiers can contain hyphen tt can easily be seen that, it is impossible to deconstruct the name mechanically and the enforcer requires guidance. This is provided by the <versionExtensions/> and <classifiers/> configuration elements. The text content of these elements is a space separated list of names (with the leading hyphen). So, in order to get the decomposition above you would need the following

<versionExtensions>-empty-to-avoid-conflict-with-guava</versionExtensions> <classifiers>-linux-x86_64</classifiers>

The groupId

There is no mechanical method of determining the groupId from a file name. Instead the enforcer relies on a property file which is distributed in the enforcer data jar at location net/shibboleth/mvn/enforcer/data/artifactMap.properties. Appropriate additions for the above example would include

listenablefuture=com.google.guava netty-transport-native-epoll=io.netty

The bcutil-jdk5on artifactId does not need to be mentioned since it is explicitly defined in the pom files.

See also the troubleshooting section.

The Enforcer Data jar

The jar file referenced by the dataGroupId/dataArtifactId/dataVersion coordinated contains three sets of data used by the enforcer. These are all contained below the net/shibboleth/mvn/enforcer/data/ directory.

  1. A file called artifactMap.propertieswhich contains the mapping from artifactid to group. This is used where jar files are encountered which are not explicitly mentioned in the parent pom (see above).

  2. A directory called localsignatures which contains a series of files whose name is of the form artifactId-version.jar.asc (e.g. antlr-2.7.7.jar.asc) these being signatures on the respective jar files. This is a fallback for when jar files have been publish with no signature. Such signatures should only be added if the jar file has had independend verification

  3. A directory called keyRing which contains a series off keyrings and which is described below.

Configuring keyRings

Signature checking is done against a series of (GPG) keyrings in the net/shibboleth/mvn/enforcer/data/keyRings subdirectory of the jar specified by the dataXXX maven coordinates in V3 and the enforcerData path in V2.

Each keyring contains all the public keys which are trusted to validate the signatures of all artifacts with that groupId. The name of each key ring is the groupId with suffix .gpg. As an interim step in keyring deployment it is possible to supply an .asc file with the name derived soley from the groupId (with no suffix).

These keyrings require maintenance:

  • If an artifact is added to the distribution and it is not signed with a trusted key

  • If the version of an artifact changed and the signature on it is not a trusted key.

In both this cases the artifact will not have been added to our repository without a signature check and so it is an absolute assumption that the public key is available.

Adding a new artifact

  • If an artifact is added implicitly as a dependency then you may need to add the artifactId to groupId mapping to the artifactMap.

  • If the artifact is in a previously unknown groupId then you must create an empty GPG keyring with the appropriate name (and empty keyring is an empty file with a .gpg extension

Adding a new public key to a keyring

Only commit a change to the keyring after discussion with the committers group (either by Slack or by email) to ensure that an appropriate process has been followed for accrediting the signatory.

Assuming that the key is help in a file called pubkey.asc and the groupId is the.maven.group the following command will add the key.

Shibboleth Deployments

In the Shibboleth projects the enforcer is used in two distinct types of place:

  • Dependency & Signature checking when distributables are created.

  • M2 checking towards the end if the build of every project

Dependency & Signature checking

This is enabled for

  • The -dist part of every plugin

  • The idp-dist project

The enforcer is configured to run during the verify phase and the entirety of the configuration (with the exception of the properties defining versions) is contained within the relevant pom file.

M2 checking

This is usually performed in the last module of every project. For multi module projects this is usually the bom project.

The checking is performed in the verify phase and is enabled via specific profile. This allow (nearly) all the configuration to be contained in the parent pom. M2 checking is enabled for any particular module by creating a file called .check-m2 in the modules root directory. It can then be supressed by the -Dno-check-m2 parameter to command line maven.

Finally the output location for the m2 report can be changed for any project by defining a property maven-dist-enforcer-data.m2ReportPath in that project’s pom containing the fully qualified name that the output file should have.

Example output

Troubleshooting

Many of the error messages are (supposed to be) self explanatory and people having issues with them should enter a case in JIRA This will allow me to update the documentation or the error messages.

[TBD] Needs worked examples of

  • wrong versions

  • wrong classifiers

  • missing groupId

 

Â