Skip to main content

keystore-credstore

Use this skill whenever migrating an SAP BTP Neo Java app that uses keystores, password storage, or any JNDI-bound credential lookup to Cloud Foundry. Triggers on `com.sap.cloud.crypto.keystore.api.KeyStoreService`, `com.sap.cloud.security.password.PasswordStorage`, `<resource-ref>` blocks for `KeyStoreService` or `PasswordStorage` in `web.xml`, `InitialContext.lookup("java:comp/env/KeyStoreService"|"PasswordStorage")`, or `@Resource(name = "KeyStoreService"|"PasswordStorage")`. Even if the user just says "migrate this app to CF" without naming credentials explicitly, invoke this skill the moment any of those symbols appear in the source. Replaces JNDI lookups with the SAP Credential Store REST API over mTLS, ships an offline-friendly Lombok-free Java client, and emits a Step 4 mtad that BINDS to a pre-existing, manually-provisioned credstore instance via `org.cloudfoundry.existing-service` (do NOT create or delete the credstore from the MTA).

Datos de origen

Repositorio
SAP-samples/btp-neo-java-app-migration
Última actividad en el origen
29 de julio de 2026 a las 13:30
Idioma detectado de SKILL.md
inglés
Estrellas
6
Forks
0

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Explorador de archivos
9 archivos

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
keystore-credstore
description
Use this skill whenever migrating an SAP BTP Neo Java app that uses keystores, password storage, or any JNDI-bound credential lookup to Cloud Foundry. Triggers on `com.sap.cloud.crypto.keystore.api.KeyStoreService`, `com.sap.cloud.security.password.PasswordStorage`, `<resource-ref>` blocks for `KeyStoreService` or `PasswordStorage` in `web.xml`, `InitialContext.lookup("java:comp/env/KeyStoreService"|"PasswordStorage")`, or `@Resource(name = "KeyStoreService"|"PasswordStorage")`. Even if the user just says "migrate this app to CF" without naming credentials explicitly, invoke this skill the moment any of those symbols appear in the source. Replaces JNDI lookups with the SAP Credential Store REST API over mTLS, ships an offline-friendly Lombok-free Java client, and emits a Step 4 mtad that BINDS to a pre-existing, manually-provisioned credstore instance via `org.cloudfoundry.existing-service` (do NOT create or delete the credstore from the MTA).
disable-model-invocation
false
allowed-tools
Read, Edit, Write, Bash, Grep, Glob
# Keystore Service Migrate keystore and password storage to SAP Credential Store. ## Purpose Replace Neo's `KeyStoreService` and `PasswordStorage` JNDI resources with SAP Credential Store service using mTLS authentication and REST API. ## Detection This skill applies if any of these patterns are found: ### In web.xml ```xml <resource-ref> <res-ref-name>KeyStoreService</res-ref-name> <res-type>com.sap.cloud.crypto.keystore.api.KeyStoreService</res-type> </resource-ref> <!-- OR --> <resource-ref> <res-ref-name>PasswordStorage</res-ref-name> <res-type>com.sap.cloud.security.password.PasswordStorage</res-type> </resource-ref> ``` ### In Java source files ```java import com.sap.cloud.crypto.keystore.api.KeyStoreService; import com.sap.cloud.security.password.PasswordStorage; @Resource(name = "KeyStoreService") private KeyStoreService keyStoreService; @Resource(name = "PasswordStorage") private PasswordStorage passwordStorage; ``` ## Prerequisites > **Working directory:** This skill must run inside the `-cf-migration` copy of your app, created by `jakarta-java25-migration` or `neo-to-cf-migration-orchestrator`. If your current directory does not end in `-cf-migration`, switch to it before proceeding. Before invoking this skill, ensure you have invoked: 1. **sdk-replacement** - `Use the sdk-replacement skill` - Sets up SAP Cloud SDK - REQUIRED before this skill Also required: - Credential Store entitlement in subaccount ## Transformation Steps ### Step 1: Remove Resource References from web.xml **Remove these from web.xml:** ```xml <resource-ref> <res-ref-name>KeyStoreService</res-ref-name> <res-type>com.sap.cloud.crypto.keystore.api.KeyStoreService</res-type> </resource-ref> <resource-ref> <res-ref-name>PasswordStorage</res-ref-name> <res-type>com.sap.cloud.security.password.PasswordStorage</res-type> </resource-ref> ``` ### Step 2: Copy Client Classes Copy the following helper classes to your project from [assets/](assets/). All assets declare `package com.example.document;`, so place them flat under `src/main/java/com/example/document/`: > **Note on the package**: `com.example.document` is a placeholder. Rename to your project's package > (e.g. `com.acme.security`) when you copy the assets in, and update the imports below to match. | File | |------| | `ServiceCredentials.java` | | `ServiceCredentialsAccessor.java` | | `CertificateParser.java` | | `KeyParser.java` | | `SSLContextProvider.java` | | `CredStoreResponse.java` | | `CredStoreRequestBuilder.java` | | `CredStoreClient.java` | These classes handle: - Reading service credentials from VCAP_SERVICES via `DefaultServiceBindingAccessor` - Parsing PEM certificates (X.509) and PKCS#1 private keys (BouncyCastle PEMParser) - Creating an `SSLContext` for mTLS (PKCS12 keystore in memory) - REST API calls to Credential Store using `java.net.http.HttpClient` The classes have no Lombok dependency — getters are written by hand so they build cleanly under JDK 25. #### Required Maven dependencies Add these to your application's `pom.xml`. The `cf-tomcat-bom` + `sdk-modules-bom` BOMs already manage versions for `scp-cf` (which transitively brings in `service-binding-api`, BouncyCastle, Apache HttpClient, Jackson, and SLF4J), so you only need the `scp-cf` dependency itself plus `jakarta.servlet-api` for the servlet: ```xml <properties> <!-- sap_java_buildpack_jakarta supports SapMachine 17, 21, and 25. Always pin the runtime JRE explicitly via JBP_CONFIG_SAP_MACHINE_JRE in mtad.yaml / manifest.yml, and make sure this compile target matches that major version — a higher class file version yields java.lang.UnsupportedClassVersionError at servlet load time (HTTP 500). --> <maven.compiler.source>25</maven.compiler.source> <maven.compiler.target>25</maven.compiler.target> <!-- Resolve via sdk-replacement Step 3. --> <cf-tomcat-bom-version>RESOLVED_CF_TOMCAT_BOM_VERSION</cf-tomcat-bom-version> <sdk-modules-bom-version>RESOLVED_SDK_MODULES_BOM_VERSION</sdk-modules-bom-version> <jakarta.servlet-api.version>6.1.0</jakarta.servlet-api.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>com.sap.cloud.sjb.cf</groupId> <artifactId>cf-tomcat-bom</artifactId> <version>${cf-tomcat-bom-version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.sap.cloud.sdk</groupId> <artifactId>sdk-modules-bom</artifactId> <version>${sdk-modules-bom-version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>com.sap.cloud.sdk.cloudplatform</groupId> <artifactId>scp-cf</artifactId> </dependency> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>${jakarta.servlet-api.version}</version> <scope>provided</scope> </dependency> </dependencies> ``` > **Pin the runtime JRE explicitly.** `sap_java_buildpack_jakarta` supports SapMachine > 17, 21, and 25. The buildpack's implicit JRE choice changes over time, so don't rely on > it — always pin the version in `mtad.yaml` (or `manifest.yml`) and keep the compile > target in `pom.xml` aligned with it. A class file newer than the runtime JRE causes > Tomcat to fail loading the servlet with > `java.lang.UnsupportedClassVersionError: ... class file version N.0, this version of the > Java Runtime only recognizes class file versions up to M.0` and every request returns > HTTP 500. > > For Java 25 (the recommended target for new migrations) add this to your module > `properties:` block in `mtad.yaml`: > > ```yaml > JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jre.SAPMachineJRE']" > JBP_CONFIG_SAP_MACHINE_JRE: '{ version: 25.+ }' > ``` > > Same form for any other supported major version — substitute `17.+` or `21.+`. > Build locally with a matching JDK (e.g. SapMachine 25 from https://sapmachine.io/). > The Lombok-free helper classes in `assets/` compile cleanly on any supported version. ### Step 3: Update Application Code > **The HTTP URL contract changes — do NOT preserve the old Neo query parameters.** > > The Neo `KeyStoreServlet` was driven by params shaped around its JNDI API: > `?method=getKeyStore&keyStoreName=<file>&password=<password>`. None of those map > onto SAP Credential Store, which is namespace-scoped and addresses each credential > by alias. The migrated servlet **MUST** accept the new contract: > > | URL | Behaviour | > |-----|-----------| > | `GET /keystore?namespace=<ns>` | List all credentials in `<ns>` (calls `CredStoreClient.retrieveCredentials(ns)`) | > | `GET /keystore?namespace=<ns>&alias=<name>` | Retrieve a single credential by alias (calls `CredStoreClient.retrieveCredential(alias, ns)`) | > | `GET /keystore` (no `namespace`) | Reject with `400 Bad Request`, body `Namespace is required!` | > > A Neo password-storage app likewise drops to: `GET /?namespace=<ns>&alias=<name>` > calling `CredStoreClient.retrievePassword(alias, ns)`. > > Why this matters: do **NOT** carry over the `method=getKeyStore` / > `keyStoreName=` / `password=` validation block from the original Neo servlet. > Doing so makes the migrated app keep returning `400` for the actual contract a > credstore-aware caller will use, and silently breaks integration tests. Replace > the old guard with the namespace check shown below. **Before (Neo `KeyStoreServlet` — JNDI lookup, `method=…&keyStoreName=…&password=…`):** ```java import com.sap.cloud.crypto.keystore.api.KeyStoreService; import java.security.KeyStore; import javax.naming.InitialContext; import javax.servlet.http.*; public class KeyStoreServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String method = request.getParameter("method"); if (method == null || !"getKeyStore".equals(method)) { // Neo-shaped guard. DELETE THIS BLOCK in the migrated version — // the new contract does not use a "method" parameter. response.sendError(HttpServletResponse.SC_BAD_REQUEST, "method invalid"); return; } KeyStoreService svc = (KeyStoreService) new InitialContext().lookup("java:comp/env/KeyStoreService"); KeyStore keyStore = svc.getKeyStore( request.getParameter("keyStoreName"), request.getParameter("password").toCharArray()); // ... iterate keyStore.aliases() ... } } ``` **After (Cloud Foundry — `?namespace=<ns>[&alias=<name>]`):** ```java package com.example.document; // CredStoreClient and CredStoreResponse live in the same package — no import needed. import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.security.GeneralSecurityException; public class KeyStoreServlet extends HttpServlet { private static final String PARAM_ALIAS = "alias"; private static final String PARAM_NAMESPACE = "namespace"; private final CredStoreClient credStoreClient; public KeyStoreServlet() throws GeneralSecurityException, IOException { this.credStoreClient = new CredStoreClient(); } @Override public void destroy() { // Release the mTLS private key when the servlet is taken out of service. // CredStoreClient holds the key for its whole lifetime (the SSLContext // reuses it across requests), so it must only be destroyed here — never // per request — otherwise subsequent mTLS handshakes fail. credStoreClient.close(); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { String alias = request.getParameter(PARAM_ALIAS); String namespace = request.getParameter(PARAM_NAMESPACE); if (isBlank(namespace)) { sendError(response, "Namespace is required!", HttpServletResponse.SC_BAD_REQUEST); return; } CredStoreResponse credstoreResponse = isBlank(alias) ? credStoreClient.retrieveCredentials(namespace) // list all keys : credStoreClient.retrieveCredential(alias, namespace); // single key if (credstoreResponse.isSuccess()) { sendOk(response, isBlank(alias) ? "Successfully retrieved credentials." : "Successfully retrieved credential."); } else { sendError(response, credstoreResponse.getMessage(), HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } private static boolean isBlank(String s) { return s == null || s.isBlank(); } private static void sendOk(HttpServletResponse response, String body) throws IOException { try (PrintWriter out = response.getWriter()) { out.println(body); } } private static void sendError(HttpServletResponse response, String body, int status) throws IOException { response.setStatus(status); try (PrintWriter out = response.getWriter()) { out.println(body); } } } ``` The corresponding `web.xml` must drop the `<resource-ref>` (Step 1) AND map the servlet at `/keystore` (or `/` for password apps) so the URL above resolves. Any Neo `ErrorUtil` helper that imported `javax.servlet.*` must be ported to `jakarta.servlet.*` or inlined as private helpers like above. **Before (Neo Password Storage — JNDI lookup, no namespace param):** ```java import com.sap.cloud.security.password.PasswordStorage; @Resource(name = "PasswordStorage") private PasswordStorage passwordStorage; public void usePassword(HttpServletRequest request) throws Exception { String alias = request.getParameter("alias"); // alias only char[] password = passwordStorage.getPassword(alias); // ... use password ... } ``` **After (Cloud Foundry — `?namespace=<ns>&alias=<name>`):** ```java @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { String alias = request.getParameter("alias"); String namespace = request.getParameter("namespace"); // NEW required param if (isBlank(alias) || isBlank(namespace)) { response.getWriter().println("Alias and namespace must be provided as query parameters."); return; } CredStoreResponse r = credStoreClient.retrievePassword(alias, namespace); response.getWriter().println(r.isSuccess() ? "Password retrieved successfully." : "Failed to retrieve password."); } ``` ### Step 4: Update MTA Descriptor > 🛑 **HARD RULE — the credstore resource MUST be `org.cloudfoundry.existing-service`.** > Do **not** emit `type: org.cloudfoundry.managed-service` for credstore, and do > **not** add a `parameters.service: credstore` / `service-plan:` block. Getting > this wrong is the single most common failure of this migration: > > | If you emit… | What happens at `cf deploy` | > |---|---| > | `managed-service` (`service: credstore`) | Broker rejects it — `Service broker credstore failed with: Illegal parameters or arguments used` — because the mTLS `config` is set out-of-band, not in the MTA. Deploy fails. | > | `existing-service` ✅ | Binds to the operator-provisioned instance. Correct. | > > The credstore instance is created and mTLS-configured **outside** the MTA > (by the space operator, or by CI before deploy). The MTA only *binds* to it. ```yaml modules: - name: ${app-name} type: java.tomcat path: target/<artifactId>.war parameters:
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub