Table of Contents

  1. Security Architecture
  2. Authentication (XSUAA)
  3. Authorization & Roles
  4. API & Transport Security
  5. Secret Management
  6. Transaction Signing Security
  7. Input Validation
  8. Audit Trail
  9. Production Security Checklist

Security Architecture

Design Principles

  • Private key isolation — The server NEVER handles, stores, or has access to private keys. All signing is external.
  • Defense in depth — Multiple independent layers: authentication, authorization, input validation, transport encryption, audit logging.
  • Least privilege — Role-based access restricts operations to authorized scopes.
  • Fail-secure — Invalid/expired requests and missing auth result in explicit rejection.

Security Layers

Layer 1: Transport Security (TLS/HTTPS)
Layer 2: SAP AppRouter (Reverse Proxy + CSRF)
Layer 3: XSUAA Authentication (JWT Validation)
Layer 4: CDS Authorization (@requires + Scopes)
Layer 5: Input Validation (validators.ts + const.ts limits)
Layer 6: Transaction Integrity (Signature Verification + Body Hash Matching)
Layer 7: Audit Trail (SigningRequests + SignatureVerifications + TransactionSubmissions)

Service Separation

ServicePurposeRisk Profile
CardanoODataServiceRead-only blockchain queriesLow — no state mutation
CardanoTransactionServiceTransaction building, signing, submissionMedium — involves funds movement preparation
CardanoSigningServiceTransaction signingHigh — involves private key operations

This enables independent security policies per service.


Authentication (XSUAA)

ODATANO uses SAP BTP’s XSUAA for production authentication. In development, auth is disabled for local testing.

Profile Configuration

{
  "cds": {
    "requires": {
      "[production]": {
        "db": { "kind": "hana" },
        "auth": "xsuaa"
      }
    }
  }
}
  • Development (cds watch): No authentication, all endpoints open.
  • Production ([production] profile): XSUAA enforced, every request needs a valid JWT.

xs-security.json

{
  "xsappname": "odatano",
  "tenant-mode": "dedicated",
  "scopes": [
    { "name": "$XSAPPNAME.BlockchainRead", "description": "Read blockchain data" },
    { "name": "$XSAPPNAME.TransactionBuild", "description": "Build unsigned transactions" },
    { "name": "$XSAPPNAME.TransactionSubmit", "description": "Submit signed transactions" },
    { "name": "$XSAPPNAME.SigningManage", "description": "Create/manage signing requests" },
    { "name": "$XSAPPNAME.Admin", "description": "Full administrative access" }
  ],
  "role-templates": [
    {
      "name": "BlockchainViewer",
      "description": "Read-only access",
      "scope-references": ["$XSAPPNAME.BlockchainRead"]
    },
    {
      "name": "TransactionOperator",
      "description": "Build and submit transactions",
      "scope-references": [
        "$XSAPPNAME.BlockchainRead", "$XSAPPNAME.TransactionBuild",
        "$XSAPPNAME.TransactionSubmit", "$XSAPPNAME.SigningManage"
      ]
    },
    {
      "name": "Administrator",
      "description": "Full access",
      "scope-references": [
        "$XSAPPNAME.BlockchainRead", "$XSAPPNAME.TransactionBuild",
        "$XSAPPNAME.TransactionSubmit", "$XSAPPNAME.SigningManage", "$XSAPPNAME.Admin"
      ]
    }
  ]
}

Setup on BTP

cf create-service xsuaa application odatano-uaa -c xs-security.json
cf bind-service odatano odatano-uaa

Authorization & Roles

SAP CAP provides declarative authorization via @requires annotations in CDS.

CDS Annotations

// Read-only service — single scope
service CardanoODataService @(requires: 'BlockchainRead') { ... }

// Transaction service — fine-grained per action
service CardanoTransactionService {
    action BuildSimpleAdaTransaction(...)      @(requires: 'TransactionBuild');
    action SubmitTransaction(...)              @(requires: 'TransactionSubmit');
    action CreateSigningRequest(...)           @(requires: 'SigningManage');
    action VerifySignature(...)                @(requires: 'SigningManage');
}

BTP Role Collections

Role CollectionRole TemplateIntended Users
ODATANO ViewerBlockchainViewerAnalysts, dashboards, monitoring
ODATANO OperatorTransactionOperatorApplication backends, dApp frontends
ODATANO AdminAdministratorPlatform administrators

Create role collections in the SAP BTP Cockpit and assign them to users.


API & Transport Security

TLS/HTTPS

All production traffic must use TLS. SAP BTP provides automatic TLS termination. For Docker, configure a reverse proxy (NGINX, HAProxy) for TLS.

AppRouter as Reverse Proxy

Internet --> AppRouter (auth + CSRF) --> CAP Server (internal only)

The AppRouter provides TLS termination, JWT validation, CSRF protection, and session management. The CAP server should NOT be directly internet-accessible.

CSRF Protection

Enabled in xs-app.json routes via "csrfProtection": true. Clients must:

  1. Fetch token via GET with X-CSRF-Token: Fetch header
  2. Include token in X-CSRF-Token header on state-changing requests (POST/PUT/DELETE)

CORS

Restrict allowed origins in production (no wildcards):

{
  "cors": {
    "allowedOrigins": [{ "host": "your-domain.com" }],
    "allowedMethods": ["GET", "POST", "OPTIONS"],
    "allowedHeaders": ["Authorization", "Content-Type", "X-CSRF-Token"]
  }
}
HeaderValue
Content-Security-Policydefault-src 'self'; script-src 'self' 'unsafe-inline'; ...
X-Content-Type-Optionsnosniff
X-Frame-OptionsSAMEORIGIN
Strict-Transport-Securitymax-age=31536000; includeSubDomains

Secret Management

Sensitive Variables

VariableSensitivityNotes
BLOCKFROST_API_KEYHighGrants API access, counts toward quota
KOIOS_API_KEYMediumFree tier available without key too
OGMIOS_URLMediumExposes local node endpoint
HSM_PINCriticalPKCS#11 authentication

Best Practices

  • Never commit secrets to version control
  • .gitignore must include: .env, *.skey, *.vkey, *.pem, credentials*.json
  • Local dev: Use .env files
  • BTP Production: Use SAP Credential Store or User-Provided Services:
cf create-user-provided-service odatano-secrets -p '{
  "BLOCKFROST_API_KEY": "your-production-key",
  "KOIOS_API_KEY": "your-koios-key"
}'
cf bind-service odatano odatano-secrets

API Key Rotation

Rotate keys every 90 days. With multi-backend failover, rotate without downtime:

  1. Rotate Blockfrost key → Koios handles requests during restart
  2. Rotate Koios key → Blockfrost handles requests during restart

Transaction Signing Security

This is the most critical security boundary. Private keys NEVER touch the server.

External Signing Architecture

External Signing Architecture

The server only sees: unsigned CBOR, signed CBOR (public witnesses, NOT keys), and transaction body hashes.

Signing Methods

MethodKey LocationSecurity LevelUse Case
CIP-30 Browser WalletBrowser extensionVery HighWeb dApps, Fiori apps
Cardano CLIFile system (.skey)HighBackend automation
Hardware WalletDedicated deviceMaximumHigh-value transactions
HSM (PKCS#11)HSM chipMaximumEnterprise server-side automation

For signing code examples, see Transaction Workflow.

HSM (PKCS#11) Integration

ODATANO supports server-side signing via PKCS#11-compatible HSMs.

Configuration:

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

Security properties: Key generated inside HSM with CKA_EXTRACTABLE=false, signing via CKM_EDDSA (Ed25519), PIN-based session auth. HSM failure is non-fatal — app starts without HSM, other signing methods still work.

Verify HSM status:

curl -X POST http://localhost:4004/odata/v4/cardano-sign/GetHsmStatus \
  -H "Content-Type: application/json" -d '{}'

Verification Checks

  1. CBOR parsing — Valid, parseable CBOR
  2. Body hash matching — Detects tampering between build and sign
  3. Witness presence — At least one VKey witness present
  4. Required signers — Specific key hashes present if required
  5. Ed25519 verification — Cryptographic signature check per witness

Signing Request TTL

Signing requests expire after 30 minutes (default). Expired requests are rejected even with valid signatures, preventing replay attacks with stale UTxO data.


Input Validation

All inputs are validated before processing via srv/utils/validators.ts with limits from srv/utils/const.ts.

Validators

ValidatorFormatPurpose
isTxHash(s)64 hex charsTransaction/block hashes
isValidBech32Address(s)Bech32 addr/addr_testPayment addresses (network-aware)
isValidBech32StakeAddress(s)Bech32 stake/stake_testStake addresses
isValidPoolId(s)Bech32 pool, 28-byte payloadStake pool IDs
isValidDrepId(s)Bech32 drep, 29-byte payloadDRep IDs
isAssetUnit(s)56-192 hex charsNative asset identifiers
isValidCbor(s)Even-length hexTransaction CBOR
validateTransactionInputs()CompositeMulti-field validation
validateJsonWithLimits()JSON + complexityDoS prevention

JSON Complexity Limits (DoS Prevention)

LimitValuePurpose
Max JSON size1 MBPrevents memory exhaustion
Max nesting depth10Prevents stack overflow
Max object keys100Prevents hash collision attacks
Max array length1,000Prevents excessive iteration
Max string length65,536Prevents oversized values

Network-Aware Address Validation

Addresses are validated against the configured NETWORK. A preview address (addr_test1...) is rejected on mainnet, and vice versa.


Audit Trail

ODATANO records every step of the transaction lifecycle via OData-queryable entities.

Tracked Entities

EntityRecordsKey Fields
TransactionBuildsEvery build requestsenderAddress, fee, unsignedTxCbor, txBodyHash
SigningRequestsSigning lifecyclestatus (pendingverifiedsubmitted), signerType, hsmKeyId, expiresAt
SignatureVerificationsEvery verification attemptisValid, witnessCount, signerKeyHashes, errorMessage
TransactionSubmissionsEvery submission attempttxHash, status, errorCode, retryCount

Querying

# Signing requests for an address
POST /odata/v4/cardano-transaction/GetSigningRequestsByAddress
{"address": "addr_test1qq..."}

# Filtered queries with OData
GET /odata/v4/cardano-transaction/SigningRequests
    ?$filter=status eq 'verified'&$orderby=createdAt desc&$expand=verifications,build

# Failed submissions
GET /odata/v4/cardano-transaction/TransactionSubmissions
    ?$filter=status eq 'failed'

References