diff --git a/README.md b/README.md index 67af2d11..fe3cf1cd 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,10 @@ Jsign - Java implementation of Microsoft Authenticode Jsign is a Java implementation of Microsoft Authenticode that lets you sign and timestamp executable files for Windows, Microsoft Installers (MSI), Cabinet -files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX) and scripts. -Jsign is platform independent and provides an alternative to native tools like -signcode/signtool on Windows or the Mono development tools on Unix systems. +files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX), Microsoft +Dynamics 365 extension packages and scripts. Jsign is platform independent and +provides an alternative to native tools like signcode/signtool on Windows or +the Mono development tools on Unix systems. Jsign comes as an easy-to-use task/plugin for the main build systems (Maven, Gradle, Ant). It's especially suitable for signing executable wrappers and @@ -20,7 +21,7 @@ Jsign can also be used programmatically or standalone as a command line tool. Jsign is free to use and licensed under the [Apache License version 2.0](https://www.apache.org/licenses/LICENSE-2.0). ## Features -* Platform independent signing of Windows executables, DLLs, Microsoft Installers (MSI), Cabinet files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX) and scripts (PowerShell, VBScript, JScript, WSF) +* Platform independent signing of Windows executables, DLLs, Microsoft Installers (MSI), Cabinet files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX), Microsoft Dynamics 365 extension packages and scripts (PowerShell, VBScript, JScript, WSF) * Timestamping with retries and fallback on alternative servers (RFC 3161 and Authenticode protocols supported) * Supports multiple signatures per file, for all file types * Extracts and embeds detached signatures to support [reproducible builds](https://reproducible-builds.org/docs/embedded-signatures/) @@ -41,7 +42,8 @@ See https://ebourg.github.io/jsign for more information. ## Changes #### Version 5.1 (in development) -* APPX/MSIX package signing has been implemented (thanks to Maciej Panek for the help) +* Signing of APPX/MSIX packages has been implemented (thanks to Maciej Panek for the help) +* Signing of Microsoft Dynamics 365 extension packages has been implemented * The certificate chain in the file specified by the `certfile` parameter can now be in any order * VBScript, JScript and PowerShell XML files without byte order marks are now parsed as Windows-1252 instead of ISO-8859-1 * The format detection based on the file extension is now case insensitive (contributed by Mathieu Delrocq) diff --git a/docs/index.html b/docs/index.html index 291c8cdb..f88c26b1 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@
- + @@ -37,9 +37,10 @@Jsign comes as an easy to use task/plugin for the main build systems (Maven, Gradle, Ant). It's especially suitable for signing executable wrappers and installers @@ -56,7 +57,7 @@
usage: jsign [OPTIONS] [FILE]... Sign and timestamp Windows executable files, Microsoft Installers (MSI), Cabinet - files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX) or scripts - (PowerShell, VBScript, JScript, WSF). + files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX), Microsoft Dynamics + 365 extension packages and scripts (PowerShell, VBScript, JScript, WSF). -s,--keystore <FILE> The keystore file, the SunPKCS11 configuration file or the cloud keystore name diff --git a/jsign-cli/src/main/java/net/jsign/JsignCLI.java b/jsign-cli/src/main/java/net/jsign/JsignCLI.java index 6a6be854..d3ca0e23 100644 --- a/jsign-cli/src/main/java/net/jsign/JsignCLI.java +++ b/jsign-cli/src/main/java/net/jsign/JsignCLI.java @@ -139,7 +139,7 @@ private void setOption(String key, SignerHelper helper, CommandLine cmd) { } private void printHelp() { - String header = "Sign and timestamp Windows executable files, Microsoft Installers (MSI), Cabinet files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX) or scripts (PowerShell, VBScript, JScript, WSF).\n\n"; + String header = "Sign and timestamp Windows executable files, Microsoft Installers (MSI), Cabinet files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX), Microsoft Dynamics 365 extension packages and scripts (PowerShell, VBScript, JScript, WSF).\n\n"; String footer ="\n" + "Examples:\n\n" + " Signing with a PKCS#12 keystore and timestamping:\n\n" + diff --git a/jsign-core/src/main/java/net/jsign/Signable.java b/jsign-core/src/main/java/net/jsign/Signable.java index 6007f00c..7d17b50f 100644 --- a/jsign-core/src/main/java/net/jsign/Signable.java +++ b/jsign-core/src/main/java/net/jsign/Signable.java @@ -32,6 +32,7 @@ import net.jsign.mscab.MSCabinetFile; import net.jsign.msi.MSIFile; import net.jsign.appx.APPXFile; +import net.jsign.navx.NAVXFile; import net.jsign.pe.PEFile; import net.jsign.script.JScript; import net.jsign.script.PowerShellScript; @@ -149,6 +150,9 @@ static Signable of(File file, Charset encoding) throws IOException { } else if (CatalogFile.isCatalogFile(file)) { return new CatalogFile(file); + } else if (NAVXFile.isNAVXFile(file)) { + return new NAVXFile(file); + } else if (file.getName().toLowerCase().endsWith(".ps1") || file.getName().toLowerCase().endsWith(".psd1") || file.getName().toLowerCase().endsWith(".psm1")) { diff --git a/jsign-core/src/main/java/net/jsign/navx/NAVXFile.java b/jsign-core/src/main/java/net/jsign/navx/NAVXFile.java new file mode 100644 index 00000000..e69d398c --- /dev/null +++ b/jsign-core/src/main/java/net/jsign/navx/NAVXFile.java @@ -0,0 +1,190 @@ +/** + * Copyright 2023 Emmanuel Bourg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.jsign.navx; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.List; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.DERNull; +import org.bouncycastle.asn1.cms.Attribute; +import org.bouncycastle.asn1.cms.AttributeTable; +import org.bouncycastle.asn1.cms.ContentInfo; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.DigestInfo; +import org.bouncycastle.cms.CMSProcessable; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.SignerInformation; + +import net.jsign.DigestAlgorithm; +import net.jsign.Signable; +import net.jsign.asn1.authenticode.AuthenticodeObjectIdentifiers; +import net.jsign.asn1.authenticode.SpcAttributeTypeAndOptionalValue; +import net.jsign.asn1.authenticode.SpcIndirectDataContent; +import net.jsign.asn1.authenticode.SpcSipInfo; +import net.jsign.asn1.authenticode.SpcUuid; + +import static net.jsign.ChannelUtils.*; + +/** + * Microsoft Dynamics 365 extension package (NAVX) + * + * @author Emmanuel Bourg + * @since 5.1 + */ +public class NAVXFile implements Signable { + + /** The channel used for in-memory signing */ + private final SeekableByteChannel channel; + + /** The underlying file */ + private File file; + + /** The file header */ + private final NAVXHeader header = new NAVXHeader(); + + /** + * Tells if the specified file is a NAVX file. + * + * @param file the file to check + * @returntrue
if the file is a NAVX file,false
otherwise + * @throws IOException if an I/O error occurs + */ + public static boolean isNAVXFile(File file) throws IOException { + if (file.length() < NAVXHeader.SIZE) { + return false; + } + + // read the signature + try (SeekableByteChannel channel = Files.newByteChannel(file.toPath(), StandardOpenOption.READ)) { + ByteBuffer buffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN); + channel.read(buffer); + buffer.flip(); + return buffer.getInt() == NAVXHeader.SIGNATURE; + } + } + + /** + * Create a NAVXFile from the specified file. + * + * @param file the file to open + * @throws IOException if an I/O error occurs + */ + public NAVXFile(File file) throws IOException { + this(Files.newByteChannel(file.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE)); + } + + /** + * Create a NAVXFile from the specified channel. + * + * @param channel the channel to read the file from + * @throws IOException if an I/O error occurs + */ + public NAVXFile(SeekableByteChannel channel) throws IOException { + this.channel = channel; + + channel.position(0); + header.read(channel); + + if (header.contentSize + NAVXHeader.SIZE > channel.size()) { + throw new IOException("NAVX file is corrupt: invalid size in the header"); + } + } + + @Override + public byte[] computeDigest(DigestAlgorithm digestAlgorithm) throws IOException { + MessageDigest digest = digestAlgorithm.getMessageDigest(); + updateDigest(channel, digest, 0, getSignatureOffset()); + return digest.digest(); + } + + @Override + public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) throws IOException { + AlgorithmIdentifier algorithmIdentifier = new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE); + DigestInfo digestInfo = new DigestInfo(algorithmIdentifier, computeDigest(digestAlgorithm)); + + SpcUuid uuid = new SpcUuid("12341234-F804-0000-781D-123412341234"); + SpcAttributeTypeAndOptionalValue data = new SpcAttributeTypeAndOptionalValue(AuthenticodeObjectIdentifiers.SPC_SIPINFO_OBJID, new SpcSipInfo(1, uuid)); + + return new SpcIndirectDataContent(data, digestInfo); + } + + private int getSignatureOffset() { + return NAVXHeader.SIZE + header.contentSize; + } + + @Override + public ListgetSignatures() throws IOException { + List signatures = new ArrayList<>(); + + try { + channel.position(getSignatureOffset()); + NAVXSignatureBlock signatureBlock = new NAVXSignatureBlock(); + signatureBlock.read(channel); + CMSSignedData signedData = signatureBlock.signedData; + if (signedData != null) { + signatures.add(signedData); + + // look for nested signatures + SignerInformation signerInformation = signedData.getSignerInfos().getSigners().iterator().next(); + AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes(); + if (unsignedAttributes != null) { + Attribute nestedSignatures = unsignedAttributes.get(AuthenticodeObjectIdentifiers.SPC_NESTED_SIGNATURE_OBJID); + if (nestedSignatures != null) { + for (ASN1Encodable nestedSignature : nestedSignatures.getAttrValues()) { + signatures.add(new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(nestedSignature))); + } + } + } + } + } catch (UnsupportedOperationException e) { + // unsupported type, just skip + } catch (Exception e) { + e.printStackTrace(); + } + + return signatures; + } + + @Override + public void setSignature(CMSSignedData signature) throws IOException { + NAVXSignatureBlock signatureBlock = new NAVXSignatureBlock(); + signatureBlock.signedData = signature; + + channel.position(getSignatureOffset()); + signatureBlock.write(channel); + channel.truncate(channel.position()); + } + + @Override + public void save() throws IOException { + } + + @Override + public void close() throws IOException { + channel.close(); + } +} diff --git a/jsign-core/src/main/java/net/jsign/navx/NAVXHeader.java b/jsign-core/src/main/java/net/jsign/navx/NAVXHeader.java new file mode 100644 index 00000000..cb5bfa87 --- /dev/null +++ b/jsign-core/src/main/java/net/jsign/navx/NAVXHeader.java @@ -0,0 +1,61 @@ +/** + * Copyright 2023 Emmanuel Bourg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.jsign.navx; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; + +import static java.nio.ByteOrder.*; + +/** + * NAVX file header + * + * + * signature 4 bytes (NAVX) + * unknown 24 bytes + * content size 4 bytes + * unknown 4 bytes + * signature 4 bytes (NAVX) + *+ * + * @since 5.1 + */ +class NAVXHeader { + + public static final int SIGNATURE = 0x5856414E; // NAVX; + public static final int SIZE = 40; + + public int contentSize; + + public void read(ReadableByteChannel channel) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(SIZE).order(LITTLE_ENDIAN); + channel.read(buffer); + buffer.flip(); + + int signature = buffer.getInt(); + if (signature != SIGNATURE) { + throw new IOException("Invalid NAVX header signature"); + } + contentSize = buffer.getInt(28); + + signature = buffer.getInt(36); + if (signature != SIGNATURE) { + throw new IOException("Invalid NAVX header signature"); + } + } +} diff --git a/jsign-core/src/main/java/net/jsign/navx/NAVXSignatureBlock.java b/jsign-core/src/main/java/net/jsign/navx/NAVXSignatureBlock.java new file mode 100644 index 00000000..712cc008 --- /dev/null +++ b/jsign-core/src/main/java/net/jsign/navx/NAVXSignatureBlock.java @@ -0,0 +1,89 @@ +/** + * Copyright 2023 Emmanuel Bourg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.jsign.navx; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; + +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.cms.ContentInfo; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessable; +import org.bouncycastle.cms.CMSSignedData; + +import static java.nio.ByteOrder.*; + +/** + * NAVX signature block + * + *+ * signature 4 bytes (NXSB) + * CMS Signed Data (variable size) + * offset of the signature block 4 bytes + * signature 4 bytes (NXSB) + *+ * + * @since 5.1 + */ +class NAVXSignatureBlock { + + public static final int SIGNATURE = 0x4253584E; // NXSB; + + public CMSSignedData signedData; + + public void read(SeekableByteChannel channel) throws IOException { + int size = (int) (channel.size() - channel.position()); + + if (size == 0) { + return; + } + + ByteBuffer buffer = ByteBuffer.allocate(size).order(LITTLE_ENDIAN); + channel.read(buffer); + buffer.flip(); + + int signature = buffer.getInt(0); + if (signature != SIGNATURE) { + throw new IOException("Invalid NAVX signature block"); + } + + byte[] signatureBytes = new byte[size - 8]; + buffer.position(4); + buffer.get(signatureBytes); + try { + signedData = new CMSSignedData((CMSProcessable) null, ContentInfo.getInstance(new ASN1InputStream(signatureBytes).readObject())); + } catch (CMSException e) { + throw new IOException("Invalid CMS signature", e); + } + } + + public void write(SeekableByteChannel channel) throws IOException { + long offset = channel.position(); + byte[] content = signedData != null ? signedData.toASN1Structure().getEncoded("DER") : new byte[0]; + if (content.length > 0) { + ByteBuffer buffer = ByteBuffer.allocate(content.length + 12).order(LITTLE_ENDIAN); + buffer.putInt(SIGNATURE); + buffer.put(content); + buffer.putInt((int) offset); + buffer.putInt(SIGNATURE); + buffer.flip(); + + channel.write(buffer); + } + } +} diff --git a/jsign-core/src/test/resources/minimal.navx b/jsign-core/src/test/resources/minimal.navx new file mode 100644 index 00000000..e55278e4 Binary files /dev/null and b/jsign-core/src/test/resources/minimal.navx differ diff --git a/jsign-gradle-plugin/build.gradle b/jsign-gradle-plugin/build.gradle index 583b4c90..fd284069 100644 --- a/jsign-gradle-plugin/build.gradle +++ b/jsign-gradle-plugin/build.gradle @@ -25,7 +25,7 @@ gradlePlugin { jsignPlugin { id = 'net.jsign' displayName = 'Jsign Gradle plugin' - description = 'Sign and timestamp Windows executable files, Microsoft Installers (MSI), Cabinet files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX) or scripts (PowerShell, VBScript, JScript, WSF)' + description = 'Sign and timestamp Windows executable files, Microsoft Installers (MSI), Cabinet files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX), Microsoft Dynamics 365 extension packages and scripts (PowerShell, VBScript, JScript, WSF)' tags.set(['signing', 'code-signing', 'authenticode', 'signtool']) implementationClass = 'net.jsign.JsignGradlePlugin' } diff --git a/jsign/src/choco/jsign.nuspec b/jsign/src/choco/jsign.nuspec index 565fb1db..04c3942a 100644 --- a/jsign/src/choco/jsign.nuspec +++ b/jsign/src/choco/jsign.nuspec @@ -12,13 +12,13 @@https://github.com/ebourg/jsign https://ebourg.github.io/jsign/ https://github.com/ebourg/jsign/issues -signing code-signing authenticode signtool PE EXE SYS DLL CAB MSI VBS WSF APPX MSIX +signing code-signing authenticode signtool PE EXE SYS DLL CAB MSI VBS WSF APPX MSIX NAVX Command line tool for signing Windows executable files, installers and scripts Jsign is a Java implementation of Microsoft Authenticode that lets you sign and timestamp executable files - for Windows, Microsoft Installers (MSI), Cabinet files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX) - and scripts. Jsign is platform independent and provides an alternative to native tools like signcode/signtool - on Windows or the Mono development tools on Unix systems. + for Windows, Microsoft Installers (MSI), Cabinet files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX), + Microsoft Dynamics 365 extension packages and scripts. Jsign is platform independent and provides an alternative + to native tools like signcode/signtool on Windows or the Mono development tools on Unix systems. https://github.com/ebourg/jsign/blob/master/README.md diff --git a/jsign/src/deb/control/control b/jsign/src/deb/control/control index 27dc63c3..bd07f753 100644 --- a/jsign/src/deb/control/control +++ b/jsign/src/deb/control/control @@ -11,6 +11,7 @@ Homepage: https://ebourg.github.com/jsign Description: Code signing for Windows executables, Microsoft Installers and scripts Jsign is a Java implementation of Microsoft Authenticode that lets you sign and timestamp executable files for Windows, Microsoft Installers (MSI), - Cabinet files, Windows packages (APPX/MSIX) and scripts (PowerShell, VBScript, - JScript, WSF). Jsign is platform independent and provides an alternative to native - tools like signcode/signtool on Windows or the Mono development tools on Unix systems. + Cabinet files, Windows packages (APPX/MSIX), Microsoft Dynamics 365 extension + packages and scripts (PowerShell, VBScript, JScript, WSF). Jsign is platform + independent and provides an alternative to native tools like signcode/signtool + on Windows or the Mono development tools on Unix systems. diff --git a/jsign/src/deb/data/usr/share/bash-completion/completions/jsign b/jsign/src/deb/data/usr/share/bash-completion/completions/jsign index 10884029..a5b15157 100644 --- a/jsign/src/deb/data/usr/share/bash-completion/completions/jsign +++ b/jsign/src/deb/data/usr/share/bash-completion/completions/jsign @@ -14,7 +14,7 @@ _jsign() return 0 fi - local pattern="!*.@(exe|dll|cpl|ocx|sys|scr|drv|ps1|psm1|psd1|msi|msp|cab|cat|appx|appxbundle|msix|msixbundle)" + local pattern="!*.@(exe|dll|cpl|ocx|sys|scr|drv|ps1|psm1|psd1|msi|msp|cab|cat|appx|appxbundle|msix|msixbundle|navx|app)" case $prev in -s|--keystore|--keyfile|-c|--certfile) diff --git a/jsign/src/deb/data/usr/share/man/man1/jsign.1 b/jsign/src/deb/data/usr/share/man/man1/jsign.1 index 4e48f87d..28b42f65 100644 --- a/jsign/src/deb/data/usr/share/man/man1/jsign.1 +++ b/jsign/src/deb/data/usr/share/man/man1/jsign.1 @@ -2,7 +2,7 @@ .TH man 1 "25 Mar 2021" "@VERSION@" "jsign man page" .SH NAME -jsign \- sign and timestamp executable files for Windows, Microsoft Installers (MSI), Cabinet files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX) and scripts (PowerShell, VBScript, JScript, WSF) +jsign \- sign and timestamp executable files for Windows, Microsoft Installers (MSI), Cabinet files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX), Microsoft Dynamics 365 extension packages and scripts (PowerShell, VBScript, JScript, WSF) .SH SYNOPSIS .B jsign @@ -11,9 +11,10 @@ jsign \- sign and timestamp executable files for Windows, Microsoft Installers ( .SH DESCRIPTION Jsign is a Java implementation of Microsoft Authenticode that lets you sign and timestamp executable files for Windows, Microsoft Installers (MSI), Cabinet -files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX) and scripts. -Jsign is platform independent and provides an alternative to native tools like -signcode/signtool on Windows or the Mono development tools on Unix systems. +files (CAB), Catalog files (CAT), Windows packages (APPX/MSIX), Microsoft +Dynamics 365 extension packages and scripts. Jsign is platform independent and +provides an alternative to native tools like signcode/signtool on Windows or +the Mono development tools on Unix systems. Jsign can use private keys and certificates in various formats, either Java keystores, PKCS#12 keystores, PKCS#11 hardware tokens, SPC/PVK files or from cloud key management