The osslsigncode has added support for signning PowerShell script recently. In this post, I will demonstrate how to use Hashicorp Vault and osslsigncode to sign PowerShell scripts.

Prerequisites

I am going to make the following assumptions:

  1. You have a working Hashicorp Vault cluster running at https://your-vault-server.com with a PKI mount named your-pki-mount.
  2. The root CA is already installed on the machine where you are going to run the PowerShell script, it should be installed in the Trusted Root Certification Authorities store.
  3. The intermediate CA is already installed on the machine where you are going to run the PowerShell script, it should be installed in the Intermediate Certification Authorities store.
  4. The osslsigncode is installed on the machine where you are going to sign the PowerShell script, you will need to get the latest version from one of the CI runs

Create the vault role for issuing code signing certificates

#!/usr/bin/python3
import requests
import argparse

def create_vault_pki_role(vault_addr, pki_mount, role_name, vault_token):
    # Verify the Vault address
    if not vault_addr.startswith("http://") and not vault_addr.startswith("https://"):
        print("Error: Vault address must start with http:// or https://")
        return

    # Prepare the API URL and payload with the specified parameters
    url = f"{vault_addr}/v1/{pki_mount}/roles/{role_name}"
    payload = {
        "server_flag": False,
        "client_flag": False,
        "code_signing_flag": True,
        "email_protection_flag": False,
        "key_type": "ec",
        "key_bits": 521,
        "key_usage": ["DigitalSignature", "KeyEncipherment"],
        "ext_key_usage": ["CodeSigning"],
        "use_csr_common_name": True,
        "require_cn": False,
    }

    # Headers for authentication
    headers = {
        "X-Vault-Token": vault_token
    }

    # Make the request to create the role
    response = requests.post(url, json=payload, headers=headers)
    if response.status_code in [200, 204]:
        print("PKI role created successfully.")
    else:
        print(f"Error creating PKI role: {response.status_code} - {response.text}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Create a Vault PKI role with specific parameters.")
    parser.add_argument("--vault-addr", required=True, help="The Vault address with http:// or https://")
    parser.add_argument("--pki-mount", required=True, help="The PKI mount where the role will be created.")
    parser.add_argument("--role-name", required=True, help="The name of the role to be created.")
    parser.add_argument("--vault-token", required=True, help="The Vault token for authentication.")

    args = parser.parse_args()

    create_vault_pki_role(args.vault_addr, args.pki_mount, args.role_name, args.vault_token)

let’s understand what each parameter does:

server_flag: Determines if certificates issued under this role can be used for server authentication. false means certificates cannot be used to authenticate servers.

client_flag: Determines if certificates issued can be used for client authentication. false means certificates cannot be used to authenticate clients.

code_signing_flag: Enables certificates to be used for code signing, which is essential for confirming the authenticity and integrity of software.

email_protection_flag: Determines if the certificate can be used for securing email through encryption and signing. false means this is not allowed.

key_type: Specifies the type of key to generate, where “ec” stands for Elliptic Curve cryptography, a modern choice for efficient and secure keys.

key_bits: For EC keys, this indicates the curve size. 521 is one of the highest security levels offered by elliptic curve cryptography.

key_usage: A list of purposes for which the key can be used, such as DigitalSignature and KeyEncipherment, essential for code signing and securing data in transit, respectively.

ext_key_usage: An extended list of specific uses, with “CodeSigning” indicating the certificate is suitable for signing code.

use_csr_common_name: If true, the common name in the CSR (Certificate Signing Request) will be used when issuing the certificate, allowing for customization of the certificate’s subject field.

require_cn: Whether a Common Name is required in the CSR. false allows for more flexibility in certificate signing requests.

The script accepts a few arguments: the Vault address (vault-addr), the PKI mount point (pki-mount), the name of the PKI role(role-name), the vault token for authentication(vault-token). It verifies that the Vault address starts with http:// or https:// and then proceeds to create the PKI role with the specified parameters, ensuring security best practices are followed.

Save the script as create_vault_pki_role.py and run it with the following command:

create_vault_pki_role.py --vault-addr https://your-vault-server.com --pki-mount your-pki-mount --role-name my-code-signing-role --vault-token your-vault-token

This command will create a new PKI role named my-code-signing-role with the specified security parameters on your Vault server.

The vault role access policy

path "your-pki-mount/issue/your-role-name" {
  capabilities = ["update"]
}

This policy snippet ensures that the holder can only issue certificates using your-role-name role under the your-pki-mount mount point. It does not grant permissions to create, read, list, or delete roles within the PKI secrets engine, nor does it allow managing other aspects of the PKI engine such as setting up CA certificates.

Issue a code signing certificate

vault write your-pki-mount/issue/your-role-name common_name="your-common-name" format="pem" ttl="24h"

This command will issue a code signing certificate with the common name your-common-name and a validity period of 24 hours for test purpose. The certificate will be returned in PEM format.

Sign the PowerShell script

.\osslsigncode.exe sign -certs .\test.crt -key .\test.key -n "Hello World" -i http://www.yourwebsite.com -in .\hello.ps1 -out .\hello-signed.ps1 -t http://timestamp.digicert.com

This command will sign the hello.ps1 PowerShell script with the certificate test.crt and the private key test.key. The -n option specifies the name of the signed script, the -i option specifies the URL of the software publisher, the -t option specifies the URL of the timestamp server. The signed script will be saved as hello-signed.ps1.

Execute the signed PowerShell script

 powershell -ExecutionPolicy AllSigned .\hello-signed.ps1

This command will execute the signed hello-signed.ps1 PowerShell script with the AllSigned execution policy. You may still receive a warning about the script being from an untrusted publisher, that’s because the code signing certificate is not trusted by the machine, this is the expected behaviour. You can import the certificate into the Trusted Publishers store to avoid this warning.

Conclusion

To sign PowerShell scripts with Hashicorp Vault and osslsigncode, a few conditions need to be met:

  1. The root CA need to be installed on the machine where you are going to run the PowerShell script, it should be installed in the Trusted Root Certification Authorities store.
  2. The intermediate CA need to be installed on the machine where you are going to run the PowerShell script, it should be installed in the Intermediate Certification Authorities store.
  3. The code signing certificate need to be installed on the machine where you are going to run the PowerShell script, it should be installed in the Trusted Publishers store.
  4. Ensure timestamping is used when signing the script so that the signature remains valid after the certificate expires.

To-do

At this stage I don’t know if the requirement to add signing certificate to the Trusted Pblishers store is needed for EV certificate, but for IV and OV certificate I am pretty sure it is necessary. As there is no way to create a self-signed EV certificate, I will need to test this with a real EV certificate.

I am not sure if I need use the full signing certificate chain when signing the script, I don’t think it matters when the above requirements are fulfilled. But EV certificate might be different, I will need to test this as well.