Security
Principles
- Private key isolation - server never handles, stores, or sees private keys.
- Defense in depth - TLS / XSUAA / input validation / signature verification / audit trail.
- Fail-secure - invalid, expired, or unauthenticated requests are rejected.
Authentication (XSUAA)
ODATANO uses SAP BTP XSUAA in production. Dev mode (cds watch) leaves auth open.
{ "cds": { "requires": { "[production]": { "auth": "xsuaa", "db": { "kind": "hana" } } } } }
xs-security.json defines 3 scopes and 2 role-templates:
| Scope | Role-template scopes |
|---|---|
Read | CardanoReader (Read only) |
Transact | CardanoUser (Read + Transact + Sign) |
Sign | CardanoUser |
(mtcallback scope is reserved for SaaS Registry tenant onboarding.)
cf create-service xsuaa application odatano-uaa -c xs-security.json
cf bind-service odatano odatano-uaa
Authorization
All three services declare @requires: 'authenticated-user' at service level only any authenticated user can call any action. The XSUAA scopes are exposed for consumers but not enforced per-action by ODATANO. This is intentional:
Unsigned transaction CBOR is not exploitable without the corresponding private key. Cross-address operations (script redemptions, multi-party Plutus, dApp backends) are legitimate. Consumers needing per-resource ownership should add their own
@restrictannotations or middleware.
Optional ownership check
Four signing actions accept an optional address parameter when supplied, the server verifies the build / signing request belongs to that address: VerifySignature, SubmitVerifiedTransaction, SignWithHsm, SignAndSubmitWithHsm. Pass the wallet address from your JWT claims to enforce ownership without full per-action authorization.
Adding per-action @requires in your consumer
using { CardanoTransactionService } from '@odatano/core';
annotate CardanoTransactionService with @(restrict: [
{ grant: 'READ', to: ['Read', 'Transact'] },
{ grant: 'action', to: ['Transact'], where: 'BuildSimpleAdaTransaction' },
{ grant: 'action', to: ['Sign'], where: 'SubmitTransaction' }
]);
annotate CardanoSignService with @(requires: 'Sign');
Transport
- TLS: required in production. SAP BTP terminates TLS automatically; for Docker use a reverse proxy.
- AppRouter: sits in front of the CAP backend; provides JWT validation, CSRF, session management.
- CSRF: enabled in
xs-app.jsonviacsrfProtection: true. Clients fetch withX-CSRF-Token: Fetch, then send the token on POST/PUT/DELETE. - CORS: restrict
allowedOriginsin production. - Headers: set
CSP,X-Content-Type-Options: nosniff,X-Frame-Options: SAMEORIGIN,Strict-Transport-Security: max-age=31536000.
Secrets
| Variable | Sensitivity |
|---|---|
BLOCKFROST_API_KEY | High |
KOIOS_API_KEY | Medium |
OGMIOS_URL | Medium |
HSM_PIN | Critical |
.gitignore must include: .env, *.skey, *.vkey, *.pem, credentials*.json. Production: use SAP Credential Store or a User-Provided Service. Rotate keys every 90 days; multi-backend failover absorbs rotation downtime.
Transaction Signing
Server only sees: unsigned CBOR · signed CBOR (public witnesses, no keys) · transaction body hash.
HSM (PKCS#11)
HSM_ENABLED=true
HSM_PKCS11_MODULE=/usr/lib/pkcs11/yubihsm_pkcs11.so
HSM_SLOT=0
HSM_PIN=0001password # use credential store in production
HSM_KEY_LABEL=cardano-signing-key
HSM_KEY_ID=0x0001
HSM_REQUIRES_ROLE=Sign # optional; rejects requests without this role/scope
Key generated inside the HSM with CKA_EXTRACTABLE=false, signed via CKM_EDDSA (Ed25519). HSM init failure is non-fatal, the server starts, HSM actions return ODATANO_HSM_UNAVAILABLE. HSM_REQUIRES_ROLE is enforced at action invocation time.
Verification checks
CBOR parses · body hash matches the build · ≥ 1 VKey witness present · all required signers present · per-witness Ed25519 verification.
Signing requests expire after 30 min (default). Expired requests are rejected even with valid signatures.
Input Validation
JSON DoS limits (srv/utils/const.ts): max 1 MB · max nesting depth 10 · max 100 object keys · max array length 1 000 · max string 65 536.
Audit Trail
| Entity | Records |
|---|---|
TransactionBuilds | Every build request |
SigningRequests | Signing lifecycle (pending → verified → submitted) |
SignatureVerifications | Every verification attempt (isValid, witnessCount, errorMessage) |
TransactionSubmissions | Every submission attempt (txHash, status, errorCode, retryCount) |
POST /odata/v4/cardano-sign/GetSigningRequestsByAddress {"address": "addr_test1..."}
GET /odata/v4/cardano-sign/SigningRequests
?$filter=status eq 'verified'&$expand=verifications,build
GET /odata/v4/cardano-transaction/TransactionSubmissions?$filter=status eq 'failed'