mirror of
https://github.com/JetBrains/intellij-sdk-code-samples.git
synced 2025-07-30 18:27:49 +08:00
236 lines
9.4 KiB
Markdown
236 lines
9.4 KiB
Markdown
[//]: # (title: Plugin Signing)
|
||
|
||
<!-- Copyright 2000-2021 JetBrains s.r.o. and other contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
|
||
|
||
Plugin Signing is a mechanism introduced in the 2021.2 release cycle to increase security in [JetBrains Marketplace](https://plugins.jetbrains.com) and all of our IntelliJ-based IDEs.
|
||
|
||
The Marketplace signing is designed to ensure that plugins are not modified over the course of the publishing and delivery pipeline.
|
||
If the plugin is not signed by the author or if a certificate has been revoked, a warning dialog will appear in the IDE during installation.
|
||
|
||
On our side, we will check if the public part of a key is present, and we will verify the signature. This is similar to the [Google upload key](https://developer.android.com/studio/publish/app-signing#generate-key) mechanism.
|
||
|
||
## How Signing Works
|
||
|
||
To be sure a file has not been modified, the file will be signed twice – first by the plugin author, then by JetBrains Marketplace.
|
||
|
||
The plugin author's sign-verify process is as follows:
|
||
|
||
- A plugin author generates a key pair and uploads the public part to [JetBrains Hub](https://www.jetbrains.com/hub/).
|
||
- A build tool signs a plugin file during the assembly process.
|
||
- The user uploads the plugin file to JetBrains Marketplace.
|
||
- JetBrains Marketplace checks if the public key is present in the [JetBrains Hub](https://www.jetbrains.com/hub/) user profile.
|
||
- JetBrains Marketplace verifies the signature.
|
||
- The JetBrains sign-verify process is as follows:
|
||
|
||
JetBrains CA is used as the source of truth here.
|
||
Its public part will be added to the IDE Java TrustStore, while the private part will be used only once to generate an intermediate certificate.
|
||
The private key of JetBrains CA is super-secret; in fact, we've already said too much.
|
||
|
||
- The intermediate certificate issues a certificate that will be used to sign plugins.
|
||
This way, it will be possible to re-generate this certificate without access to JetBrains CA's super-secret private key.
|
||
The private key of the intermediate certificate is issued and kept in the AWS Certificate Manager, and no application has access to it; people's access is also limited.
|
||
So now we have an AWS-based Intermediate CA.
|
||
The public part of the intermediate certificate will be added to the plugin file together with the signing certificate.
|
||
- The certificate used to sign plugins is stored securely, too.
|
||
JetBrains Marketplace uses AWS KMS as a signature provider to sign plugin files.
|
||
|
||
## Signing Methods
|
||
|
||
To provide a suitable method for plugin signing, we have introduced the [Marketplace ZIP Signer](https://github.com/JetBrains/marketplace-zip-signer) library.
|
||
It can be executed using the `signPlugin` task provided by the [Gradle IntelliJ Plugin](https://github.com/JetBrains/gradle-intellij-plugin) if your project is Gradle-based.
|
||
Alternatively, it can be used standalone [CLI Tool](#cli-tool).
|
||
|
||
Both methods require a private certificate key to be already present.
|
||
|
||
### Generate Private Key
|
||
|
||
To generate an RSA `private.pem` private key, run the `openssl genpkey` command in the terminal, as below:
|
||
|
||
```bash
|
||
openssl genpkey -aes-256-cbc -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:4096
|
||
```
|
||
|
||
At this point, the content of the generated `private.key` should be provided to the `signPlugin.privateKey` property.
|
||
Provided password should be specified as `signPlugin.password` property in the `signPlugin` configuration.
|
||
|
||
As a next step, we'll generate a `chain.crt` certificate chain with:
|
||
|
||
```bash
|
||
openssl req -key private.key -new -x509 -days 365 -out chain.crt
|
||
```
|
||
|
||
The content of the `chain.crt` file will be used for the `signPlugin.certificateChain` property.
|
||
|
||
### Gradle IntelliJ Plugin
|
||
|
||
The Gradle IntelliJ Plugin in version `1.x` provides the `signPlugin` task which will be executed automatically right before the `publishPlugin` task when `signPlugin.certificateChain` and `signPlugin.privateKey` signing properties are specified.
|
||
Otherwise, it'll be skipped.
|
||
|
||
An example `pluginSigning` configuration may look like:
|
||
|
||
<tabs>
|
||
<tab title="Gradle">
|
||
|
||
```Groovy
|
||
signPlugin {
|
||
certificateChain = """
|
||
-----BEGIN CERTIFICATE-----
|
||
MIIElgCCAn4CCQDo83LWYj2QSTANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJQ
|
||
...
|
||
gdZzxCN8t1EmH8kD2Yve6YKGFCRAIIzveEg=
|
||
-----END CERTIFICATE-----
|
||
""".stripIndent()
|
||
|
||
privateKey = """
|
||
-----BEGIN RSA PRIVATE KEY-----
|
||
MIIJKgIBAAKCAgEAwU8awS22Rw902BmwVDDBMlTREX440BAAVM40NW3E0lJ7YTJG
|
||
...
|
||
EnNBfIVFhh6khisKqTBWSEo5iS2RYJcuZs961riCn1LARztiaXL4l17oW8t+Qw==
|
||
-----END RSA PRIVATE KEY-----
|
||
""".stripIndent()
|
||
|
||
password = "8awS22%#3(4wVDDBMlTREX"
|
||
}
|
||
|
||
publishPlugin {
|
||
token = "perm:a961riC....l17oW8t+Qw=="
|
||
}
|
||
```
|
||
|
||
</tab>
|
||
<tab title="Gradle Kotlin DSL">
|
||
|
||
```Kotlin
|
||
signPlugin {
|
||
certificateChain.set("""
|
||
-----BEGIN CERTIFICATE-----
|
||
MIIElgCCAn4CCQDo83LWYj2QSTANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJQ
|
||
...
|
||
gdZzxCN8t1EmH8kD2Yve6YKGFCRAIIzveEg=
|
||
-----END CERTIFICATE-----
|
||
""".trimIndent())
|
||
|
||
privateKey.set("""
|
||
-----BEGIN RSA PRIVATE KEY-----
|
||
MIIJKgIBAAKCAgEAwU8awS22Rw902BmwVDDBMlTREX440BAAVM40NW3E0lJ7YTJG
|
||
...
|
||
EnNBfIVFhh6khisKqTBWSEo5iS2RYJcuZs961riCn1LARztiaXL4l17oW8t+Qw==
|
||
-----END RSA PRIVATE KEY-----
|
||
""".trimIndent())
|
||
|
||
password.set("8awS22%#3(4wVDDBMlTREX")
|
||
}
|
||
|
||
publishPlugin {
|
||
token.set("perm:a961riC....l17oW8t+Qw==")
|
||
}
|
||
```
|
||
|
||
</tab>
|
||
</tabs>
|
||
|
||
> Do not commit your credentials to the Version Control System! To avoid that, you may use environment variables, like:
|
||
> ```
|
||
> token.set(System.getenv("PUBLISH_TOKEN"))
|
||
> ```
|
||
>
|
||
{type="warning"}
|
||
|
||
### Provide Secrets to IDE
|
||
|
||
To avoid storing hardcoded values in the project configuration, the most suitable method for local development would be using environment variables provided within the _Run/Debug Configuration_.
|
||
|
||
To specify secrets like `PUBLISH_TOKEN` and values required for the `signPlugin` task, modify your Gradle configuration as follows:
|
||
|
||
<tabs>
|
||
<tab title="Gradle">
|
||
|
||
```Groovy
|
||
signPlugin {
|
||
certificateChain = System.getenv("CERTIFICATE_CHAIN")
|
||
privateKey = System.getenv("PRIVATE_KEY")
|
||
password = System.getenv("PRIVATE_KEY_PASSWORD")
|
||
}
|
||
|
||
publishPlugin {
|
||
token = System.getenv("PUBLISH_TOKEN")
|
||
}
|
||
```
|
||
|
||
</tab>
|
||
<tab title="Gradle Kotlin DSL">
|
||
|
||
```Kotlin
|
||
signPlugin {
|
||
certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
|
||
privateKey.set(System.getenv("PRIVATE_KEY"))
|
||
password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
|
||
}
|
||
|
||
publishPlugin {
|
||
token.set(System.getenv("PUBLISH_TOKEN"))
|
||
}
|
||
```
|
||
|
||
</tab>
|
||
</tabs>
|
||
|
||
In the _Run/Debug Configuration_ for `publishPlugin` Gradle task, provide _Environment Variables_ using relevant environment variable names:
|
||
|
||

|
||
|
||
### CLI Tool
|
||
|
||
CLI tool is required in case you don't rely on the Gradle IntelliJ Plugin – i.e. when working with on Themes.
|
||
|
||
To get the latest Marketplace ZIP Signer CLI Tool, visit the [JetBrains/marketplace-zip-signer](https://github.com/JetBrains/marketplace-zip-signer/releases) GitHub Releases page.
|
||
After downloading the `zip-signer-cli.jar`, execute it as below:
|
||
|
||
```bash
|
||
java -jar zip-signer-cli.jar sign\
|
||
-in "unsigned.zip"\
|
||
-out "signed.zip"\
|
||
-cert "/path/to/chain.crt"\
|
||
-key "/path/to/private.pem"\
|
||
-key-pass "PRIVATE_KEY_PASSWORD"
|
||
```
|
||
|
||
|
||
## Signing for custom repositories
|
||
|
||
Signing plugins hosted on a custom repository can be accomplished for added trust between the repository and installation,
|
||
however unlike the marketplace, the custom repository will not re-sign the plugin with the Jetbrains key. Instead a trusted private CA
|
||
or self-signed certificate can be used to sign and validate plugins.
|
||
|
||
### Verification
|
||
|
||
Before looking at how we can sign a plugin, lets first revierw how verification works when a non-Jetbrains certificate is used.
|
||
As of 2021.2, during verification, IntelliJ-based IDEs check if the plugin was signed by the Jetbrains CA certificate or
|
||
any public keys provided by the user via `Preferences > Plugins > Manage Plugin Certificates`. In 2021.2.1, a
|
||
system property has been added: `intellij.plugins.truststore` which can point to a trusted JKS truststore. During verification,
|
||
the plugins public key is extracted from the signature and the last certificate entry in the chain matched against the certificates stored
|
||
in one of the storages from above.
|
||
|
||
### Using a trusted internal CA
|
||
|
||
If an internal CA is available, you can use this to generate certificates to be used for signing. Its important when choosing this route
|
||
that the certificate chain includes the root CA public key at the end of the chain.
|
||
|
||
With this approach, existing internal truststores may exist and could be used. Be sure when choosing a truststore that the CAs are limited
|
||
to the internal CAs you trust. Using an truststore with public CAs can expose the users to an attack vector.
|
||
|
||
If adding a truststore to a users environment is not possible, the user may also add the root CAs public key to `Preferences > Plugins > Manage Plugin Certificates`
|
||
|
||
### Using self-signed certificates
|
||
|
||
Using a self-signed certificate is an option if no internal CAs exist. To generate the key and public key,
|
||
see: [Generate Private Key](#Generate Private Key)
|
||
|
||
If providing users with a truststore, you can generate one with the public key using keytool:
|
||
|
||
```bash
|
||
keytool -import -alias IdeaPlugin -file chain.crt -keystore pluginKeystore.jks -storepass changeit
|
||
```
|
||
(note: the truststore password must remain `changeit`)
|
||
|
||
Otherwise users may add the public key manually to `Preferences > Plugins > Manage Plugin Certificates`. |