{"record":{"id":"812a9ba735bdf87d","repo":"conductor-oss/conductor","slug":"unable-to-sign-file-storage-url","errorCode":null,"errorMessage":"Unable to sign file-storage URL","messagePattern":"Unable to sign file-storage URL","errorType":"exception","errorClass":"IllegalStateException","httpStatus":500,"severity":"error","filePath":"core/src/main/java/org/conductoross/conductor/core/storage/FileStorageUrlSigner.java","lineNumber":168,"sourceCode":"                \"\\n\",\n                VERSION,\n                operation.getValue(),\n                nullToEmpty(workflowId),\n                nullToEmpty(fileId),\n                String.valueOf(expirationEpochSeconds),\n                nullToEmpty(uploadId),\n                partNumber == null ? \"\" : String.valueOf(partNumber));\n    }\n\n    private byte[] hmac(String canonical, ConductorFileStorageProperties.Key key) {\n        try {\n            Mac mac = Mac.getInstance(HMAC_SHA_256);\n            mac.init(\n                    new SecretKeySpec(\n                            key.getSecret().getBytes(StandardCharsets.UTF_8), HMAC_SHA_256));\n            return mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8));\n        } catch (Exception exception) {\n            throw new IllegalStateException(\"Unable to sign file-storage URL\", exception);\n        }\n    }\n\n    private boolean isBlank(String value) {\n        return value == null || value.isBlank();\n    }\n\n    private String nullToEmpty(String value) {\n        return value == null ? \"\" : value;\n    }\n\n    /** Operations encoded into signed content URLs. */\n    public enum Operation {\n        UPLOAD(\"upload\", \"PUT\"),\n        DOWNLOAD(\"download\", \"GET\");\n\n        private final String value;\n        private final String httpMethod;","sourceCodeStart":150,"sourceCodeEnd":186,"githubUrl":"https://github.com/conductor-oss/conductor/blob/cf7c3e4a8adfb158be778ab1ec525323c363cd3a/core/src/main/java/org/conductoross/conductor/core/storage/FileStorageUrlSigner.java#L150-L186","documentation":"Thrown by FileStorageUrlSigner.hmac() as a catch-all wrapper around the three JCA operations it performs: Mac.getInstance(\"HmacSHA256\"), mac.init(new SecretKeySpec(...)), and mac.doFinal(...). Any exception from those calls (NoSuchProviderException/NoSuchAlgorithmException for a missing HmacSHA256, InvalidKeyException for a bad key, or a runtime failure in doFinal) is rethrown as IllegalStateException with the original as the cause. It signals that signing was enabled and a key was selected, but the cryptographic step itself broke.","triggerScenarios":"Called transitively from sign() (signing IS enabled and keys is non-empty) when: (a) the JCE provider backing HmacSHA256 is unavailable (minimal/stripped JRE, or a native-image build missing reflection/resource metadata for the Mac algorithm); (b) the selected key's secret is empty or produces a zero-length/invalid SecretKeySpec after construction-time validation was bypassed (e.g. the Key bean was mutated after FileStorageUrlSigner was built, or @ConfigurationProperties relax-binding turned a missing env var into an empty string); (c) doFinal throws on an unexpected encoding or provider state error.","commonSituations":"Running under GraalVM native-image without registering HmacSHA256 in reflection/resource config; production secret env var (e.g. ${SIGNING_SECRET}) unset so Spring binds an empty string, bypassing the constructor's validate() because the bean was already built; deploying on a JMOD-trimmed JDK that dropped the SunJCE provider; a config-reload/Cloud event mutated the Key.secret to null at runtime; misconfigured BouncyCastle-only setup where HmacSHA256 was not registered under that name.","solutions":["Inspect the attached cause (exception.getCause()) in the logs first: NoSuchAlgorithmException points to a missing provider, InvalidKeyException points to the key bytes.","Confirm the signing key secret is non-empty at runtime: log key.getId() and key.getSecret().length() (never the value) from the calling path; if zero, fix the env var / config source feeding conductor.file-storage.conductor.signing.keys[*].secret.","If running on GraalVM native-image, add HmacSHA256 and SecretKeySpec to reachability metadata (reflection-config / resource-config) or use the native-image hints so the SunJCE provider is retained.","On a custom/stripped JDK, ensure the SunJCE provider is present (java.security lists it) or register an alternative provider (e.g. BouncyCastle) that exposes HmacSHA256.","If keys can change at runtime, rebuild the FileStorageUrlSigner bean on config refresh instead of mutating the Key objects in place, so validate() re-runs against the new secret.","Wrap the calling controller/service so this surfaces as a 5xx with the cause class name rather than propagating an opaque IllegalStateException to the client."],"exampleFix":"// before — env var unset, Spring binds empty secret past constructor validation\nconductor:\n  file-storage:\n    conductor:\n      signing:\n        enabled: true\n        keys:\n          - id: k1\n            secret: ${SIGNING_SECRET}   # SIGNING_SECRET not exported -> \"\"\n\n// after — fail fast with a placeholder check in the env, or use a non-blank default\nconductor:\n  file-storage:\n    conductor:\n      signing:\n        enabled: true\n        keys:\n          - id: k1\n            secret: ${SIGNING_SECRET:}\n# plus a startup guard:\n// if (signerKey.getSecret() == null || signerKey.getSecret().isBlank()) {\n//     throw new IllegalStateException(\"SIGNING_SECRET must be set\");\n// }","handlingStrategy":"try-catch","validationCode":"// Pre-flight the crypto + key before delegating to signer.sign()\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.nio.charset.StandardCharsets;\n\npublic boolean signingStackIsHealthy(ConductorFileStorageProperties.Key key) {\n    if (key == null || key.getSecret() == null || key.getSecret().isBlank()) {\n        return false;\n    }\n    try {\n        Mac mac = Mac.getInstance(\"HmacSHA256\");\n        mac.init(new SecretKeySpec(\n            key.getSecret().getBytes(StandardCharsets.UTF_8), \"HmacSHA256\"));\n        mac.doFinal(new byte[]{0x00});\n        return true;\n    } catch (Exception e) {\n        return false;\n    }\n}\n\n// usage: only call signer.sign(...) when signingStackIsHealthy(activeKey) is true","typeGuard":null,"tryCatchPattern":"// Distinguish the crypto failure from other IllegalStateExceptions and surface the cause class\ntry {\n    SignedUrl signed = signer.sign(op, workflowId, fileId, exp, uploadId, partNumber);\n} catch (IllegalStateException e) {\n    if (\"Unable to sign file-storage URL\".equals(e.getMessage())) {\n        Throwable cause = e.getCause();\n        log.error(\"HMAC signing failed (cause={})\", cause == null ? \"unknown\" : cause.getClass().getName(), e);\n        // Do NOT retry the same key: it is either empty or the provider is gone.\n        // Return 500 and page; recovery is a config/key fix, not a retry.\n        throw new FileStorageSigningUnavailableException(cause);\n    }\n    throw e;\n}","preventionTips":["Always read exception.getCause() first — its class (NoSuchAlgorithmException vs InvalidKeyException) tells you whether the fix is the provider or the key.","Bind signing key secrets from a source that errors on absence (e.g. Spring Cloud Config, Vault) rather than defaulting to an empty string, so an unset env var fails at boot via validate() instead of at sign() time.","For GraalVM native-image builds, add a RuntimeHints bean registering HmacSHA256 / SecretKeySpec so reachability analysis keeps the SunJCE provider.","Never mutate a Key instance after the FileStorageUrlSigner constructor runs; rebuild the bean on key rotation so validate() re-checks id and secret.","Add a startup health check that performs one throwaway sign() in @PostConstruct so a broken crypto stack is detected before traffic arrives."],"tags":["crypto","hmac","jce","file-storage","native-image","configuration"],"backgroundTag":null,"analyzedSha":"cf7c3e4a8adfb158be778ab1ec525323c363cd3a","analyzedAt":"2026-08-14T03:33:19.897Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}