New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
IGN-7871 PKCS#11 signing #43
IGN-7871 PKCS#11 signing #43
Conversation
Add HSM/hardware token keystore capability via new property `pkcs11CfgFile`. Unlike filesystem-based `keystoreFile`, the signing key is stored in a device such as a YubiKey, with driver and other `SunPKCS11` provider properties specified in a configuration file for the `signModule` task. Other changes include refactoring existing tests in `SignModuleTest`, Spotless delinting in `gradle-module-plugin`, and a few minor test and typo fixes. NOTE 1: This is currently versioned at `0.2.0-SNAPSHOT`. After the PR is approved and any other validation--preferably some some testing by IA and partner developers--this will bump to `0.2.0`. NOTE 2: This was integration tested against a YubiKey 5 NFC with the wrinkle that the slot `9a` authorization key was used, not the `9c` signing key. There are notes in `SignModuleTest` as to why this workaround is necessary. (At some point we might be able to sign with the "correct" YK5 slot but it appears to require some tricky callback code.) NOTE 3: Because we call down to `module-signer` to do the actual signing, and because its filter is narrowed to keys of type `SHA256withRSA`, the key in the keystore must be that type. (This constraint predates the current enhancement.) At some point we'll likely enhance `module-signer` to support more key algorithms such as Elliptic Curve and whatever post-quantum algorithms ultimately pass muster with NIST and related entities.
ignition.signing.certAlias=selfsigned | ||
ignition.signing.keystorePassword=password | ||
ignition.signing.certFile=./certificate.pem | ||
ignition.signing.certPassword=password |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No DRY/re-use on the constant values because 1) we want to test those constants too, not just the logic, and 2) clarify test inputs for the reader--More BDD-oriented.
val keystore: Path, | ||
val certFile: Path, | ||
val keystore: Path?, | ||
val certFile: Path?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Laying the groundwork for fetching the cert file straight from the HSM. Not for all use cases, but some.
@@ -50,25 +48,36 @@ open class BaseTest { | |||
return name | |||
} | |||
|
|||
protected fun prepSigningResourcesForModuleName( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dead code AFAICT.
open fun runTaskAndFail( | ||
projectDir: File, | ||
taskArgs: List<String> | ||
): BuildResult = setupRunner(projectDir, taskArgs).buildAndFail() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to try-catch
on negative tests with buildAndFail
.
|
||
@Test | ||
// @Tag("IGN-7871") | ||
fun `skip signing - no need for signing properties`() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New test but on old use case. Just for more coverage.
// This is a test with an actual PKCS#11-compliant YubiKey 5, on Windows. | ||
// As such it is typically set to @Ignore. | ||
@Test | ||
@Ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment a few lines up. We can't stick a YubiKey up on GitHub. 馃槃
@@ -1,5 +0,0 @@ | |||
ignition.signing.keystoreFile=./keystore.jks |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test fixture from older alpha version of the plugin, IIUC.
listOf(SKIP, null).none { v -> p.getOrNull() == v } | ||
} | ||
) { | ||
throw InvalidUserDataException( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you squint at the Gradle API doc, this is the closest of their exception types that matches this situation.
"keystorePassword: ${"*".repeat(keystorePassword.length)}, " + | ||
"Signing module with keystoreFile: ${keyStoreFile?.absolutePath}, " + | ||
"pkcs11CfgFile: ${pkcs11CfgFile?.absolutePath}, " + | ||
"keystorePassword: ${"*".repeat(20)}, " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leak no info whatsoever about secrets.
|
||
val privateKey: RSAPrivateKey = keyStore.getKey(certAlias, certPassword.toCharArray()) as RSAPrivateKey | ||
val privateKey: PrivateKey = keyStore.getKey(certAlias, certPassword.toCharArray()) as PrivateKey |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Laying the groundwork for other key algorithms.
@@ -194,6 +250,26 @@ open class SignModule @Inject constructor(_providers: ProviderFactory, _objects: | |||
throw Exception("Signing key file ${keystoreFile.absolutePath} did not exist!") | |||
} | |||
} | |||
|
|||
// PKCS#11 HSM (hardware key)-based keystore | |||
if (pkcs11Cfg.isPresent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and the whole getKeyStore
function should arguably be refactored away in favor of module-signer
's existing keystore + key-fetching logic.
|
||
ModuleSigner(privateKey, cert.inputStream()) | ||
.signModule(PrintStream(OutputStream.nullOutputStream()), unsignedModule, outFile) | ||
} | ||
|
||
private fun loadKeyStore(ks: KeyStore, ksPwd: String, ksFile: File?) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment above in getKeyStore
about refactoring this away in favor of logic already in module-signer
. Something for a future ticket.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No real blockers - just some minor code cleanup opportunities I think
gradle-module-plugin/src/main/kotlin/io/ia/sdk/gradle/modl/task/SignModule.kt
Outdated
Show resolved
Hide resolved
gradle-module-plugin/src/main/kotlin/io/ia/sdk/gradle/modl/task/SignModule.kt
Outdated
Show resolved
Hide resolved
gradle-module-plugin/src/functionalTest/kotlin/io/ia/sdk/gradle/modl/BaseTest.kt
Outdated
Show resolved
Hide resolved
} | ||
|
||
// uncomment if you need a little debugging | ||
//println("props:\n$props") | ||
gradlePropsFl.appendText(props) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason I thought Kotlin was not auto-casting to String
before.
Holding off on merging until
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have not had a chance to test with hardware myself so won't 'approve' yet, but the changes look good to me; if we have additional confirmed testers, I'm a go.
An integration partner has a Digicert KeyLocker cloud keystore, which requires passing explicitly down to the `:signModule` task neither `keystorePassword` nor `certPassword`; unlocking is via other means (env vars consumed by the vendor PKCS#11 module, IIUC). This commit makes those options/properties optional, translating their ommission as `null` down the call stacks. Interestingly, omitting `keystorePassword` is not even a blocker for PKCS#12 (file-based) keystores; it just skips an integrity check on keystore load when no keystore password is specified. NOTE: Plans are in place for `certFile` ommission as well. In many cases the cert chain can be read straight from the keystore. That will come in a future PR.
@@ -129,7 +129,7 @@ open class BaseTest { | |||
Files.createDirectories(targetDir) | |||
|
|||
return resources.map { resource -> | |||
ClassLoader.getSystemResourceAsStream("certs/$resource").let { inputStream -> | |||
ClassLoader.getSystemResourceAsStream("certs/$resource").use { inputStream -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Close potential stream leak.
assertContains( | ||
out, | ||
"file as 'ignition.signing.keystorePassword=<value>" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No longer immediate fatalities.
@@ -188,25 +176,49 @@ class SignModuleTest : BaseTest() { | |||
"--keystoreFile=${signResources.keystore}", | |||
"--certFile=${signResources.certFile}", | |||
"--certAlias=selfsigned", | |||
// --keystorePassword is not required by :signModule task for most | |||
// types of keystores, though can add an integrity check on ks load |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even PKCS#12 keystores don't care much about the keystore password on a simple KeyStore.load
, only the cert password.
// outside of the call stack. Simulate this, somehow, if we can figure | ||
// out a good way to do so. | ||
@Test | ||
@Ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Iron this out in a future issue/ticket/PR.
unsignedModule, | ||
signed.get().asFile | ||
) | ||
signModule(unsignedModule) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sonar was whining about the number of args. Plus all but unsignedModule
are instance properties.
ksFile?.inputStream() | ||
// if PKCS#11 HSM (hardware key) keystore, forgo the input stream | ||
?.takeUnless { ks.type == PKCS11_KS_TYPE } | ||
.use { maybeStream -> | ||
ks.load(maybeStream, ksPwd.toCharArray()) | ||
ks.load(maybeStream, ksPwd?.toCharArray()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null
here is not a blocker even on PKCS#12 (only?) keystores with passwords; see the Javadoc for the impact.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
QAed by both an integration partner using DigiCert KeyLocker and by IA QA with a YubiKey 5 and a genuine, non self-signed cert. |
See this IA forum post for instructions on using the new, |
Oddly enough we had a GitHub workflow that is still publishing to the Gradle Plugin Portal. |
馃摉 Background
The
signModule
custom task in thegradle-module-plugin
(a.k.a.io.ia.sdk.modl
) supports PKCS#12 file-based Java keystores for signing modules. Certain software supply chain best practices recommend using PKCS#11-based hardware security modules (HSMs) for more secure keystore implementations. The private keys never leave the HSM, and the HSM itself signs the module digests.鈿欙笍 Summary
Enhance the
signModule
task to allow for signing via HSM and other PKCS#11-compliant keystores. (Which need not be hardware tokens like YubiKeys, but may be cloud-based or other keystores implemented in software that are typically not local to the build machine.)More specifically, add this capability via new property/option
pkcs11CfgFile
.馃摑 Reviewer Notes
Use the Hide whitespace setting on the Files changed tab to eliminate a bunch of the delinting noise.
Because we call down to
module-signer
to do the actual signing, and because its filter is narrowed to keys of typeSHA256withRSA
, the key in the keystore must be that type. (This constraint predates the current enhancement.) At some point we'll likely enhancemodule-signer
to support more key algorithms such as Elliptic Curve and whatever post-quantum algorithms ultimately pass muster with NIST and related entities.On initial PR submission the version is
0.2.0-SNAPSHOT
. After the PR is approved and any other validation--preferably some some testing by IA and partner developers--the PR will emerge from Draft status andthe version will bump to0.2.0
. We may have to hand-crank publication to the Gradle Plugin repository as I'm not sure whether therelease.yml
workflow is working.馃И QA Notes
This was integration tested against a YubiKey 5 NFC with the wrinkle that the slot
9a
authorization key was used, not the9c
signing key. There are notes inSignModuleTest
as to why this workaround is necessary. (At some point we might be able to sign with the "correct" YK5 slot but it appears to require some tricky callback code.)Fixes IGN-7871 (/issues/41)