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

AzureKeyVault JCA provider requires list aliases permission - Unable to retrieve Azure Key Vault certificate aliases #219

Closed fishermans closed 1 month ago

fishermans commented 1 month ago

"Unable to retrieve Azure Key Vault certificate aliases" is returned when trying to use JCA provider for jarsigner.

Response via Postman gives the following result:

{
    "error": {
        "code": "Forbidden",
        "message": "The user, group or application 'appid=XXXXXXX;iss=https://sts.windows.net/YYYYYYYYY/' does not have certificates list permission on key vault 'AAAAAAAAA;location=BBBBBBBB'. For help resolving this issue, please see https://go.microsoft.com/fwlink/?linkid=2125287",
        "innererror": {
            "code": "ForbiddenByPolicy"
        }
    }
} 

Hence, it is required to have list permissions on the key vault.

Requesting the certificate directly via https://AAAAAAAAAA.vault.azure.net//certificates/CCCCCCCC?api-version=7.2 is working well.

ebourg commented 1 month ago

Thank you for reporting this. I'm not sure this extra permission can be avoided when using jarsigner. When generating an Authenticode signature Jsign already calls https://{vaultname}.vault.azure.net//certificates/{alias} for fetching the certificate, the call to list the certificates is only made when the alias parameter is omitted (for guessing the alias when the vault contains only one certificate). With jarsigner the alias parameter is mandatory and I fail to see why it lists the aliases, there is no need to do that.

ebourg commented 1 month ago

Does your account have the Key Vault Crypto User and Key Vault Certificate User roles?

ebourg commented 1 month ago

If it doesn't work with the Key Vault Crypto User and Key Vault Certificate User roles, try adding the Key Vault Reader role to your account.

Let me know how it works for your, I'll update the documentation afterward if necessary.

fishermans commented 1 month ago

Thank you for reporting this. I'm not sure this extra permission can be avoided when using jarsigner. When generating an Authenticode signature Jsign already calls https://{vaultname}.vault.azure.net//certificates/{alias} for fetching the certificate, the call to list the certificates is only made when the alias parameter is omitted (for guessing the alias when the vault contains only one certificate). With jarsigner the alias parameter is mandatory and I fail to see why it lists the aliases, there is no need to do that.

I was able to sign the jar after modifying the code to return an empty list instead of throwing the exception.

fishermans commented 1 month ago

Does your account have the Key Vault Crypto User and Key Vault Certificate User roles?

I am not the admin of the azure key vault and don't have permissions to have a look at.

fishermans commented 1 month ago
    @Override
    public List<String> aliases() throws KeyStoreException {
        List<String> aliases = new ArrayList<>();

        try {
            Map<String, ?> response = client.get("/certificates?api-version=7.2");
            Object[] certificates = (Object[]) response.get("value");
            for (Object certificate : certificates) {
                String id = (String) ((Map) certificate).get("id");
                aliases.add(id.substring(id.lastIndexOf('/') + 1));
            }
        } catch (IOException ignore) {
            //throw new KeyStoreException("Unable to retrieve Azure Key Vault certificate aliases", ignore);
        }

        return aliases;
    }

Is working for me.

ebourg commented 1 month ago

This is where jarsigner fetches all the certificates in the keystore:

https://github.com/openjdk/jdk/blob/dfacda488bfbe2e11e8d607a6d08527710286982/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Main.java#L2120

It looks like it adds all the certificates to a list of trusted certificates, I guess this is necessary when verifying a signature, but I don't think it's necessary when signing.

A possible workaround would be to throw the KeyStoreException in AzureKeyVaultSigningService.aliases() only if no element of the stacktrace contains the sun.security.tools.jarsigner string.

ebourg commented 1 month ago

Could you try this:

    @Override
    public List<String> aliases() throws KeyStoreException {
        List<String> aliases = new ArrayList<>();

        try {
            Map<String, ?> response = client.get("/certificates?api-version=7.2");
            Object[] certificates = (Object[]) response.get("value");
            for (Object certificate : certificates) {
                String id = (String) ((Map) certificate).get("id");
                aliases.add(id.substring(id.lastIndexOf('/') + 1));
            }
        } catch (IOException e) {
            // check if the call comes from jarsigner
            boolean jarsigner = false;
            for (StackTraceElement element : e.getStackTrace()) {
                if (element.getClassName().contains("jarsigner")) {
                    jarsigner = true;
                    break;
                }
            }

            if (!jarsigner) {
                throw new KeyStoreException("Unable to retrieve Azure Key Vault certificate aliases", e);
            }
        }

        return aliases;
    }
fishermans commented 1 month ago

Looks good to me. 👍

Splitting into a separate method would make the code more readable:

    @Override
    public List<String> aliases() throws KeyStoreException {
        List<String> aliases = new ArrayList<>();

        try {
            Map<String, ?> response = client.get("/certificates?api-version=7.2");
            Object[] certificates = (Object[]) response.get("value");
            for (Object certificate : certificates) {
                String id = (String) ((Map) certificate).get("id");
                aliases.add(id.substring(id.lastIndexOf('/') + 1));
            }
        } catch (IOException e) {
            if (!isJarSignerUsed(e)) {
                throw new KeyStoreException("Unable to retrieve Azure Key Vault certificate aliases", e);
            }
        }

        return aliases;
    }

    private boolean isJarSignerUsed(IOException e) {
        for (StackTraceElement element : e.getStackTrace()) {
            if (element.getClassName().contains("jarsigner")) {
                return true;
            }
        }
        return false;
    }
fishermans commented 1 month ago

Perfect! Thank you so much for your very fast support! Appreciate.