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:

ScopeRole-template scopes
ReadCardanoReader (Read only)
TransactCardanoUser (Read + Transact + Sign)
SignCardanoUser

(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 @restrict annotations 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.json via csrfProtection: true. Clients fetch with X-CSRF-Token: Fetch, then send the token on POST/PUT/DELETE.
  • CORS: restrict allowedOrigins in production.
  • Headers: set CSP, X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Strict-Transport-Security: max-age=31536000.

Secrets

VariableSensitivity
BLOCKFROST_API_KEYHigh
KOIOS_API_KEYMedium
OGMIOS_URLMedium
HSM_PINCritical

.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

External signing architecture

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

EntityRecords
TransactionBuildsEvery build request
SigningRequestsSigning lifecycle (pendingverifiedsubmitted)
SignatureVerificationsEvery verification attempt (isValid, witnessCount, errorMessage)
TransactionSubmissionsEvery 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'

See Also