ebourg / jsign

Java implementation of Microsoft Authenticode for signing Windows executables, installers & scripts
https://ebourg.github.io/jsign
Apache License 2.0
250 stars 107 forks source link

apksigner & jsign with AWS? #226

Open hongkongkiwi opened 3 weeks ago

hongkongkiwi commented 3 weeks ago

Hi there,

I'm struggling a little bit to get this working. I'd like to use JSign to sign an apk (using apksigner) using an AWS KMS key.

Here's what I've got (not, I've substituted the jar paths here, but locally I'm using the full path to the jar file).

In this case, CREDS="<ACCESS_KEY>|<SECRET_KEY>|<SESSION_TOKEN>" as specified in the docs.

java -cp apksigner.jar:jsign-6.0.jar com.android.apksigner.ApkSignerTool sign \
    --provider-class "net.jsign.jca.JsignJcaProvider" \
    --provider-arg "us-west-1" \
    --ks NONE \
    --ks-type AWS \
    --ks-pass "env:CREDS" \
    --ks-key-alias "alias/MyKeyAlias" \
    --in ./app/build/outputs/apk/debug/app-debug.apk \
    --out ./test-signed.apk

Is this the correct usage? I'm getting this error:

Failed to load signer "signer #1"
java.lang.UnsupportedOperationException
    at net.jsign.jca.AbstractKeyStoreSpi.engineIsKeyEntry(AbstractKeyStoreSpi.java:84)
    at java.base/java.security.KeyStore.isKeyEntry(KeyStore.java:1346)
    at com.android.apksigner.SignerParams.loadPrivateKeyAndCertsFromKeyStore(SignerParams.java:304)
    at com.android.apksigner.SignerParams.loadPrivateKeyAndCerts(SignerParams.java:202)
    at com.android.apksigner.ApkSignerTool.getSignerConfig(ApkSignerTool.java:438)
    at com.android.apksigner.ApkSignerTool.sign(ApkSignerTool.java:353)
    at com.android.apksigner.ApkSignerTool.main(ApkSignerTool.java:92)
ebourg commented 3 weeks ago

Could you try again with the latest build: https://github.com/ebourg/jsign/actions/runs/9286970939/artifacts/1548458586

hongkongkiwi commented 3 weeks ago

The latest build fixes the issue above, it almost works, but I found that when I want to pass the certificate file that's going to be used for signing the build and now that's where I'm getting stuck (forgot to add that arg in above command):

java -cp apksigner.jar:jsign-6.1-SNAPSHOT.jar com.android.apksigner.ApkSignerTool sign \
    --provider-class "net.jsign.jca.JsignJcaProvider" \
    --provider-arg "us-west-1" \
    --ks NONE \
    --ks-type AWS \
    --ks-pass "env:CREDS" \
    --ks-key-alias "c00c48df-b7a5-47ce-be55-476db10f407b" \
    --in ./app/build/outputs/apk/debug/app-debug.apk \
    --out ./test-signed.apk

This above doesn't work as I need the certificate:

Failed to load signer "signer #1"
java.lang.RuntimeException: Failed to load the certificate from
    at net.jsign.KeyStoreType.lambda$getCertificateStore$0(KeyStoreType.java:672)
    at net.jsign.jca.AmazonSigningService.getCertificateChain(AmazonSigningService.java:152)
    at net.jsign.jca.SigningServiceKeyStore.engineGetCertificateChain(SigningServiceKeyStore.java:43)
    at java.base/java.security.KeyStore.getCertificateChain(KeyStore.java:1100)
    at net.jsign.jca.JsignJcaProvider$JsignJcaKeyStore.engineGetCertificateChain(JsignJcaProvider.java:134)
    at java.base/java.security.KeyStore.getCertificateChain(KeyStore.java:1100)
    at com.android.apksigner.SignerParams.loadPrivateKeyAndCertsFromKeyStore(SignerParams.java:359)
    at com.android.apksigner.SignerParams.loadPrivateKeyAndCerts(SignerParams.java:202)
    at com.android.apksigner.ApkSignerTool.getSignerConfig(ApkSignerTool.java:438)
    at com.android.apksigner.ApkSignerTool.sign(ApkSignerTool.java:353)
    at com.android.apksigner.ApkSignerTool.main(ApkSignerTool.java:92)
Caused by: java.io.FileNotFoundException:  (No such file or directory)
    at java.base/java.io.FileInputStream.open0(Native Method)
    at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
    at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
    at net.jsign.CertificateUtils.loadCertificateChain(CertificateUtils.java:57)
    at net.jsign.KeyStoreType.lambda$getCertificateStore$0(KeyStoreType.java:670)
    ... 10 more

Maybe it's simple, but I couldn't quite figure out a way to do pass jsign the certfile name. I've tried:

--ks-certfile
--ks-cert
--certfile
--cert

Looking at it logically I guess we are actually running apksigner and can only use the arguments it supports and it doesn't seem to support any --ks-cert* option (it does support a --cert option, but it uses this exclusively with local keyfiles only, it's not used for plugins),

The apksigner does mention a --cert option when using jsign as a keystore provider but in the latest apksigner this does not actually work.

Failed to load signer "signer #1": --ks and --cert may not be specified at the same time

So instead I guess we need to pass the certfile to use for signing in a different way back to jsign. Either as part of --ks-key-alias or perhaps --ks-provider-arg ?

hongkongkiwi commented 3 weeks ago

For reference for others reading this the AWS KMS doesn't store certificates at all, only public and private keys. If you want to use it for signing with any kind of certificate that's signed by the key, you'll have to generate a signed certificate, then use that as a file, then store this certificate out of band somewhere.

Amazon does have some (expensive) solutions such as amazon Private CA which can handle storing certificates, but not their low cost KMS product this plugin uses.

Also for others who are going down the same track, if you want to quickly export credentials in a jsign compatible format, you can use this one liner to set the JSIGN_AWS_CREDS environment variable: export JSIGN_AWS_CREDS=$(aws sts get-session-token | jq -r '.Credentials | "\(.AccessKeyId)|\(.SecretAccessKey)|\(.SessionToken)"')

hongkongkiwi commented 3 weeks ago

@ebourg I just found that this rejected) PR #215 contains a fix which can exactly fix this issue: https://github.com/ebourg/jsign/pull/215/commits/f81a6bb9ba3fa9cf2f3c11d1cb3f4892a8f90abc , that PR was talking about a problem with jarsigner, but with jarsigner can you pass an option for a certchain so it's not necessary, but for apksigner you cannot, so we need a method like this to pass it.

Essentially, with the above fix we can pass the certificate file as a system parameter:

java -cp apksigner.jar:./jsign-6.1-SNAPSHOT.jar -Djsign.certfile=certificate.pem com.android.apksigner.ApkSignerTool sign ...

If we don't use above, we could change the name of the keystore to be something like "region=us-east-1:certfile=certificate.pem" via the --provider-arg. This might be slightly more complex though.

ebourg commented 3 weeks ago

@hongkongkiwi Thank you for the analysis. I didn't realize that the --ks and --cert parameters were mutually exclusive, and indeed it won't work with the services storing only the key (AWS, Google Cloud and Oracle Cloud).

I see several approaches to this problem:

  1. Use a system property to pass the certificate as suggested in the PR #215. I'm not a big fan of this solution, this would make it difficult to use the JCA provider programmatically on a server handling concurrent requests.
  2. Append the certificate to another parameter. You suggested the keystore, but the alias would be more suitable.
  3. Send a PR to AOSP to make --cert usable with --ks. I've contacted the apksigner developers to see if this is possible.
  4. Send a PR to AOSP to integrate Jsign into apksigner. Even better, no need to mess with complex command line parameters.
  5. Fork apksigner and integrate it with Jsign. If Google rejects the Jsign integration, let's do it ourself. But this implies a continuous maintenance to keep the forked tool up to date.
  6. Import the apksigner code into Jsign. APK would become just another format supported by Jsign. The only issue is the mismatch between the Jsign and the apksigner parameters. apksigner has more settings (key rotation, multiple signature schemes, sdk version) and I'd rather not pollute the Jsign parameters with these. Or I could introduce an alternative sign command replicating the apksigner parameters (i.e. jsign signapk --ks-type AWS --ks-key-alias ...).
hongkongkiwi commented 3 weeks ago

Idea (1) using the system property wouldn't cause a conflict in my case as I'm calling it each time with the specific property set in a Lambda. But I see what you mean, it's not going to be idea for some people's batch use cases. Probably another option is better.

Ideally, I think (3) is a good move. This seems like a bug and other tools support this just fine.

Google uses the sun PKCS11 plugin along with a google cloud pkcs11 implementation. This uses a separate config file to setup the parameters for the sun PKCS11 plugin. That's why they havn't really come across this probably since the sun pkcs11 plugin has it's own way of configuring.

I think implementing (2) is a also good idea and something that's relatively straightforward and can be done without assistance from third parties. Perhaps it could be done in a way that is backwards compatible. For example first we check if we have key=value then we can pass multiple keys based on jsign's normal input parameters. If no key=value exists then just fallback to existing method of processing the alias.

That could be "alias=abc|certfile=abc.pem". This would assume that aliases don't normally contain a pipe character or equals character (not sure about this?

Methods 4, 5, 6 seem to have a high development burden and don't address other possible tools which may have the same issue (although I don't know any of those).

Google seems to regularly develop and modify apksigner, especially as they are on signing version v4 now. I like the idea that I can just use the latest version of that alongside the latest version of jsign. This is a great product and I don't think it makes sense to add a lot of development distraction for keeping upto date with apksigner.

hongkongkiwi commented 2 weeks ago

Looks like this issue affects all keystore types when using apksigner and jsign via JCA, not only AWS. I'm trying to use JKS keystore type as a JCA plugin, but it also has the same issue.

ebourg commented 2 weeks ago

What exception do you get with a JKS keystore?