diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1565996 --- /dev/null +++ b/.env.example @@ -0,0 +1,53 @@ +# ----------------------------------------------------------------------------- +# Approov Node.js Quickstart - Environment Configuration +# +# This file is a template for local development. Copy it to `.env` and replace +# the placeholder values with real secrets or environment-specific settings. +# The server loads `.env` on startup and uses these variables to configure: +# - which address/port to bind +# - how to verify Approov tokens +# - optional message signing validation +# - optional verbose HTTP logging for debugging +# ----------------------------------------------------------------------------- + +# HTTP port the backend listens on (internal server port) +HTTP_PORT=8111 + +# Approov shared secret (base64url). Required to verify Approov JWTs. +# Obtain with: `approov secret -get base64url` +APPROOV_BASE64URL_SECRET=approov_base64url_secret_here + +# Optional: account-level message signing secret. +# Use ONE of the formats below. If you set more than one, the first non-empty +# value (in the order shown) is used. +APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64URL= +APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64= +APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_RAW= + +# Optional: expected account key id for message signing (mskid claim). +# If set, incoming requests must match this key id when using account signatures. +APPROOV_ACCOUNT_MESSAGE_SIGNING_KEY_ID= + +# Signature mode toggle: +# - false (default): install signature only +# - true: account signature only (install signature disabled) +APPROOV_ENABLE_ACCOUNT_SIGNATURE=false + +# Optional: allowed clock skew (seconds) when validating message signature +# created/expires parameters. Increase slightly if clients' clocks drift. +APPROOV_MESSAGE_SIGNING_TOLERANCE_SECONDS=60 + +# Verbose logging for Approov + message signing flow (true/false). +# Use `true` when debugging token/signature issues. +APPROOV_VERBOSE_LOGGING=true + +# Log full HTTP request/response payloads (true/false). +# Use with caution: this can log sensitive data. +APPROOV_HTTP_LOGGING=false + +# Address/hostname to bind the HTTP server to. +# Use 0.0.0.0 when running in Docker so the port is reachable externally. +SERVER_HOSTNAME=0.0.0.0 + +# Command that starts your server inside the container (Docker). +APP_START_CMD=npm start diff --git a/.gitignore b/.gitignore index 2d7ec5c..a4e399d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ .env node_modules/ +!vendor/http-message-signatures/node_modules/ +!vendor/http-message-signatures/node_modules/structured-headers/ +!vendor/http-message-signatures/node_modules/structured-headers/** +.config/ +.DS_Store diff --git a/ApproovApplication.js b/ApproovApplication.js new file mode 100644 index 0000000..7513696 --- /dev/null +++ b/ApproovApplication.js @@ -0,0 +1,1366 @@ +'use strict'; + +const http = require('http'); +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const { httpbis, createVerifier } = require('./vendor/http-message-signatures/lib'); +const structuredHeaders = require('./vendor/http-message-signatures/node_modules/structured-headers'); + +loadEnvFile(path.join(__dirname, '.env')); + +const SERVER_HOSTNAME = process.env.SERVER_HOSTNAME || 'localhost'; +const HTTP_PORT = parsePort(process.env.HTTP_PORT, 8111); + +const APPROOV_SECRET_BASE64URL = + process.env.APPROOV_BASE64URL_SECRET || process.env.APPROOV_BASE64_SECRET; + +if (!hasText(APPROOV_SECRET_BASE64URL)) { + console.error('APPROOV_BASE64URL_SECRET environment variable is not set'); + process.exit(1); +} + +const APPROOV_SECRET = base64UrlDecodeToBuffer(APPROOV_SECRET_BASE64URL.trim()); + +const APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64URL = + process.env.APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64URL || + process.env.APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET; +const APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64 = + process.env.APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64; +const APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_RAW = + process.env.APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_RAW; +const APPROOV_ACCOUNT_MESSAGE_SIGNING_KEY_ID = + process.env.APPROOV_ACCOUNT_MESSAGE_SIGNING_KEY_ID; + +const MESSAGE_SIGNING_TOLERANCE_SECONDS = parsePositiveInt( + process.env.APPROOV_MESSAGE_SIGNING_TOLERANCE_SECONDS, + 60 +); +const ACCOUNT_SIGNATURE_MODE_ENABLED = parseBoolean( + process.env.APPROOV_ENABLE_ACCOUNT_SIGNATURE ?? + process.env.APPROOV_ACCOUNT_SIGNATURE_ENABLED, + false +); +const MESSAGE_SIGNATURE_MODE = ACCOUNT_SIGNATURE_MODE_ENABLED + ? 'account' + : 'install'; + +const INSTALL_MESSAGE_SIGNING_ALGORITHM = 'ecdsa-p256-sha256'; +const ACCOUNT_MESSAGE_SIGNING_ALGORITHM = 'hmac-sha256'; + +const MESSAGE_SIGNING_REQUIRED_PARAMS = Object.freeze([ + 'alg', + 'created', + 'expires', +]); + +const VERBOSE_LOGGING = parseBoolean(process.env.APPROOV_VERBOSE_LOGGING, true); +const HTTP_LOGGING_ENABLED = parseBoolean( + process.env.APPROOV_HTTP_LOGGING, + false +); + +const APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET = loadAccountMessageSigningSecret(); + +let approovEnabled = true; +let tokenBindingEnabled = true; + +const HEADER_NAMES = Object.freeze({ + APPROOV_TOKEN: 'approov-token', + AUTHORIZATION: 'authorization', + CONTENT_DIGEST: 'content-digest', + SESSION_ID: 'session-id', + SIGNATURE: 'signature', + SIGNATURE_INPUT: 'signature-input', +}); + +const ROUTES = Object.freeze([ + { method: 'GET', path: '/', handler: homeHandler, requiresApproov: false }, + { + method: 'GET', + path: '/approov-state', + handler: approovStateHandler, + requiresApproov: false, + }, + { + method: 'POST', + path: '/approov/enable', + handler: enableApproovHandler, + requiresApproov: false, + }, + { + method: 'POST', + path: '/approov/disable', + handler: disableApproovHandler, + requiresApproov: false, + }, + { + method: 'POST', + path: '/token-binding/enable', + handler: enableTokenBindingHandler, + requiresApproov: false, + }, + { + method: 'POST', + path: '/token-binding/disable', + handler: disableTokenBindingHandler, + requiresApproov: false, + }, + { + method: 'GET', + path: '/unprotected', + handler: unprotectedHandler, + requiresApproov: false, + }, + { + method: 'GET', + path: '/token-check', + handler: tokenCheckHandler, + requiresApproov: true, + }, + { + method: 'GET', + path: '/token-check-signature', + handler: tokenCheckSignatureHandler, + requiresApproov: true, + requiresMessageSignature: true, + }, + { + method: 'POST', + path: '/token-check-signature', + handler: tokenCheckSignatureHandler, + requiresApproov: true, + requiresMessageSignature: true, + }, + { + method: 'GET', + path: '/token-binding', + handler: tokenBindingHandler, + requiresApproov: true, + // Token binding can cover multiple headers. The bindingHeaders list is + // concatenated (in order), hashed, and compared to the Approov token pay claim. + bindingHeaders: [HEADER_NAMES.AUTHORIZATION], + }, + { + method: 'GET', + path: '/token-double-binding', + handler: tokenDoubleBindingHandler, + requiresApproov: true, + // Example of multi-header binding: Authorization + Session-Id. + // Keep headers stable across requests; avoid Content-Digest because the body + // can legitimately change and would invalidate the binding. + bindingHeaders: [HEADER_NAMES.AUTHORIZATION, HEADER_NAMES.SESSION_ID], + }, +]); + +const ROUTE_TABLE = new Map( + ROUTES.map((route) => [`${route.method} ${route.path}`, route]) +); + +const MIDDLEWARE = Object.freeze([approovTokenVerifier]); + +const server = http.createServer((req, res) => { + const requestInfo = parseRequest(req); + const route = ROUTE_TABLE.get(`${requestInfo.method} ${requestInfo.path}`); + + if (!route) { + writeJson(res, 404, { error: 'not_found' }); + return; + } + + const ctx = { + req, + res, + route, + method: requestInfo.method, + path: requestInfo.path, + headers: req.headers, + state: {}, + }; + + if (HTTP_LOGGING_ENABLED) { + enableHttpLogging(ctx); + readRequestBody(ctx).catch((error) => { + logVerbose(ctx, 'http', 'body', `Failed to read body: ${error.message}`); + }); + } + + runMiddleware(ctx, MIDDLEWARE, route.handler).catch((error) => { + console.error('Unhandled error:', error); + if (!res.writableEnded) { + writeJson(res, 500, { error: 'server_error' }); + } + }); +}); + +server.listen(HTTP_PORT, SERVER_HOSTNAME, () => { + console.log( + `Approov demo API running at http://${SERVER_HOSTNAME}:${HTTP_PORT}` + ); +}); + +/** + * Handles the unauthenticated root endpoint used as a simple health check. + * We keep this route outside Approov enforcement so operators can verify the + * server is reachable without a valid Approov token or message signature. + * The response mirrors the current port to make it clear which instance replied. + */ +function homeHandler(ctx) { + writeJson( + ctx.res, + 200, + infoPayload(`Approov demo API is running on port ${HTTP_PORT}.`) + ); +} + +/** + * Returns the current in-memory enforcement toggles. + * This is intended for demos/tests to confirm whether Approov checks and + * token binding are enabled before running scripted requests. + */ +function approovStateHandler(ctx) { + writeJson(ctx.res, 200, statePayload()); +} + +/** + * Enables Approov token verification and token binding in one operation. + * Approov is the primary access gate, so we also re-enable binding here to + * keep the demo in a consistent "fully protected" state. + */ +function enableApproovHandler(ctx) { + approovEnabled = true; + tokenBindingEnabled = true; + writeJson(ctx.res, 200, statePayload()); +} + +/** + * Disables Approov token verification and binding. + * This is a demo/testing switch only; in production you would not expose + * an endpoint that bypasses token and binding checks. + */ +function disableApproovHandler(ctx) { + approovEnabled = false; + tokenBindingEnabled = false; + writeJson(ctx.res, 200, statePayload()); +} + +/** + * Enables token binding checks while leaving Approov token verification on. + * Token binding proves possession of a client-bound value (for example an + * Authorization header) that was hashed into the Approov token pay claim. + */ +function enableTokenBindingHandler(ctx) { + tokenBindingEnabled = true; + writeJson(ctx.res, 200, statePayload()); +} + +/** + * Disables token binding checks while keeping Approov token verification on. + * This is useful for testing that plain token validation still succeeds even + * when the bound header is missing or intentionally mismatched. + */ +function disableTokenBindingHandler(ctx) { + tokenBindingEnabled = false; + writeJson(ctx.res, 200, statePayload()); +} + +/** + * Demonstrates an endpoint that does not require Approov. + * This is useful to show the contrast between protected and unprotected routes + * when validating client integrations and automated tests. + */ +function unprotectedHandler(ctx) { + writeJson( + ctx.res, + 200, + infoPayload("Unprotected endpoint '/unprotected'; no Approov checks performed.") + ); +} + +/** + * Basic Approov-protected endpoint. + * The Approov middleware has already validated the JWT signature and exp claim + * before this handler is invoked, so we simply return a success payload. + */ +function tokenCheckHandler(ctx) { + writeJson( + ctx.res, + 200, + infoPayload("Protected endpoint '/token-check'; Approov token verified.") + ); +} + +/** + * Approov-protected endpoint that also requires HTTP Message Signatures. + * This is the strictest path in the demo: it validates the Approov token and + * then enforces a request signature bound to the Approov message-signing claims. + */ +function tokenCheckSignatureHandler(ctx) { + writeJson( + ctx.res, + 200, + infoPayload( + "Protected endpoint '/token-check-signature'; Approov token and message signature verified." + ) + ); +} + +/** + * Approov-protected endpoint that enforces token binding against Authorization. + * We echo whether the Authorization header was present to make debugging + * token binding and binding header selection easier during integration. + */ +function tokenBindingHandler(ctx) { + const authorization = headerValue(ctx.headers, HEADER_NAMES.AUTHORIZATION); + const response = infoPayload( + "Protected endpoint '/token-binding'; Approov token binding enforced." + ); + response.authorizationHeaderPresent = hasText(authorization); + writeJson(ctx.res, 200, response); +} + +/** + * Approov-protected endpoint that enforces a composite binding. + * The binding material is the Authorization header plus a stable Session-Id + * header, which avoids coupling the binding to a mutable request body. + */ +function tokenDoubleBindingHandler(ctx) { + const authorization = headerValue(ctx.headers, HEADER_NAMES.AUTHORIZATION); + const sessionId = headerValue(ctx.headers, HEADER_NAMES.SESSION_ID); + const response = infoPayload( + "Protected endpoint '/token-double-binding'; dual token binding enforced." + ); + response.authorizationHeaderPresent = hasText(authorization); + response.sessionIdHeaderPresent = hasText(sessionId); + writeJson(ctx.res, 200, response); +} + +/** + * Middleware that enforces Approov token verification, optional token binding, + * and optional HTTP Message Signatures depending on the current route. + * Approov tokens are JWTs signed with a shared secret (HS256) and include + * claims such as exp (expiry), ipk (install public key), and mskid (account key id). + * Successful validation attaches the decoded claims to ctx.state for handlers. + */ +async function approovTokenVerifier(ctx, next) { + const route = ctx.route; + if (!route || !route.requiresApproov) { + logVerbose(ctx, 'approov', 'skip', 'Route does not require Approov.'); + await next(); + return; + } + + if (!approovEnabled) { + logVerbose(ctx, 'approov', 'skip', 'Approov checks disabled.'); + await next(); + return; + } + + logVerbose(ctx, 'approov', 'start', 'Approov verification started.'); + const token = readApproovToken(ctx.headers); + if (!hasText(token)) { + logVerbose(ctx, 'approov', 'fail', 'Missing Approov-Token header.'); + unauthorized(ctx.res, 'Missing Approov-Token header.'); + return; + } + + let claims; + try { + claims = verifyApproovToken(token); + logVerbose( + ctx, + 'approov', + 'token', + `Token verified (exp=${claims.exp ?? 'n/a'}; ipk=${hasText(claims.ipk ? String(claims.ipk) : '')}; mskid=${claims.mskid ?? 'n/a'}).` + ); + } catch (error) { + logVerbose(ctx, 'approov', 'fail', `Token verification failed: ${error.message}`); + unauthorized(ctx.res, error.message); + return; + } + + // Binding headers are configured per-route so the server can bind different + // endpoints to different header sets without hardcoding path checks. + const bindingHeaders = normalizeBindingHeaders(route.bindingHeaders); + if (tokenBindingEnabled && bindingHeaders.length > 0) { + // Build the binding material by concatenating all required headers. + // If any header is missing, we treat the binding as invalid. + const bindingValue = extractBindingValue(ctx.headers, bindingHeaders); + if (!hasText(bindingValue) || !isBindingValid(bindingValue, claims)) { + logVerbose(ctx, 'binding', 'fail', 'Token binding validation failed.'); + unauthorized(ctx.res, 'Invalid token binding.'); + return; + } + logVerbose(ctx, 'binding', 'ok', 'Token binding validation passed.'); + } + + if (route.requiresMessageSignature) { + try { + logVerbose(ctx, 'signature', 'start', 'Message signature verification started.'); + await verifyMessageSignatures(ctx, claims); + logVerbose(ctx, 'signature', 'ok', 'Message signature verification passed.'); + } catch (error) { + logVerbose(ctx, 'signature', 'fail', `Message signature verification failed: ${error.message}`); + unauthorized(ctx.res, error.message); + return; + } + } else { + logVerbose(ctx, 'signature', 'skip', 'Route does not require message signing.'); + } + + ctx.state.approovClaims = claims; + logVerbose(ctx, 'approov', 'done', 'Approov verification completed.'); + await next(); +} + +/** + * Validates the Approov JWT using the shared secret and standard JWT rules. + * We explicitly require HS256 because Approov tokens in this demo are HMAC based + * and rejecting other algorithms prevents algorithm substitution attacks. + * The JWT signature is recomputed and compared using timing-safe equality, then + * we enforce the exp claim to avoid accepting expired tokens. + */ +function verifyApproovToken(token) { + const parsed = parseJwt(token); + + if (parsed.header.alg !== 'HS256') { + logVerbose(null, 'approov', 'token', `Rejected token alg=${parsed.header.alg || 'unknown'}.`); + throw new Error(`Unsupported token alg: ${parsed.header.alg || 'unknown'}.`); + } + + const expectedSignature = signHmac(parsed.signingInput, APPROOV_SECRET); + if (!bufferEquals(expectedSignature, parsed.signature)) { + logVerbose(null, 'approov', 'token', 'Token signature mismatch.'); + throw new Error('Approov token signature verification failed.'); + } + + validateExpiration(parsed.payload); + return parsed.payload; +} + +/** + * Splits a JWT into header, payload, and signature and decodes the base64url parts. + * We also return the signing input (header.payload) so HMAC verification can + * be performed exactly as the token was produced. + */ +function parseJwt(token) { + const parts = token.split('.'); + if (parts.length !== 3) { + throw new Error('Approov token is not a valid JWT.'); + } + + const [headerB64, payloadB64, signatureB64] = parts; + const headerJson = base64UrlDecodeToString(headerB64); + const payloadJson = base64UrlDecodeToString(payloadB64); + const signature = base64UrlDecodeToBuffer(signatureB64); + + return { + header: parseJson(headerJson, 'header'), + payload: parseJson(payloadJson, 'payload'), + signature, + signingInput: `${headerB64}.${payloadB64}`, + }; +} + +/** + * Extracts the header material that was bound into the Approov token pay claim. + * The binding is created by concatenating the configured headers (in order) and + * hashing the result. This allows binding to multiple headers while keeping + * the binding material stable across otherwise mutable request bodies. + */ +/** + * Normalizes a route's bindingHeaders list into a clean array of header names. + * This guards against undefined/null entries and trims whitespace so the + * comparison is consistent. + */ +function normalizeBindingHeaders(bindingHeaders) { + if (!Array.isArray(bindingHeaders)) { + return []; + } + return bindingHeaders + .map((header) => (typeof header === 'string' ? header.trim() : '')) + .filter((header) => hasText(header)); +} + +/** + * Builds the token binding material by concatenating the configured headers. + * The Approov mobile SDK computes the hash of the same concatenation and stores + * it in the JWT pay claim; we repeat the process server-side for verification. + */ +function extractBindingValue(headers, bindingHeaders) { + const values = []; + for (const header of bindingHeaders) { + const value = trimOrNull(headerValue(headers, header)); + if (!hasText(value)) { + return null; + } + values.push(value); + } + return values.join(''); +} + +/** + * Validates the token binding by comparing the pay claim with a hash of the + * bound header value. Approov binding uses SHA-256 and base64 encoding, so we + * compute the same and compare using timing-safe equality to reduce side-channels. + */ +function isBindingValid(bindingValue, claims) { + const expected = typeof claims.pay === 'string' ? claims.pay.trim() : ''; + if (!hasText(expected)) { + logVerbose(null, 'binding', 'token', 'Missing pay claim for token binding.'); + return false; + } + + const computed = hashBase64(bindingValue); + return safeStringEqual(expected, computed); +} + +/** + * Enforces Approov HTTP Message Signatures using RFC 9421 semantics. + * The server runs in a single signature mode controlled by environment: + * - install mode (default): verifies only the install signature entry. + * - account mode: verifies only the account signature entry. + * Signature headers are parsed as Structured Headers and verified with + * http-message-signatures, including Content-Digest validation when present. + */ +async function verifyMessageSignatures(ctx, claims) { + const signatureMode = MESSAGE_SIGNATURE_MODE; + const accountKeyId = typeof claims.mskid === 'string' ? claims.mskid.trim() : ''; + const shouldVerifyInstall = signatureMode === 'install'; + const shouldVerifyAccount = signatureMode === 'account'; + const installPublicKey = shouldVerifyInstall + ? loadInstallPublicKey(claims) + : null; + + logVerbose(ctx, 'signature', 'mode', `Using ${signatureMode} signature mode.`); + + if (shouldVerifyInstall && !installPublicKey) { + logVerbose(ctx, 'signature', 'install', 'Install mode enabled but ipk claim missing.'); + throw new Error('Install signature mode requires ipk claim.'); + } + + if (shouldVerifyAccount) { + if (!hasText(accountKeyId)) { + logVerbose(ctx, 'signature', 'account', 'Account mode enabled but mskid claim missing.'); + throw new Error('Account signature mode requires mskid claim.'); + } + if (!APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET) { + logVerbose(ctx, 'signature', 'fail', 'Account message signing secret not configured.'); + throw new Error('Account message signing secret not configured.'); + } + if (hasText(APPROOV_ACCOUNT_MESSAGE_SIGNING_KEY_ID)) { + if (APPROOV_ACCOUNT_MESSAGE_SIGNING_KEY_ID !== accountKeyId) { + logVerbose( + ctx, + 'signature', + 'account', + `Account key id mismatch (expected=${APPROOV_ACCOUNT_MESSAGE_SIGNING_KEY_ID}, got=${accountKeyId}).` + ); + throw new Error('Account message signing key id mismatch.'); + } + } + } + + const signatureHeader = headerValue(ctx.headers, HEADER_NAMES.SIGNATURE); + const signatureInputHeader = headerValue(ctx.headers, HEADER_NAMES.SIGNATURE_INPUT); + if (!hasText(signatureHeader) || !hasText(signatureInputHeader)) { + logVerbose(ctx, 'signature', 'fail', 'Missing Signature or Signature-Input header.'); + throw new Error('Missing Signature headers.'); + } + + const signatures = structuredHeaders.parseDictionary(signatureHeader); + const signatureInputs = structuredHeaders.parseDictionary(signatureInputHeader); + const hasInstallEntry = signatures.has('install') || signatureInputs.has('install'); + const hasAccountEntry = signatures.has('account') || signatureInputs.has('account'); + logVerbose( + ctx, + 'signature', + 'headers', + `Signature keys=${Array.from(signatures.keys()).join(',') || 'none'}; Signature-Input keys=${Array.from(signatureInputs.keys()).join(',') || 'none'}.` + ); + + if (hasText(headerValue(ctx.headers, HEADER_NAMES.CONTENT_DIGEST))) { + await verifyContentDigest(ctx); + } + + if (shouldVerifyInstall && hasAccountEntry) { + logVerbose(ctx, 'signature', 'account', 'Account signature entry rejected in install mode.'); + throw new Error('Account signature is disabled.'); + } + + if (shouldVerifyAccount && hasInstallEntry) { + logVerbose(ctx, 'signature', 'install', 'Install signature entry rejected in account mode.'); + throw new Error('Install signature is disabled.'); + } + + if (shouldVerifyInstall) { + logVerbose(ctx, 'signature', 'install', 'Verifying install signature entry.'); + await verifySignatureEntry( + ctx, + signatures, + signatureInputs, + 'install', + installPublicKey, + INSTALL_MESSAGE_SIGNING_ALGORITHM + ); + } + + if (shouldVerifyAccount) { + logVerbose(ctx, 'signature', 'account', 'Verifying account signature entry.'); + await verifySignatureEntry( + ctx, + signatures, + signatureInputs, + 'account', + APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET, + ACCOUNT_MESSAGE_SIGNING_ALGORITHM + ); + } +} + +/** + * Loads the install public key from the Approov ipk claim. + * The claim is a base64url-encoded SPKI DER key, which we convert to a KeyObject + * so Node.js crypto can verify ECDSA signatures. + */ +function loadInstallPublicKey(claims) { + const publicKeyB64 = + typeof claims.ipk === 'string' ? claims.ipk.trim() : ''; + if (!hasText(publicKeyB64)) { + return null; + } + return createPublicKeyFromValue(publicKeyB64, 'Approov install public key'); +} + +/** + * Verifies a single Signature/Signature-Input entry by name (install or account). + * We build a canonical request, pass the verification key and required params, + * and rely on http-message-signatures to validate the signature base and params. + * The tolerance parameter allows bounded clock skew for created/expires values. + */ +async function verifySignatureEntry( + ctx, + signatures, + signatureInputs, + signatureName, + verificationKey, + algorithm +) { + const signatureEntry = signatures.get(signatureName); + const signatureInputEntry = signatureInputs.get(signatureName); + + if (!signatureEntry || !signatureInputEntry) { + logVerbose(ctx, 'signature', signatureName, 'Signature entry missing.'); + throw new Error(`Missing ${signatureName} signature entry.`); + } + + logSignatureInputDetails(ctx, signatureName, signatureInputEntry); + + const signatureHeader = structuredHeaders.serializeDictionary( + new Map([[signatureName, signatureEntry]]) + ); + const signatureInputHeader = structuredHeaders.serializeDictionary( + new Map([[signatureName, signatureInputEntry]]) + ); + + const signatureRequest = buildSignatureRequest( + ctx, + signatureHeader, + signatureInputHeader + ); + logVerbose(ctx, 'signature', signatureName, `Request URL used: ${signatureRequest.url}`); + logSignatureBaseHash(ctx, signatureName, signatureRequest, signatureInputEntry); + + const verified = await httpbis.verifyMessage( + { + keyLookup: async () => ({ + id: signatureName, + algs: [algorithm], + verify: createVerifier(verificationKey, algorithm), + }), + requiredParams: MESSAGE_SIGNING_REQUIRED_PARAMS, + tolerance: MESSAGE_SIGNING_TOLERANCE_SECONDS, + }, + signatureRequest + ); + + if (verified !== true) { + logVerbose(ctx, 'signature', signatureName, 'Signature verification returned false.'); + throw new Error(`Invalid ${signatureName} message signature.`); + } +} + +/** + * Constructs a minimal request object for signature verification. + * We re-serialize only the signature entry being verified to avoid interference + * from unrelated signature keys and to mirror how the client produced the base. + */ +function buildSignatureRequest(ctx, signatureHeader, signatureInputHeader) { + return { + method: ctx.method, + url: buildRequestUrl(ctx.req), + headers: { + ...ctx.headers, + [HEADER_NAMES.SIGNATURE]: signatureHeader, + [HEADER_NAMES.SIGNATURE_INPUT]: signatureInputHeader, + }, + }; +} + +/** + * Builds an absolute request URL for signature verification and logging. + * HTTP Message Signatures can include derived components like @authority and + * @target-uri, so we must reconstruct the public URL as seen by the client. + * We honor X-Forwarded-* and RFC 7239 Forwarded headers to support proxies. + */ +function buildRequestUrl(req) { + const forwardedProto = firstHeaderValue(req.headers['x-forwarded-proto']); + const forwardedHost = firstHeaderValue(req.headers['x-forwarded-host']); + const forwarded = firstHeaderValue(req.headers['forwarded']); + const forwardedHostFromForwarded = parseForwardedParam(forwarded, 'host'); + + let scheme = forwardedProto; + if (!scheme && forwarded) { + const forwardedProtoFromForwarded = parseForwardedParam(forwarded, 'proto'); + if (forwardedProtoFromForwarded) { + scheme = forwardedProtoFromForwarded; + } + } + if (!scheme) { + scheme = req.socket && req.socket.encrypted ? 'https' : 'http'; + } + + const host = + forwardedHost || + forwardedHostFromForwarded || + req.headers.host || + `${SERVER_HOSTNAME}:${HTTP_PORT}`; + return `${scheme}://${host}${req.url || '/'}`; +} + +/** + * Validates the Content-Digest header (RFC 9530) against the request body. + * This ensures payload integrity and is required when the signature input + * references content-digest or when clients include it for additional safety. + */ +async function verifyContentDigest(ctx) { + const header = headerValue(ctx.headers, HEADER_NAMES.CONTENT_DIGEST); + if (!hasText(header)) { + return; + } + + const digestEntries = structuredHeaders.parseDictionary(header); + if (!digestEntries || digestEntries.size === 0) { + logVerbose(ctx, 'digest', 'fail', 'Content-Digest header empty.'); + throw new Error('Content-Digest header is empty.'); + } + + const body = await readRequestBody(ctx); + + for (const [algo, [item]] of digestEntries.entries()) { + const normalizedAlgo = algo.toLowerCase(); + const hashAlgo = + normalizedAlgo === 'sha-256' + ? 'sha256' + : normalizedAlgo === 'sha-512' + ? 'sha512' + : null; + + if (!hashAlgo) { + logVerbose(ctx, 'digest', 'fail', `Unsupported algorithm: ${algo}.`); + throw new Error(`Unsupported content digest algorithm: ${algo}.`); + } + + let expectedDigest; + if (item instanceof structuredHeaders.ByteSequence) { + expectedDigest = item.toBase64(); + } else if (typeof item === 'string') { + expectedDigest = item; + } else { + logVerbose(ctx, 'digest', 'fail', `Unsupported Content-Digest value for ${algo}.`); + throw new Error(`Unsupported Content-Digest value for ${algo}.`); + } + + const computedDigest = crypto + .createHash(hashAlgo) + .update(body) + .digest('base64'); + + if (!safeStringEqual(expectedDigest, computedDigest)) { + logVerbose( + ctx, + 'digest', + 'fail', + `Digest mismatch for ${algo} (expected=${expectedDigest}, computed=${computedDigest}).` + ); + throw new Error(`Content digest verification failed for ${algo}.`); + } + logVerbose(ctx, 'digest', 'ok', `Digest matched for ${algo}.`); + } +} + +/** + * Reads and caches the request body so multiple consumers can access it. + * We store both the Promise and the resolved buffer to avoid double-reading + * the stream when verifying Content-Digest and when logging HTTP exchanges. + */ +async function readRequestBody(ctx) { + if (ctx.state.bodyBuffer) { + return ctx.state.bodyBuffer; + } + if (ctx.state.bodyPromise) { + return ctx.state.bodyPromise; + } + + ctx.state.bodyPromise = new Promise((resolve, reject) => { + const chunks = []; + ctx.req.on('data', (chunk) => { + chunks.push(chunk); + }); + ctx.req.on('end', () => resolve(Buffer.concat(chunks))); + ctx.req.on('error', reject); + }); + + const bodyBuffer = await ctx.state.bodyPromise; + ctx.state.bodyBuffer = bodyBuffer; + ctx.state.bodyPromise = null; + return bodyBuffer; +} + +/** + * Ensures the Approov token has a valid exp claim and is not expired. + * Approov tokens are time-bound, so exp must be in seconds since epoch and + * strictly greater than the current time to be accepted. + */ +function validateExpiration(claims) { + const exp = Number(claims.exp); + if (!Number.isFinite(exp)) { + throw new Error('Approov token missing exp claim.'); + } + + const now = Math.floor(Date.now() / 1000); + if (exp <= now) { + throw new Error('Approov token expired.'); + } +} + +/** + * Builds a standard response fragment describing enforcement state. + * This keeps demo handlers consistent and makes responses easier to parse in tests. + */ +function statePayload() { + return { + approovEnabled, + tokenBindingEnabled, + }; +} + +/** + * Adds a human-friendly detail string to the standard state payload. + * This helps demo clients confirm which endpoint they hit and which checks ran. + */ +function infoPayload(details) { + return { + ...statePayload(), + details, + }; +} + +/** + * Reads the Approov token from the expected header. + * Approov SDKs send the token as "Approov-Token" which Node exposes lowercased. + */ +function readApproovToken(headers) { + return trimOrNull(headerValue(headers, HEADER_NAMES.APPROOV_TOKEN)); +} + +/** + * Normalizes header values that may be arrays in Node.js. + * For our purposes we only care about the first header value. + */ +function headerValue(headers, name) { + const value = headers[name]; + return Array.isArray(value) ? value[0] : value; +} + +/** + * Runs middleware in sequence, similar to a tiny Koa-style dispatcher. + * Each middleware receives a next() function it must await to continue the chain. + */ +async function runMiddleware(ctx, middlewares, handler) { + let index = -1; + + const dispatch = async () => { + index += 1; + const fn = index < middlewares.length ? middlewares[index] : handler; + if (!fn) { + return; + } + await fn(ctx, dispatch); + }; + + await dispatch(); +} + +/** + * Extracts path and method for routing. + * We build a URL using the Host header so relative URLs can be parsed reliably. + */ +function parseRequest(req) { + const host = req.headers.host || `${SERVER_HOSTNAME}:${HTTP_PORT}`; + const url = new URL(req.url || '/', `http://${host}`); + return { + path: url.pathname, + method: (req.method || 'GET').toUpperCase(), + }; +} + +/** + * Sends a consistent 401 JSON response for authentication failures. + * This keeps error handling predictable for demo clients and scripts. + */ +function unauthorized(res, message) { + writeJson(res, 401, { error: 'unauthorized', message }); +} + +/** + * Serializes and writes JSON with standard headers. + * Content-Length is set to avoid chunked encoding surprises in simple clients. + */ +function writeJson(res, statusCode, payload) { + const body = JSON.stringify(payload); + res.writeHead(statusCode, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-cache', + 'Content-Length': Buffer.byteLength(body), + }); + res.end(body); +} + +/** + * Computes an HMAC-SHA256 signature of the JWT signing input. + * Approov tokens in this demo are HS256, so this reproduces the expected signature. + */ +function signHmac(value, secret) { + return crypto.createHmac('sha256', secret).update(value).digest(); +} + +/** + * Hashes a string with SHA-256 and returns base64 output. + * Approov token binding uses this exact hash+base64 format for the pay claim. + */ +function hashBase64(value) { + return crypto + .createHash('sha256') + .update(value, 'utf8') + .digest('base64'); +} + +/** + * Compares two buffers using timing-safe equality. + * This avoids leaking partial match information when verifying signatures. + */ +function bufferEquals(left, right) { + if (!Buffer.isBuffer(left) || !Buffer.isBuffer(right)) { + return false; + } + if (left.length !== right.length) { + return false; + } + return crypto.timingSafeEqual(left, right); +} + +/** + * Performs a timing-safe comparison of two non-empty strings. + * Empty values are rejected to avoid false positives in security checks. + */ +function safeStringEqual(left, right) { + if (!hasText(left) || !hasText(right)) { + return false; + } + return bufferEquals(Buffer.from(left), Buffer.from(right)); +} + +/** + * Parses a JSON string with a clearer error message on failure. + * This is used for JWT header and payload decoding to produce precise errors. + */ +function parseJson(value, label) { + try { + return JSON.parse(value); + } catch (error) { + throw new Error(`Approov token ${label} is not valid JSON.`); + } +} + +/** + * Decodes a base64url string to UTF-8 text. + * JWT header and payload sections are base64url-encoded JSON. + */ +function base64UrlDecodeToString(value) { + return base64UrlDecodeToBuffer(value).toString('utf8'); +} + +/** + * Decodes a base64url string into a Buffer. + * We normalize URL-safe characters and apply required padding before decoding. + */ +function base64UrlDecodeToBuffer(value) { + const normalized = value.replace(/-/g, '+').replace(/_/g, '/'); + const padded = normalized.padEnd( + normalized.length + ((4 - (normalized.length % 4)) % 4), + '=' + ); + return Buffer.from(padded, 'base64'); +} + +/** + * Writes verbose, contextual logs when enabled. + * We include the method/path prefix to tie logs back to a specific request. + */ +function logVerbose(ctx, area, step, message) { + if (!VERBOSE_LOGGING) { + return; + } + const prefix = ctx ? `[${ctx.method} ${ctx.path}]` : '[Approov]'; + console.log(`${prefix} ${area}:${step} ${message}`); +} + +/** + * Logs structured details of the Signature-Input entry. + * This helps debug which components and parameters were used in the signature base. + */ +function logSignatureInputDetails(ctx, signatureName, signatureInputEntry) { + if (!VERBOSE_LOGGING) { + return; + } + if (!Array.isArray(signatureInputEntry) || signatureInputEntry.length < 2) { + logVerbose(ctx, 'signature', signatureName, 'Signature input entry malformed.'); + return; + } + const components = signatureInputEntry[0] || []; + const params = signatureInputEntry[1] || new Map(); + const componentNames = components.map(([name]) => name).join(',') || 'none'; + const paramPairs = Array.from(params.entries()) + .map(([key, value]) => `${key}=${value}`) + .join(',') || 'none'; + logVerbose( + ctx, + 'signature', + signatureName, + `Signature-Input components=[${componentNames}], params=[${paramPairs}].` + ); +} + +/** + * Computes and logs a hash of the signature base for debugging. + * Comparing this value with the client helps diagnose canonicalization mismatches. + */ +function logSignatureBaseHash(ctx, signatureName, signatureRequest, signatureInputEntry) { + if (!VERBOSE_LOGGING) { + return; + } + try { + if (!Array.isArray(signatureInputEntry) || signatureInputEntry.length < 2) { + return; + } + const components = signatureInputEntry[0] || []; + const fields = components.map((item) => structuredHeaders.serializeItem(item)); + const signatureBase = httpbis.createSignatureBase( + { fields }, + signatureRequest + ); + signatureBase.push([ + '"@signature-params"', + [structuredHeaders.serializeList([signatureInputEntry])], + ]); + const base = httpbis.formatSignatureBase(signatureBase); + const baseHash = crypto + .createHash('sha256') + .update(base) + .digest('base64'); + logVerbose( + ctx, + 'signature', + signatureName, + `Signature base sha256 (base64)=${baseHash}` + ); + } catch (error) { + logVerbose( + ctx, + 'signature', + signatureName, + `Failed to compute signature base hash: ${error.message}` + ); + } +} + +/** + * Wraps the response stream to capture and log full HTTP exchanges. + * This is intended for debugging message signing and payload digest mismatches. + */ +function enableHttpLogging(ctx) { + if (!HTTP_LOGGING_ENABLED) { + return; + } + + const startTime = Date.now(); + const responseChunks = []; + const originalWrite = ctx.res.write.bind(ctx.res); + const originalEnd = ctx.res.end.bind(ctx.res); + + ctx.res.write = (chunk, encoding, callback) => { + if (chunk) { + responseChunks.push(toBuffer(chunk, encoding)); + } + return originalWrite(chunk, encoding, callback); + }; + + ctx.res.end = (chunk, encoding, callback) => { + if (chunk) { + responseChunks.push(toBuffer(chunk, encoding)); + } + const responseBody = Buffer.concat(responseChunks); + logHttpExchange(ctx, responseBody, startTime); + return originalEnd(chunk, encoding, callback); + }; +} + +/** + * Logs the request/response pair with headers and bodies. + * Uses buildRequestUrl to reflect the public URL used for signature verification. + */ +function logHttpExchange(ctx, responseBody, startTime) { + if (!HTTP_LOGGING_ENABLED) { + return; + } + const durationMs = Date.now() - startTime; + const requestBodyPromise = + ctx.state.bodyPromise || Promise.resolve(ctx.state.bodyBuffer || Buffer.alloc(0)); + requestBodyPromise + .then((requestBody) => { + const requestUrl = buildRequestUrl(ctx.req); + const responseHeaders = ctx.res.getHeaders(); + console.log('--- HTTP EXCHANGE START ---'); + console.log(`Request: ${ctx.method} ${requestUrl}`); + console.log('Request Headers:', JSON.stringify(ctx.headers, null, 2)); + console.log('Request Body:', requestBody.toString('utf8')); + console.log(`Response Status: ${ctx.res.statusCode}`); + console.log('Response Headers:', JSON.stringify(responseHeaders, null, 2)); + console.log('Response Body:', responseBody.toString('utf8')); + console.log(`Duration: ${durationMs}ms`); + console.log('--- HTTP EXCHANGE END ---'); + }) + .catch((error) => { + console.log('--- HTTP EXCHANGE START ---'); + console.log(`Request: ${ctx.method} ${ctx.path}`); + console.log('Request Headers:', JSON.stringify(ctx.headers, null, 2)); + console.log(`Failed to read request body: ${error.message}`); + console.log(`Response Status: ${ctx.res.statusCode}`); + console.log('Response Headers:', JSON.stringify(ctx.res.getHeaders(), null, 2)); + console.log('Response Body:', responseBody.toString('utf8')); + console.log(`Duration: ${durationMs}ms`); + console.log('--- HTTP EXCHANGE END ---'); + }); +} + +/** + * Normalizes response chunks into Buffer form. + * This keeps logging code simple regardless of whether chunks are strings or Buffers. + */ +function toBuffer(chunk, encoding) { + if (Buffer.isBuffer(chunk)) { + return chunk; + } + if (typeof chunk === 'string') { + return Buffer.from(chunk, encoding); + } + return Buffer.from(String(chunk)); +} + +/** + * Parses a parameter from the RFC 7239 Forwarded header. + * We only inspect the first Forwarded entry because it represents the client-facing hop. + */ +function parseForwardedParam(forwardedValue, key) { + if (!hasText(forwardedValue) || !hasText(key)) { + return null; + } + + const targetKey = key.toLowerCase(); + const entry = forwardedValue.split(',')[0]; + const pairs = entry.split(';'); + for (const pair of pairs) { + const trimmed = pair.trim(); + if (!trimmed) { + continue; + } + const index = trimmed.indexOf('='); + if (index <= 0) { + continue; + } + const paramKey = trimmed.slice(0, index).trim().toLowerCase(); + if (paramKey !== targetKey) { + continue; + } + let value = trimmed.slice(index + 1).trim(); + if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1); + } + return value; + } + return null; +} + +/** + * Returns the first header value from a potentially comma-separated list. + * Proxies often append values; we only use the first for scheme/host reconstruction. + */ +function firstHeaderValue(value) { + if (!value) { + return null; + } + const raw = Array.isArray(value) ? value[0] : value; + if (!hasText(raw)) { + return null; + } + return raw.split(',')[0].trim(); +} + +/** + * Converts a PEM or base64url-encoded SPKI key into a KeyObject. + * Approov ipk claims are DER-encoded keys, while some tooling may supply PEM. + */ +function createPublicKeyFromValue(value, label) { + const trimmed = value.trim(); + try { + if (trimmed.includes('-----BEGIN')) { + return crypto.createPublicKey(trimmed); + } + const der = base64UrlDecodeToBuffer(trimmed); + return crypto.createPublicKey({ key: der, format: 'der', type: 'spki' }); + } catch (error) { + throw new Error(`${label} is invalid: ${error.message}`); + } +} + +/** + * Loads the account-level message signing secret from environment variables. + * Approov supports raw, base64, or base64url encodings; we accept all three for + * flexibility and exit early if decoding fails to avoid silent misconfiguration. + */ +function loadAccountMessageSigningSecret() { + const secretValue = + APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64URL || + APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64 || + APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_RAW; + + if (!hasText(secretValue)) { + return null; + } + + try { + if (APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_RAW) { + return Buffer.from(APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_RAW, 'utf8'); + } + if (APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64URL) { + return base64UrlDecodeToBuffer(APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64URL); + } + return Buffer.from(APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64, 'base64'); + } catch (error) { + console.error(`Account message signing secret is invalid: ${error.message}`); + process.exit(1); + } +} + +/** + * Parses a port number with a safe fallback. + * Invalid input returns the default to keep the demo server predictable. + */ +function parsePort(value, fallback) { + if (!hasText(value)) { + return fallback; + } + const port = Number.parseInt(value, 10); + return Number.isFinite(port) ? port : fallback; +} + +/** + * Parses a non-negative integer with a fallback. + * Used for configurable tolerance values such as message-signing clock skew. + */ +function parsePositiveInt(value, fallback) { + if (!hasText(value)) { + return fallback; + } + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; +} + +/** + * Parses common boolean environment values (true/false, 1/0, yes/no, on/off). + * Unrecognized values fall back to the provided default. + */ +function parseBoolean(value, fallback) { + if (value == null) { + return fallback; + } + const normalized = String(value).trim().toLowerCase(); + if (['true', '1', 'yes', 'y', 'on'].includes(normalized)) { + return true; + } + if (['false', '0', 'no', 'n', 'off'].includes(normalized)) { + return false; + } + return fallback; +} + +/** + * Returns true when a value is a non-empty, non-whitespace string. + * This is used throughout to validate required configuration and headers. + */ +function hasText(value) { + return typeof value === 'string' && value.trim().length > 0; +} + +/** + * Trims a string or returns null for null/undefined. + * This keeps header handling uniform without adding special-case checks. + */ +function trimOrNull(value) { + return value == null ? null : value.trim(); +} + +/** + * Minimal .env loader used instead of dotenv to keep the example self-contained. + * We avoid overwriting existing process.env values so explicit environment + * configuration takes precedence over the local file. + */ +function loadEnvFile(filePath) { + if (!fs.existsSync(filePath)) { + return; + } + + const content = fs.readFileSync(filePath, 'utf8'); + content.split(/\r?\n/).forEach((line) => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) { + return; + } + const index = trimmed.indexOf('='); + if (index <= 0) { + return; + } + const key = trimmed.slice(0, index).trim(); + let value = trimmed.slice(index + 1).trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + if (process.env[key] === undefined) { + process.env[key] = value; + } + }); +} diff --git a/Dockerfile b/Dockerfile index af659ec..c737cd6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,47 +1,16 @@ -ARG TAG=17.7.1-slim +# syntax=docker/dockerfile:1 +# Builds the quickstart backend container image and configures scripts/install-prerequisites.sh and scripts/build.sh +# as the entrypoint used both locally and when deployed via Docker. +FROM node:22-slim -FROM node:${TAG} +ENV APP_HOME=/workspace \ + RUN_MODE=container -ARG CONTAINER_USER="node" -ARG LANGUAGE_CODE="en" -ARG COUNTRY_CODE="GB" -ARG ENCODING="UTF-8" +WORKDIR /app -ARG LOCALE_STRING="${LANGUAGE_CODE}_${COUNTRY_CODE}" -ARG LOCALIZATION="${LOCALE_STRING}.${ENCODING}" +COPY . . -ARG OH_MY_ZSH_THEME="bira" +RUN npm install -RUN apt update && apt -y upgrade && \ - apt -y install \ - locales \ - git \ - curl \ - inotify-tools \ - zsh && \ - - echo "${LOCALIZATION} ${ENCODING}" > /etc/locale.gen && \ - locale-gen "${LOCALIZATION}" && \ - - # useradd -m -u 1000 -s /usr/bin/zsh "${CONTAINER_USER}" && \ - - bash -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" && \ - - cp -v /root/.zshrc /home/"${CONTAINER_USER}"/.zshrc && \ - cp -rv /root/.oh-my-zsh /home/"${CONTAINER_USER}"/.oh-my-zsh && \ - sed -i "s/\/root/\/home\/${CONTAINER_USER}/g" /home/"${CONTAINER_USER}"/.zshrc && \ - sed -i s/ZSH_THEME=\"robbyrussell\"/ZSH_THEME=\"${OH_MY_ZSH_THEME}\"/g /home/${CONTAINER_USER}/.zshrc && \ - mkdir /home/"${CONTAINER_USER}"/workspace && \ - chown -R "${CONTAINER_USER}":"${CONTAINER_USER}" /home/"${CONTAINER_USER}" - -USER ${CONTAINER_USER} - -ENV USER ${CONTAINER_USER} -ENV LANG "${LOCALIZATION}" -ENV LANGUAGE "${LOCALE_STRING}:${LANGUAGE_CODE}" -ENV PATH=/home/${CONTAINER_USER}/.local/bin:${PATH} -ENV LC_ALL "${LOCALIZATION}" - -WORKDIR /home/${CONTAINER_USER}/workspace - -CMD ["zsh"] +# Provide APP_START_CMD via --env-file. +CMD ["bash", "scripts/build.sh"] diff --git a/EXAMPLES.md b/EXAMPLES.md deleted file mode 100644 index 307e39e..0000000 --- a/EXAMPLES.md +++ /dev/null @@ -1,114 +0,0 @@ -# Approov Integrations Examples - -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps, and here you can find the Hello servers examples that are the base for the Approov [quickstarts](/docs) for NodeJS. - -For more information about how Approov works and why you should use it you can read the [Approov Overview](/OVERVIEW.md) at the root of this repo. - -If you are looking for the Approov quickstarts to integrate Approov in your NodeJS API server then you can find them [here](/QUICKSTARTS.md). - - -## Hello Server Examples - -To learn more about each Hello server example you need to read the README for each one at: - -* [Unprotected Server](./src/unprotected-server) -* [Approov Protected Server - Token Check](./src/approov-protected-server/token-check) -* [Approov Protected Server - Token Binding Check](./src/approov-protected-server/token-binding-check) - - -## Docker Stack - -The docker stack provided via the `docker-compose.yml` file in this folder is used for development proposes and if you are familiar with docker then feel free to also use it to follow along the examples on the README of each server. - -If you decide to use the docker stack then you need to bear in mind that the Postman collections, used to test the servers examples, will connect to port `8002` therefore you cannot start all docker compose services at once, for example with `docker-compose up`, instead you need to run one at a time as exemplified below. - -### Setup Env File - -Do not forget to properly setup the `.env` file in the root of each Approov protected server example before you run the server with the docker stack. - -```bash -cp src/unprotected-server/.env.example src/unprotected-server/.env -cp src/approov-protected-server/token-check/.env.example src/approov-protected-server/token-check/.env -cp src/approov-protected-server/token-binding-check/.env.example src/approov-protected-server/token-binding-check/.env -``` - -Edit each file and add the [dummy secret](/TESTING.md#the-dummy-secret) to it in order to be able to test the Approov integration with the provided [Postman collection](https://github.com/approov/postman-collections/blob/master/quickstarts/hello-world/hello-world.postman_curl_requests_examples.md). - - -### Build the Docker Stack - -The three services in the `docker-compose.yml` use the same Dockerfile, therefore to build the Docker image we just need to used one of them: - -```bash -sudo docker-compose build approov-token-binding-check -``` - -Now, you are ready to start using the Docker stack for NodeJS. - - -### Command Examples - -To run each of the Hello servers with docker compose you just need to follow the respective example below. - -#### For the unprotected server - -Run the container attached to your machine bash shell: - -```bash -sudo docker-compose up unprotected-server -``` - -or get a bash shell inside the container: - -```bash -sudo docker-compose run --rm --service-ports unprotected-server zsh -``` - -#### For the Approov Token Check - -Run the container attached to the shell: - -```bash -sudo docker-compose up approov-token-check -``` - -or get a bash shell inside the container: - -```bash -sudo docker-compose run --rm --service-ports approov-token-check zsh -``` - -#### For the Approov Token Binding Check - -Run the container attached to the shell: - -```bash -sudo docker-compose up approov-token-binding-check -``` - -or get a bash shell inside the container: - -```bash -sudo docker-compose run --rm --service-ports approov-token-binding-check zsh -``` - - -## Issues - -If you find any issue while following the example then just open an issue on this repo with the steps to reproduce it and we will help you to solve them. - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) diff --git a/OVERVIEW.md b/OVERVIEW.md deleted file mode 100644 index 40f2398..0000000 --- a/OVERVIEW.md +++ /dev/null @@ -1,55 +0,0 @@ -# Approov Overview - -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps. - - -## Why? - -You can learn more about Approov, the motives for adopting it, and more detail on how it works by following this [link](https://approov.io/product). In brief, Approov: - -* Ensures that accesses to your API come from official versions of your apps; it blocks accesses from republished, modified, or tampered versions -* Protects the sensitive data behind your API; it prevents direct API abuse from bots or scripts scraping data and other malicious activity -* Secures the communication channel between your app and your API with [Approov Dynamic Certificate Pinning](https://approov.io/docs/latest/approov-usage-documentation/#approov-dynamic-pinning). This has all the benefits of traditional pinning but without the drawbacks -* Removes the need for an API key in the mobile app -* Provides DoS protection against targeted attacks that aim to exhaust the API server resources to prevent real users from reaching the service or to at least degrade the user experience. - - -## How it works? - -This is a brief overview of how the Approov cloud service and the backend server fit together from a backend perspective. For a complete overview of how the mobile app and backend fit together with the Approov cloud service and the Approov SDK we recommend to read the [Approov overview](https://approov.io/product) page on our website. - -### Approov Cloud Service - -The Approov cloud service attests that a device is running a legitimate and tamper-free version of your mobile app. - -* If the integrity check passes then a valid token is returned to the mobile app -* If the integrity check fails then a legitimate looking token will be returned - -In either case, the app, unaware of the token's validity, adds it to every request it makes to the Approov protected API(s). - -### The Backend Server - -The backend server ensures that the token supplied in the `Approov-Token` header is present and valid. The validation is done by using a shared secret known only to the Approov cloud service and the backend server. - -The request is handled such that: - -* If the Approov Token is valid, the request is allowed to be processed by the API endpoint -* If the Approov Token is invalid, an HTTP 401 Unauthorized response is returned - -You can choose to log JWT verification failures, but we left it out on purpose so that you can have the choice of how you prefer to do it and decide the right amount of information you want to log. - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) diff --git a/QUICKSTARTS.md b/QUICKSTARTS.md deleted file mode 100644 index 37b80e3..0000000 --- a/QUICKSTARTS.md +++ /dev/null @@ -1,51 +0,0 @@ -# Approov Integration Quickstarts - -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps. - - -## The Quickstarts - -The quickstart code for the Approov backend server is split into two implementations. The first gets you up and running with basic token checking. The second uses a more advanced Approov feature, _token binding_. Token binding may be used to link the Approov token with other properties of the request, such as user authentication (more details can be found [here](https://approov.io/docs/latest/approov-usage-documentation/#token-binding)). -* [Approov token check quickstart](/docs/APPROOV_TOKEN_QUICKSTART.md) -* [Approov token check with token binding quickstart](/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md) - -Both the quickstarts are built from the unprotected example server defined [here](/src/unprotected-server/hello-server-unprotected.js), thus you can use Git to see the code differences between them. - -Code difference between the Approov token check quickstart and the original unprotected server: - -``` -git diff --no-index src/unprotected-server/hello-server-unprotected.js src/approov-protected-server/token-check/hello-server-protected.js -``` - -You can do the same for the Approov token binding quickstart: - -``` -git diff --no-index src/unprotected-server/hello-server-unprotected.js src/approov-protected-server/token-binding-check/hello-server-protected.js -``` - -Or you can compare the code difference between the two quickstarts: - -``` -git diff --no-index src/approov-protected-server/token-check/hello-server-protected.js src/approov-protected-server/token-binding-check/hello-server-protected.js -``` - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-nodejs-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) diff --git a/README.md b/README.md index 1701d6d..8e2930d 100644 --- a/README.md +++ b/README.md @@ -1,147 +1,275 @@ -# Approov QuickStart - NodeJS Token Check +# Approov Backend Quickstart - Node.js -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps. +This project provides a server-side example of Approov token verification for a protected backend API. It exposes a simple API that verifies Approov tokens before granting access to protected endpoints and demonstrates how the endpoints behave under the current Approov configuration: -This repo implements the Approov server-side request verification code in NodeJS (framework agnostic), which performs the verification check before allowing valid traffic to be processed by the API endpoint. + - `/unprotected` - no Approov token required. + - `/token-check` - requires a valid Approov token. + - `/token-check-signature` - requires a valid Approov token and HTTP message signature + - `/token-binding` - requires a valid Approov token which is bound to a header value. + - `/token-double-binding` - requires a valid Approov token which is bound to two header values. +In this example, Approov protection is enforced by [approovTokenVerifier](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L199-L235), which reads the `Approov-Token` header, and validates the token `signature` and `exp` claim in [verifyApproovToken](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L237-L251). Protected endpoints are those with `requiresApproov: true` in [ROUTES](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L32-L88). -## Approov Integration Quickstart +## Approov Token Verification Flow -The quickstart was tested with the following Operating Systems: +1. **Token Request:** + The Approov SDK inside the mobile app securely communicates with the Approov Cloud Service to obtain a short-lived [Approov Token](https://ext.approov.io/docs/latest/approov-usage-documentation/#approov-tokens) (a signed JWT). + Additionally, you can use the CLI [token commands](https://ext.approov.io/docs/latest/approov-cli-tool-reference/#token-commands) to validate tokens, generate new ones, and set the data hash. -* Ubuntu 20.04 -* MacOS Big Sur -* Windows 10 WSL2 - Ubuntu 20.04 +2. **Token Attachment:** + The app attaches this token to every API request using the `Approov-Token` HTTP header. -First, setup the [Approov CLI](https://approov.io/docs/latest/approov-installation/index.html#initializing-the-approov-cli). +3. **Server Validation:** + The [server verifies](https://ext.approov.io/docs/latest/approov-usage-documentation/#approov-architecture) the token using the shared Approov secret, checking its: + - Signature authenticity + - Expiration (`exp` claim) + - Other claims if configured -Now, register the API domain for which Approov will issues tokens: +4. **Token Binding (Optional):** + [Token binding](https://ext.approov.io/docs/latest/approov-usage-documentation/#token-binding) is configured by the app via the Approov SDK, which hashes a chosen binding value (for example the `Authorization` header) and embeds it into the Approov token. + The protected API then computes the same hash from the incoming request and verifies that it matches the `pay` claim, preventing token reuse or replay attacks. For local testing, you can also generate example tokens with a binding using the Approov CLI. + +5. **Request Decision:** + If all checks pass → the request is trusted and processed `200 OK`. + If validation fails → the server responds with `401 Unauthorized`. + +## Requirements: + +1. ***Approov account*** - If you're new, sign up for an [Approov trial account](https://approov.io/signup). +2. ***Approov CLI initialized*** - Follow the [installation guide](https://ext.approov.io/docs/latest/approov-installation/#initializing-the-approov-cli) and confirm `approov whoami` works. +3. ***Install curl*** - Ensure the `curl` CLI is available. +4. ***Create .env file*** - copy `.env.example` so there is a place to store the secret key. + ```bash + cp .env.example .env + ``` + +5. ***Configure secret*** - fetch the secret and add it to `.env` (`APPROOV_BASE64URL_SECRET`): + ```bash + approov secret -get base64url + ``` + +6. ***Register API domain*** - point Approov at your backend API (default example.com): + ```bash + approov api -add example.com + ``` + +7. ***Install Docker and Docker Compose*** - follow the official guide: [Docker docs](https://docs.docker.com/get-started/get-docker/) + +## Try it yourself using Docker + +*If you have all requirements, you can run* ```bash -approov api -add api.example.com +bash run-server.sh ``` -> **NOTE:** By default a symmetric key (HS256) is used to sign the Approov token on a valid attestation of the mobile app for each API domain it's added with the Approov CLI, so that all APIs will share the same secret and the backend needs to take care to keep this secret secure. -> -> A more secure alternative is to use asymmetric keys (RS256 or others) that allows for a different keyset to be used on each API domain and for the Approov token to be verified with a public key that can only verify, but not sign, Approov tokens. -> -> To implement the asymmetric key you need to change from using the symmetric HS256 algorithm to an asymmetric algorithm, for example RS256, that requires you to first [add a new key](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-new-key), and then specify it when [adding each API domain](https://approov.io/docs/latest/approov-usage-documentation/#keyset-key-api-addition). Please visit [Managing Key Sets](https://approov.io/docs/latest/approov-usage-documentation/#managing-key-sets) on the Approov documentation for more details. +This script: +- Builds and starts the container via `scripts/build.sh` (`docker build` + `docker run`) and waits for `/approov-state` to be ready. + +*Once finished, press `Ctrl+C` to stop log tailing; the container keeps running unless you stop it. Use `docker ps` to find the container name and `docker stop ` to stop it.* -Next, enable your Approov `admin` role with: +### Automated and Manual Testing + +*When the server is running (in a different terminal), validate the endpoints via the automated bash script or by running the manual checks below* ```bash -eval `approov role admin` -```` +bash test.sh +``` + +This script: +- Verifies that the `approov` and `curl` commands are installed. +- Checks Approov status by calling `/approov-state` (enabled vs disabled). +- Runs endpoint tests against `/unprotected` (no token), `/token-check` (valid/invalid Approov tokens), `/token-binding` (token bound to `Authorization`), and `/token-double-binding` (token bound to `Authorization` + `Session-Id`). +- Logs full request/response details to `.config/logs/.log`. -For the Windows powershell: +#### *1. Unprotected Endpoint (No Approov)* + +- The client sends a normal HTTP request. +- The server **does not verify** any Approov token or extra authentication header. +- This means **any client** (even tampered or unauthorized) can call the API if they know the URL. + +*The following example shows how the API responds when no Approov protection is applied.* ```bash -set APPROOV_ROLE=admin:___YOUR_APPROOV_ACCOUNT_NAME_HERE___ -```` +curl -iX GET http://localhost:8111/unprotected +``` + +The response will be `200 OK` for this request: +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache +``` + +#### *2. Approov Token Check* -Now, get your Approov Secret with the [Approov CLI](https://approov.io/docs/latest/approov-installation/index.html#initializing-the-approov-cli): +- The client includes an `Approov-Token` (a short-lived JWT) in each API request header. +- The server verifies this token using the **Approov secret key** that is securely configured on the backend and checks: + - Token verification - confirms the token is signed by the Approov secret. + - Expiration (`exp` claim) - ensures the token is still valid. +- If the token is valid → request is trusted. +- If invalid → server returns `401 Unauthorized`. +- **Purpose**: Protect API endpoints so that only authentic, unmodified Approov-integrated apps can access them. + +***The following example shows how the API responds when an Approov token is required.*** + +*Generate a valid Approov token:* ```bash -approov secret -get base64 +approov token -genExample example.com ``` -Next, add the [Approov secret](https://approov.io/docs/latest/approov-usage-documentation/#account-secret-key-export) to your project `.env` file: +*Use the generated token in the `Approov-Token` header and `/token-check` endpoint.* -```env -APPROOV_BASE64_SECRET=approov_base64_secret_here +```bash +curl -iX GET http://localhost:8111/token-check \ + -H "Approov-Token: valid_approov_token_here" ``` -Now, add to your `package.json` file the [JWT dependency](https://github.com/auth0/node-jsonwebtoken#readme): +The response will be `200 OK` for this request: -```json -"jsonwebtoken": "^8.5.1" +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache ``` -Next, in your code require the [JWT dependency](https://github.com/auth0/node-jsonwebtoken#readme): +*If you use an invalid or missing token, the server will respond with `401 Unauthorized`.* + +#### *3. Approov Token Binding Check* + +- The client sends two headers on authenticated API calls: + - `Approov-Token` + - `Authorization` – your auth token value (e.g., `ExampleAuthToken==`) +- The server verifies the token and ensures that the bound value matches what the app used. +- Prevents token replay - the Approov token cannot be reused or stolen for another session. +- **Use case:** Stronger protection for authenticated API calls tied to a specific user or device. + +***The following example shows how the API responds when an Approov token with binding is required.*** -```javascript -const jwt = require('jsonwebtoken') +*Generate a valid Approov token bound to the `Authorization` header:* + +```bash +approov token -setDataHashInToken ExampleAuthToken== -genExample example.com +``` + +*Use the generated token with binding in the Approov-Token and Authorization headers when calling the /token-binding endpoint.* + +```bash +curl -iX GET http://localhost:8111/token-binding \ + -H "Approov-Token: valid_approov_token_here" \ + -H "Authorization: ExampleAuthToken==" ``` -Now, grab the Approov secret into a constant: +The response will be `200 OK` for this request: -```javascript -const dotenv = require('dotenv').config() -const approovBase64Secret = dotenv.parsed.APPROOV_BASE64_SECRET; -const approovSecret = Buffer.from(approovBase64Secret, 'base64') +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache ``` -Next, verify the Approov token: +*If you use an invalid or missing header or token, the server will respond with `401 Unauthorized`.* -```javascript -const verifyApproovToken = function(req) { +#### Approov Token Binding Check with Two Different Bound Values - const approovToken = req.headers['approov-token'] +- The client sends three headers on authenticated API calls: + - `Approov-Token` + - `Authorization` + - `Session-Id` It is combined with the `Authorization` header to create a stronger binding. +- Both are included in the hash inside the Approov token. This means the server verifies a single hash that covers both authentication credentials. +- **Use case:** Stronger protection then single binding by tying both headers together. - if (!approovToken) { - // You may want to add some logging here. - return false - } +***The following example shows how the API responds when an Approov token with two bindings is required.*** - // Decode the token with strict verification of the signature (['HS256']) to - // prevent against the `none` algorithm attack. - return jwt.verify(approovToken, approovSecret, { algorithms: ['HS256'] }, function(err, decoded) { +*Generate a valid Approov token bound to the `Authorization` and `Session-Id` headers:* - if (err) { - // You may want to add some logging here. - return false - } +```bash +approov token -setDataHashInToken ExampleAuthToken==Session-123 -genExample example.com +``` - // The Approov token was successfully verified. We will add the claims to the - // request object to allow further use of them during the request processing. - req.approovTokenClaims = decoded +*Use the generated token with two bindings in the Approov-Token and Authorization headers when calling the `/token-double-binding` endpoint.* - return true - }) -} +```bash +curl -iX GET http://localhost:8111/token-double-binding \ + -H "Approov-Token: valid_approov_token_here" \ + -H "Authorization: ExampleAuthToken==" \ + -H "Session-Id: Session-123" ``` -Finally, invoke the check for each API endpoint you want to protect with Approov: +The response will be `200 OK` for this request. -```javascript -if (!verifyApproovToken(req)) { - res.statusCode = 401 - res.end(JSON.stringify({})) - return -} +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache ``` -Not enough details in the bare bones quickstart? No worries, check the [detailed quickstarts](QUICKSTARTS.md) that contain a more comprehensive set of instructions, including how to test the Approov integration. +*If you use an invalid or missing header or token, the server will respond with `401 Unauthorized`.* +## Approov Message Signing -## More Information +If your Approov account enables HTTP Message Signatures, this server verifies exactly one signature +mode at a time (configured by env): -* [Approov Overview](OVERVIEW.md) -* [Detailed Quickstarts](QUICKSTARTS.md) -* [Examples](EXAMPLES.md) -* [Testing](TESTING.md) +- `APPROOV_ENABLE_ACCOUNT_SIGNATURE=false` (default): **install-only** mode. +- `APPROOV_ENABLE_ACCOUNT_SIGNATURE=true`: **account-only** mode (install signature disabled). -### System Clock +- **Install message signing** uses the `ipk` claim inside the Approov token. When the claim is + present, requests must include a `Signature` and `Signature-Input` header with an `install` + signature entry, signed with the install key. +- **Account message signing** is enabled by setting one of: + - `APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64URL` + - `APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64` + - `APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_RAW` + In account-only mode, requests must include an `account` signature entry and the token must + include an `mskid` claim. You can + optionally enforce an expected key id via `APPROOV_ACCOUNT_MESSAGE_SIGNING_KEY_ID`. -In order to correctly check for the expiration times of the Approov tokens is very important that the backend server is synchronizing automatically the system clock over the network with an authoritative time source. In Linux this is usually done with a NTP server. +Install signatures use `ecdsa-p256-sha256`. Account signatures use `hmac-sha256`. +Both must include `created` and `expires` parameters. You can +adjust the allowed clock skew with `APPROOV_MESSAGE_SIGNING_TOLERANCE_SECONDS` (default `60`). +If a `Content-Digest` header is present, the server validates the body digest before verifying the +message signatures. -## Issues +To exercise the message signing flow with an `ipk` claim, run: -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-nodejs-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. +```bash +bash test-message-signing.sh +``` +## Enable or Disable Approov Protection -## Useful Links +When the example server is running on `localhost:8111`, you can toggle Approov protection with these commands: -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: +```bash +curl -X POST http://localhost:8111/approov/disable # disable the Approov service + +curl -X POST http://localhost:8111/approov/enable # enable the Approov service + +curl -X GET http://localhost:8111/approov-state # check current state +``` + +*You can rerun the tests with Approov disabled to observe how the application behaves when the Approov protection is ***no longer active***.* + +## Reporting Issues + +**Environments where the quickstart was tested:** +```text +* Runtime: node.js v25.2.1 +* Build Tool: npm 11.6.2 +``` + +If you encounter any problems while following this guide, or have any other concerns, please let us know by opening an issue [here](https://github.com/approov/quickstart-nodejs-token-check/issues) and we will be happy to assist you. + +## Useful Links -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) +* [Approov QuickStarts](https://approov.io/resource/quickstarts/) +* [Approov Docs](https://ext.approov.io/docs) +* [Approov Blog](https://approov.io/blog) * [Approov Resources](https://approov.io/resource/) * [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) +* [Approov Support](https://approov.io/info/technical-support) * [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) +* [Contact Us](https://approov.io/info/contact) diff --git a/TESTING.md b/TESTING.md deleted file mode 100644 index 5a0dfb8..0000000 --- a/TESTING.md +++ /dev/null @@ -1,43 +0,0 @@ -# Approov Integration Testing - -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps. - -## Testing the Approov Integration - -Each Quickstart has at their end a dedicated section for testing, that will walk you through the necessary steps to use the Approov CLI to generate valid and invalid tokens to test your Approov integration without the need to rely on the genuine mobile app(s) using your backend. - -* [Approov Token](/docs/APPROOV_TOKEN_QUICKSTART.md#test-your-approov-integration) test examples. -* [Approov Token Binding](/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md#test-your-approov-integration) test examples. - -### Testing with Postman - -A ready-to-use Postman collection can be found [here](https://raw.githubusercontent.com/approov/postman-collections/master/quickstarts/hello-world/hello-world.postman_collection.json). It contains a comprehensive set of example requests to send to the backend server for testing. The collection contains requests with valid and invalid Approov tokens, and with and without token binding. - -### Testing with Curl - -An alternative to the Postman collection is to use cURL to make the API requests. Check some examples [here](https://github.com/approov/postman-collections/blob/master/quickstarts/hello-world/hello-world.postman_curl_requests_examples.md). - -### The Dummy Secret - -The valid Approov tokens in the Postman collection and cURL requests examples were signed with a dummy secret that was generated with `openssl rand -base64 64 | tr -d '\n'; echo`, therefore not a production secret retrieved with `approov secret -get base64`, thus in order to use it you need to set the `APPROOV_BASE64_SECRET`, in the `.env` file for each [Approov integration example](/src/approov-protected-server), to the following value: `h+CX0tOzdAAR9l15bWAqvq7w9olk66daIH+Xk+IAHhVVHszjDzeGobzNnqyRze3lw/WVyWrc2gZfh3XXfBOmww==`. - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-nodejs-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index bf73e97..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,37 +0,0 @@ -version: "2.3" - -services: - - unprotected-server: - image: approov/nodejs:17.7.1 - build: ./ - networks: - - default - command: sh -c "npm install && npm start" - ports: - - ${HOST_IP:-127.0.0.1}:${HTTP_PORT:-8002}:${HTTP_PORT:-8002} - volumes: - - ./src/unprotected-server:/home/node/workspace - - approov-token-check: - image: approov/nodejs:17.7.1 - build: ./ - networks: - - default - command: sh -c "npm install && npm start" - ports: - - ${HOST_IP:-127.0.0.1}:${HTTP_PORT:-8002}:${HTTP_PORT:-8002} - volumes: - - ./src/approov-protected-server/token-check:/home/node/workspace - - approov-token-binding-check: - image: approov/nodejs:17.7.1 - build: ./ - networks: - - default - command: sh -c "npm install && npm start" - ports: - - ${HOST_IP:-127.0.0.1}:${HTTP_PORT:-8002}:${HTTP_PORT:-8002} - volumes: - - ./src/approov-protected-server/token-binding-check:/home/node/workspace - diff --git a/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md b/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md deleted file mode 100644 index 5e10c48..0000000 --- a/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md +++ /dev/null @@ -1,243 +0,0 @@ -# Approov Token Binding Quickstart - -This quickstart is for developers familiar with NodeJS who are looking for a quick intro into how they can add [Approov](https://approov.io) into an existing project. Therefore this will guide you through the necessary steps for adding Approov with token binding to an existing NodeJS API server. - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Approov Setup](#approov-setup) -* [Approov Token Check](#approov-token-binding-check) -* [Test the Approov Integration](#test-your-approov-integration) - - -## Why? - -To lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product/) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -For more background, see the [Approov Overview](/OVERVIEW.md#how-it-works) at the root of this repository. - -The main functionality for the Approov token binding check is in the file [src/approov-protected-server/token-binding-check/hello-server-protected.js](/src/approov-protected-server/token-binding-check/hello-server-protected.js). Take a look at the `verifyApproovToken()` and `verifyApproovTokenBinding()` functions to see the simple code for the checks. - -[TOC](#toc---table-of-contents) - - -## Requirements - -To complete this quickstart you will need both the NodeJS and the Approov CLI tool installed. - -* NodeJS - Follow the official installation instructions from [here](https://nodejs.org/en/download/) -* Approov CLI - Follow our [installation instructions](https://approov.io/docs/latest/approov-installation/#approov-tool) and read more about each command and its options in the [documentation reference](https://approov.io/docs/latest/approov-cli-tool-reference/) - -[TOC](#toc---table-of-contents) - - -## Approov Setup - -To use Approov with the NodeJS API server we need a small amount of configuration. First, Approov needs to know the API domain that will be protected. Second, the NodeJS API server needs the Approov Base64 encoded secret that will be used to verify the tokens generated by the Approov cloud service. - -### Configure API Domain - -Approov needs to know the domain name of the API for which it will issue tokens. - -Add it with: - -```bash -approov api -add your.api.domain.com -``` - -> **NOTE:** By default a symmetric key (HS256) is used to sign the Approov token on a valid attestation of the mobile app for each API domain it's added with the Approov CLI, so that all APIs will share the same secret and the backend needs to take care to keep this secret secure. -> -> A more secure alternative is to use asymmetric keys (RS256 or others) that allows for a different keyset to be used on each API domain and for the Approov token to be verified with a public key that can only verify, but not sign, Approov tokens. -> -> To implement the asymmetric key you need to change from using the symmetric HS256 algorithm to an asymmetric algorithm, for example RS256, that requires you to first [add a new key](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-new-key), and then specify it when [adding each API domain](https://approov.io/docs/latest/approov-usage-documentation/#keyset-key-api-addition). Please visit [Managing Key Sets](https://approov.io/docs/latest/approov-usage-documentation/#managing-key-sets) on the Approov documentation for more details. - -Adding the API domain also configures the [dynamic certificate pinning](https://approov.io/docs/latest/approov-usage-documentation/#dynamic-pinning) setup, out of the box. - -> **NOTE:** By default the pin is extracted from the public key of the leaf certificate served by the domain, as visible to the box issuing the Approov CLI command and the Approov servers. - -### Approov Secret - -Approov tokens are signed with a symmetric secret. To verify tokens, we need to grab the secret using the [Approov secret command](https://approov.io/docs/latest/approov-cli-tool-reference/#secret-command) and plug it into the NodeJS API server environment to check the signatures of the [Approov Tokens](https://www.approov.io/docs/latest/approov-usage-documentation/#approov-tokens) that it processes. - -Retrieve the Approov secret with: - -```bash -approov secret -get base64 -``` - -> **NOTE:** The `approov secret` command requires an [administration role](https://approov.io/docs/latest/approov-usage-documentation/#account-access-roles) to execute successfully. - -#### Set the Approov Secret - -Open the `.env` file and add the Approov secret to the var: - -```bash -APPROOV_BASE64_SECRET=approov_base64_secret_here -``` - -[TOC](#toc---table-of-contents) - - -## Approov Token Check - -To check the Approov token we will use the [auth0/node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken#readme) package, but you are free to use another one of your preference. - -Add this functions to your code: - -```javascript -const verifyApproovToken = function(req) { - - const approovToken = req.headers['approov-token'] - - if (!approovToken) { - // You may want to add some logging here. - return false - } - - // decode token, verify secret and check exp - return jwt.verify(approovToken, approovSecret, { algorithms: ['HS256'] }, function(err, decoded) { - - if (err) { - // You may want to add some logging here. - return false - } - - // The Approov token was successfully verified. We will add the claims to - // the request object to allow further use of them during the request - // processing. - req.approovTokenClaims = decoded - - return true - }) -} - -const verifyApproovTokenBinding = function(req) { - - if (!("pay" in req.approovTokenClaims)) { - // You may want to add some logging here. - return false - } - - // The Approov token claims is added to the request object on a successful - // Approov token verification. See `verifyApproovToken()` function. - token_binding_claim = req.approovTokenClaims.pay - - // We use here the Authorization token, but feel free to use another header, - // but you need to bind this header to the Approov token in the mobile app. - token_binding_header = req.headers['authorization'] - - if (!token_binding_header) { - // You may want to add some logging here. - return false - } - - // We need to hash and base64 encode the token binding header, because thats - // how it was included in the Approov token on the mobile app. - const token_binding_header_encoded = crypto.createHash('sha256').update(token_binding_header, 'utf-8').digest('base64') - - return token_binding_claim === token_binding_header_encoded -} -``` - -Now you just need to invoke it from the endpoint you want to protected: - -```javascript -if (!verifyApproovToken(req)) { - res.statusCode = 401 - res.end(JSON.stringify({})) - return - } - - if (!verifyApproovTokenBinding(req)) { - res.statusCode = 401 - res.end(JSON.stringify({})) - return - } -``` - -> **NOTE:** When the Approov token validation fails we return a `401` with an empty body, because we don't want to give clues to an attacker about the reason the request failed, and you can go even further by returning a `400`. - -A full working example for a simple Hello World server can be found at [src/approov-protected-server/token-binding-check](/src/approov-protected-server/token-binding-check). - -[TOC](#toc---table-of-contents) - - -## Test your Approov Integration - -The following examples below use cURL, but you can also use the [Postman Collection](/README.md#testing-with-postman) to make the API requests. Just remember that you need to adjust the urls and tokens defined in the collection to match your deployment. Alternatively, the above README also contains instructions for using the preset _dummy_ secret to test your Approov integration. - -#### With Valid Approov Tokens - -Generate a valid token example from the Approov Cloud service: - -```bash -approov token -setDataHashInToken 'Bearer authorizationtoken' -genExample your.api.domain.com -``` - -Then make the request with the generated token: - -```bash -curl -i --request GET 'https://your.api.domain.com/v1/shapes' \ - --header 'Authorization: Bearer authorizationtoken' \ - --header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE' -``` - -The request should be accepted. For example: - -```text -HTTP/2 200 - -... - -{"message": "Hello, World!"} -``` - -#### With Invalid Approov Tokens - -##### No Authorization Token - -Let's just remove the Authorization header from the request: - -```bash -curl -i --request GET 'https://your.api.domain.com/v1/shapes' \ - --header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE' -``` - -The above request should fail with an Unauthorized error. For example: - -```text -HTTP/2 401 - -... - -{} -``` - -##### Same Approov Token with a Different Authorization Token - -Make the request with the same generated token, but with another random authorization token: - -```bash -curl -i --request GET 'https://your.api.domain.com/v1/shapes' \ - --header 'Authorization: Bearer anotherauthorizationtoken' \ - --header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE' -``` - -The above request should also fail with an Unauthorized error. For example: - -```text -HTTP/2 401 - -... - -{} -``` - -[TOC](#toc---table-of-contents) diff --git a/docs/APPROOV_TOKEN_QUICKSTART.md b/docs/APPROOV_TOKEN_QUICKSTART.md deleted file mode 100644 index bc2439d..0000000 --- a/docs/APPROOV_TOKEN_QUICKSTART.md +++ /dev/null @@ -1,193 +0,0 @@ -# Approov Token Quickstart - -This quickstart is for developers familiar with NodeJS who are looking for a quick intro into how they can add [Approov](https://approov.io) into an existing project. Therefore this will guide you through the necessary steps for adding Approov to an existing NodeJS API server. - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Approov Setup](#approov-setup) -* [Approov Token Check](#approov-token-check) -* [Test the Approov Integration](#test-your-approov-integration) - - -## Why? - -To lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product/) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -For more background, see the [Approov Overview](/OVERVIEW.md#how-it-works) at the root of this repo. - -The main functionality for the Aroov token check is in the file [src/approov-protected-server/token-check/hello-server-protected.js](/src/approov-protected-server/token-check/hello-server-protected.js). Take a look at the `verifyApproovToken()` function to see the simple code for the check. - -[TOC](#toc---table-of-contents) - - -## Requirements - -To complete this quickstart you will need both the NodeJS and the Approov CLI tool installed. - -* NodeJS - Follow the official installation instructions from [here](https://nodejs.org/en/download/) -* Approov CLI - Follow our [installation instructions](https://approov.io/docs/latest/approov-installation/#approov-tool) and read more about each command and its options in the [documentation reference](https://approov.io/docs/latest/approov-cli-tool-reference/) - -[TOC](#toc---table-of-contents) - - -## Approov Setup - -To use Approov with the NodeJS API server we need a small amount of configuration. First, Approov needs to know the API domain that will be protected. Second, the NodeJS API server needs the Approov Base64 encoded secret that will be used to verify the tokens generated by the Approov cloud service. - -### Configure API Domain - -Approov needs to know the domain name of the API for which it will issue tokens. - -Add it with: - -```bash -approov api -add your.api.domain.com -``` - -> **NOTE:** By default a symmetric key (HS256) is used to sign the Approov token on a valid attestation of the mobile app for each API domain it's added with the Approov CLI, so that all APIs will share the same secret and the backend needs to take care to keep this secret secure. -> -> A more secure alternative is to use asymmetric keys (RS256 or others) that allows for a different keyset to be used on each API domain and for the Approov token to be verified with a public key that can only verify, but not sign, Approov tokens. -> -> To implement the asymmetric key you need to change from using the symmetric HS256 algorithm to an asymmetric algorithm, for example RS256, that requires you to first [add a new key](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-new-key), and then specify it when [adding each API domain](https://approov.io/docs/latest/approov-usage-documentation/#keyset-key-api-addition). Please visit [Managing Key Sets](https://approov.io/docs/latest/approov-usage-documentation/#managing-key-sets) on the Approov documentation for more details. - -Adding the API domain also configures the [dynamic certificate pinning](https://approov.io/docs/latest/approov-usage-documentation/#dynamic-pinning) setup, out of the box. - -> **NOTE:** By default the pin is extracted from the public key of the leaf certificate served by the domain, as visible to the box issuing the Approov CLI command and the Approov servers. - -### Approov Secret - -Approov tokens are signed with a symmetric secret. To verify tokens, we need to grab the secret using the [Approov secret command](https://approov.io/docs/latest/approov-cli-tool-reference/#secret-command) and plug it into the NodeJS API server environment to check the signatures of the [Approov Tokens](https://www.approov.io/docs/latest/approov-usage-documentation/#approov-tokens) that it processes. - -Retrieve the Approov secret with: - -```bash -approov secret -get base64 -``` - -> **NOTE:** The `approov secret` command requires an [administration role](https://approov.io/docs/latest/approov-usage-documentation/#account-access-roles) to execute successfully. - -#### Set the Approov Secret - -Open the `.env` file and add the Approov secret to the var: - -```bash -APPROOV_BASE64_SECRET=approov_base64_secret_here -``` - -[TOC](#toc---table-of-contents) - - -## Approov Token Check - -To check the Approov token we will use the [auth0/node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken#readme) package, but you are free to use another one of your preference. - -Add this function to your code: - -```javascript -const verifyApproovToken = function(req) { - - const approovToken = req.headers['approov-token'] - - if (!approovToken) { - // You may want to add some logging here. - return false - } - - // decode token, verify secret and check exp - return jwt.verify(approovToken, approovSecret, { algorithms: ['HS256'] }, function(err, decoded) { - - if (err) { - // You may want to add some logging here. - return false - } - - // The Approov token was successfully verified. We will add the claims to - // the request object to allow further use of them during the request - // processing. - req.approovTokenClaims = decoded - - return true - }) -} -``` - -Now you just need to invoke it from the endpoint you want to protected: - -```javascript -if (!verifyApproovToken(req)) { - res.statusCode = 401 - res.end(JSON.stringify({})) - return -} -``` - -> **NOTE:** When the Approov token validation fails we return a `401` with an empty body, because we don't want to give clues to an attacker about the reason the request failed, and you can go even further by returning a `400`. - -A full working example for a simple Hello World server can be found at [src/approov-protected-server/token-check](/src/approov-protected-server/token-check). - -[TOC](#toc---table-of-contents) - - -## Test your Approov Integration - -The following examples below use cURL, but you can also use the [Postman Collection](/README.md#testing-with-postman) to make the API requests. Just remember that you need to adjust the urls and tokens defined in the collection to match your deployment. Alternatively, the above README also contains instructions for using the preset _dummy_ secret to test your Approov integration. - -#### With Valid Approov Tokens - -Generate a valid token example from the Approov Cloud service: - -```bash -approov token -genExample your.api.domain.com -``` - -Then make the request with the generated token: - -```bash -curl -i --request GET 'https://your.api.domain.com' \ - --header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE' -``` - -The request should be accepted. For example: - -```text -HTTP/2 200 - -... - -{"message": "Hello, World!"} -``` - -#### With Invalid Approov Tokens - -Generate an invalid token example from the Approov Cloud service: - -```bash -approov token -type invalid -genExample your.api.domain.com -``` - -Then make the request with the generated token: - -```bash -curl -i --request GET 'https://your.api.domain.com' \ - --header 'Approov-Token: APPROOV_INVALID_TOKEN_EXAMPLE_HERE' -``` - -The above request should fail with an Unauthorized error. For example: - -```text -HTTP/2 401 - -... - -{} -``` - -[TOC](#toc---table-of-contents) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c51d547 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,15 @@ +{ + "name": "approov-node-token-check", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "approov-node-token-check", + "version": "1.0.0", + "engines": { + "node": ">=18.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3ceab8f --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "approov-node-token-check", + "version": "1.0.0", + "private": true, + "description": "Approov token and token binding verification example using Node.js core HTTP.", + "main": "ApproovApplication.js", + "scripts": { + "start": "node ApproovApplication.js", + "test": "bash test.sh" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/run-server.sh b/run-server.sh new file mode 100755 index 0000000..5a64feb --- /dev/null +++ b/run-server.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Host-side wrapper: sets FOLLOW_LOGS (default true) and hands execution to scripts/build.sh, +# which handles image build/run plus container log tailing. +set -euo pipefail + +FOLLOW_LOGS="${FOLLOW_LOGS:-true}" ./scripts/build.sh diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..9b1d9a8 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Dual-purpose orchestrator: on the host it builds/runs the Docker container, +# and inside the container it starts the application command with optional +# readiness checks and log attachment. +set -euo pipefail + +requirement_check() { # verify command exists on PATH + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + fail "Missing required command: ${cmd}" + fi +} +fail() { echo "ERROR: $*" >&2; exit 1; } # uniform error output + exit +info() { echo "info $*"; } # lightweight logging helper + +# Globals configured via environment overrides +RUN_MODE="${RUN_MODE:-host}" # host orchestrator vs container entrypoint +APP_START_CMD="${APP_START_CMD:-}" # command executed when inside container +FOLLOW_LOGS="${FOLLOW_LOGS:-true}" # toggle docker logs -f attachment +HOST_PORT="${HOST_PORT:-8111}" # host-facing port (e.g., http://localhost:3000) +WAIT_URL="${WAIT_URL:-http://localhost:${HOST_PORT}/approov-state}" # readiness probe target +WAIT_TIMEOUT="${WAIT_TIMEOUT:-60}" # how long to wait before failing readiness +WAIT_INTERVAL="${WAIT_INTERVAL:-2}" # delay between readiness checks +CONTAINER_PORT="${CONTAINER_PORT:-$HOST_PORT}" # container listener, defaults to host port +IMAGE_NAME="${IMAGE_NAME:-approov-quickstart-nodejs}" +CONTAINER_NAME="${CONTAINER_NAME:-approov-quickstart-nodejs-app}" +ENV_FILE="${ENV_FILE:-.env}" +RUNTIME_BIN_DIR="${RUNTIME_BIN_DIR:-}" # optional runtime-specific bin path + +in_container() { + [[ "$RUN_MODE" == "container" ]] || [[ -f "/.dockerenv" ]] +} + +if in_container; then + [[ -n "$APP_START_CMD" ]] || fail "APP_START_CMD must be provided to run the server" + if [[ -n "$RUNTIME_BIN_DIR" ]]; then + export PATH="${RUNTIME_BIN_DIR}:$PATH" # e.g., RUNTIME_BIN_DIR=/usr/local/go/bin to expose runtime binaries for golang + fi + info "Container starting application: ${APP_START_CMD}" + exec bash -c "$APP_START_CMD" +fi + +requirement_check docker +if ! command -v approov >/dev/null 2>&1; then + info "Approov CLI not found; continuing without CLI checks (tests may need it)" +fi + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found. Run cp .env.example .env first." +[[ -f Dockerfile ]] || fail "Dockerfile not found in $(pwd)" + +if docker ps -a --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then + info "Removing stale container ${CONTAINER_NAME}" + docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true +fi + +info "Building ${IMAGE_NAME}" +docker build -t "$IMAGE_NAME" . || fail "Docker build failed" + +info "Starting ${CONTAINER_NAME} on host port ${HOST_PORT}, container port ${CONTAINER_PORT}" +docker run -d \ + --name "$CONTAINER_NAME" \ + --env-file "$ENV_FILE" \ + -e RUN_MODE=container \ + -p "${HOST_PORT}:${CONTAINER_PORT}" \ + "$IMAGE_NAME" >/dev/null || fail "Failed to start container ${CONTAINER_NAME}" + +wait_for_service() { + local url="$1" timeout="$2" interval="$3" elapsed=0 + info "Waiting for application to become ready at ${url}" + until curl -fsS "$url" >/dev/null 2>&1; do + sleep "$interval" + elapsed=$((elapsed + interval)) + if (( elapsed >= timeout )); then + fail "Application did not become ready within ${timeout}s (last url: ${url})" + fi + done + info "Application is ready" +} + +wait_for_service "$WAIT_URL" "$WAIT_TIMEOUT" "$WAIT_INTERVAL" + +if [[ "$FOLLOW_LOGS" == "true" ]]; then + info "Container logs (Ctrl+C to stop):" + docker logs -f "$CONTAINER_NAME" +else + info "Skipping container logs attachment." +fi diff --git a/scripts/install-prerequisites.sh b/scripts/install-prerequisites.sh new file mode 100755 index 0000000..96b4fd4 --- /dev/null +++ b/scripts/install-prerequisites.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Installs base OS dependencies (and optionally language runtimes) required for +# the quickstart image; intended for use inside the Docker build context. +set -euo pipefail + +requirement_check() { # helper to verify command availability + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + fail "Missing required command: ${cmd}" + fi +} +fail() { echo "ERROR: $*" >&2; exit 1; } # helper to abort with message +info() { echo "info $*"; } # helper to print informational logs + +# ensure apt operations have privileges +[[ "$(id -u)" -eq 0 ]] || fail "Run this script as root (or via sudo) inside the build context." + +# base utilities required by every quickstart +apt-get update +apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + build-essential \ + git + +# Optional runtime install; set INSTALL_LANGUAGE_RUNTIME=true per quickstart and fill commands below. +INSTALL_LANGUAGE_RUNTIME_FLAG="${INSTALL_LANGUAGE_RUNTIME:-false}" + +if [[ "$INSTALL_LANGUAGE_RUNTIME_FLAG" == "true" ]]; then + info "Installing language/runtime specific dependencies" + # TODO: add language-specific install commands (apt packages, curl tarballs, etc.) +fi diff --git a/src/approov-protected-server/token-binding-check/.env.example b/src/approov-protected-server/token-binding-check/.env.example deleted file mode 100644 index bd6a3fc..0000000 --- a/src/approov-protected-server/token-binding-check/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -# The port for the Quickstart Postman collection and cURL examples is 8002 -HTTP_PORT=8002 - -# Setting to 0.0.0.0 is only necessary to run from inside a docker container -SERVER_HOSTNAME=0.0.0.0 - -# For production usage the secret is always retrieved with the Approov CLI tool, that can be also used to generate valid -# tokens for testing purposes. Check docs at https://approov.io/docs/v2.1/approov-cli-tool-reference/#token-commands. -# -# But if you don't have the Approov CLI tool, you can still test the backend with Postman or similar, by creating a -# secret with `openssl rand -base64 64 | tr -d '\n'; echo`, and afterwards you can use jwt.io to create the JWT token. -APPROOV_BASE64_SECRET=approov_base64_secret_here diff --git a/src/approov-protected-server/token-binding-check/README.md b/src/approov-protected-server/token-binding-check/README.md deleted file mode 100644 index 112c9cb..0000000 --- a/src/approov-protected-server/token-binding-check/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# Approov Token Binding Integration Example - -This Approov integration example is from where the code example for the [Approov token binding check quickstart](/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md) is extracted, and you can use it as a playground to better understand how simple and easy it is to implement [Approov](https://approov.io) in a NodeJS API server. - - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Try the Approov Integration Example](#try-the-approov-integration-example) - - -## Why? - -To lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product/) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -The NodeJS server is very simple and is defined in the file [src/approov-protected-server/token-binding-check/hello-server-protected.js](src/approov-protected-server/token-binding-check/hello-server-protected.js). Take a look at the `verifyApproovToken()` and `verifyApproovTokenBinding()` functions to see the simple code for the checks. - -For more background on Approov, see the [Approov Overview](/OVERVIEW.md#how-it-works) at the root of this repo. - -[TOC](#toc---table-of-contents) - - -## Requirements - -To run this example you will need to have NodeJS installed. If you don't have then please follow the official installation instructions from [here](https://nodejs.org/en/download/) to download and install it. - -[TOC](#toc---table-of-contents) - - -## Try the Approov Integration Example - -First, you need to set the dummy secret in the `src/approov-protected-server/token-binding-check/.env` file as explained [here](/TESTING.md#the-dummy-secret). - -Second, you need to install the dependencies. From the `src/approov-protected-server/token-binding-check` folder execute: - -```bash -npm install -``` - -Now, you can run this example from the `src/approov-protected-server/token-binding-check` folder with: - -```bash -npm start -``` - -Next, you can test that it works with: - -```bash -curl -iX GET 'http://localhost:8002' -``` - -The response will be a `401` unauthorized request: - -```text -HTTP/1.1 401 Unauthorized -Content-Type: application/json -Date: Wed, 06 Apr 2022 12:01:58 GMT -Connection: keep-alive -Keep-Alive: timeout=5 -Content-Length: 2 - -{} -``` - -The reason you got a `401` is because the Approoov token isn't provided in the headers of the request. - -Finally, you can test that the Approov integration example works as expected with this [Postman collection](/TESTING.md#testing-with-postman) or with some cURL requests [examples](/TESTING.md#testing-with-curl). - -[TOC](#toc---table-of-contents) - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-nodejs-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - - -[TOC](#toc---table-of-contents) - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) - -[TOC](#toc---table-of-contents) diff --git a/src/approov-protected-server/token-binding-check/hello-server-protected.js b/src/approov-protected-server/token-binding-check/hello-server-protected.js deleted file mode 100644 index e065a6a..0000000 --- a/src/approov-protected-server/token-binding-check/hello-server-protected.js +++ /dev/null @@ -1,101 +0,0 @@ -const http = require('http'); -const dotenv = require('dotenv').config() -const jwt = require('jsonwebtoken') -const crypto = require('crypto') - -if (dotenv.error) { - console.debug('FAILED TO PARSE `.env` FILE | ' + dotenv.error) -} - -// To run in a docker container add to the .env file `SERVER_HOSTNAME=0.0.0.0`. -const hostname = dotenv.parsed.SERVER_HOSTNAME || 'localhost'; - -// The port for the Quickstart Postman collection and cURL examples is 8002 -const port = dotenv.parsed.HTTP_PORT || 8002; - -const approovBase64Secret = dotenv.parsed.APPROOV_BASE64_SECRET; - -const approovSecret = Buffer.from(approovBase64Secret, 'base64') - -const verifyApproovToken = function(req) { - - const approovToken = req.headers['approov-token'] - - if (!approovToken) { - // You may want to add some logging here. - console.debug("Missing Approov token") - return false - } - - // decode token, verify secret and check exp - return jwt.verify(approovToken, approovSecret, { algorithms: ['HS256'] }, function(err, decoded) { - - if (err) { - // You may want to add some logging here. - console.debug("Approov token error: " + err) - return false - } - - // The Approov token was successfully verified. We will add the claims to - // the request object to allow further use of them during the request - // processing. - req.approovTokenClaims = decoded - - return true - }) -} - -const verifyApproovTokenBinding = function(req) { - - if (!("pay" in req.approovTokenClaims)) { - // You may want to add some logging here. - console.debug("Approov token binding error: the `pay` claim missing in the payload") - return false - } - - // The Approov token claims is added to the request object on a successful - // Approov token verification. See `verifyApproovToken()` function. - token_binding_claim = req.approovTokenClaims.pay - - // We use here the Authorization token, but feel free to use another header, - // but you need to bind this header to the Approov token in the mobile app. - token_binding_header = req.headers['authorization'] - - if (!token_binding_header) { - // You may want to add some logging here. - console.debug("Approov token binding error: missing the token binding header in the request") - return false - } - - // We need to hash and base64 encode the token binding header, because thats - // how it was included in the Approov token on the mobile app. - const token_binding_header_encoded = crypto.createHash('sha256').update(token_binding_header, 'utf-8').digest('base64') - - return token_binding_claim === token_binding_header_encoded -} - -const server = http.createServer((req, res) => { - console.debug("<--- / GET") - - res.setHeader('Content-Type', 'application/json'); - - if (!verifyApproovToken(req)) { - res.statusCode = 401 - res.end(JSON.stringify({})) - return - } - - if (!verifyApproovTokenBinding(req)) { - console.debug("Approov token binding error: invalid token binding") - res.statusCode = 401 - res.end(JSON.stringify({})) - return - } - - res.statusCode = 200; - res.end(JSON.stringify({message: "Hello, World!"})) -}); - -server.listen(port, hostname, () => { - console.log(`Approov protected server running at http://${hostname}:${port}/`); -}); diff --git a/src/approov-protected-server/token-binding-check/package-lock.json b/src/approov-protected-server/token-binding-check/package-lock.json deleted file mode 100644 index e81c7bc..0000000 --- a/src/approov-protected-server/token-binding-check/package-lock.json +++ /dev/null @@ -1,218 +0,0 @@ -{ - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "dotenv": "^8.2.0", - "jsonwebtoken": "^9.0.0" - }, - "devDependencies": {} - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "engines": { - "node": ">=10" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "dependencies": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - }, - "dependencies": { - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "requires": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - } - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } -} diff --git a/src/approov-protected-server/token-binding-check/package.json b/src/approov-protected-server/token-binding-check/package.json deleted file mode 100644 index cc46170..0000000 --- a/src/approov-protected-server/token-binding-check/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "description": "Approov quickstart for NodeJS without using a framework.", - "main": "src/index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node hello-server-protected.js" - }, - "author": "paulos@criticalblue.com", - "license": "MIT", - "dependencies": { - "dotenv": "^8.2.0", - "jsonwebtoken": "^9.0.0" - }, - "devDependencies": {} -} diff --git a/src/approov-protected-server/token-check/.env.example b/src/approov-protected-server/token-check/.env.example deleted file mode 100644 index bd6a3fc..0000000 --- a/src/approov-protected-server/token-check/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -# The port for the Quickstart Postman collection and cURL examples is 8002 -HTTP_PORT=8002 - -# Setting to 0.0.0.0 is only necessary to run from inside a docker container -SERVER_HOSTNAME=0.0.0.0 - -# For production usage the secret is always retrieved with the Approov CLI tool, that can be also used to generate valid -# tokens for testing purposes. Check docs at https://approov.io/docs/v2.1/approov-cli-tool-reference/#token-commands. -# -# But if you don't have the Approov CLI tool, you can still test the backend with Postman or similar, by creating a -# secret with `openssl rand -base64 64 | tr -d '\n'; echo`, and afterwards you can use jwt.io to create the JWT token. -APPROOV_BASE64_SECRET=approov_base64_secret_here diff --git a/src/approov-protected-server/token-check/README.md b/src/approov-protected-server/token-check/README.md deleted file mode 100644 index 3ed4595..0000000 --- a/src/approov-protected-server/token-check/README.md +++ /dev/null @@ -1,101 +0,0 @@ -# Approov Token Integration Example - -This Approov integration example is from where the code example for the [Approov token check quickstart](/docs/APPROOV_TOKEN_QUICKSTART.md) is extracted, and you can use it as a playground to better understand how simple and easy it is to implement [Approov](https://approov.io) in a NodeJS API server. - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Try the Approov Integration Example](#try-the-approov-integration-example) - - -## Why? - -To lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product/) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -The NodeJS server is very simple and is defined in the file [src/approov-protected-server/token-check/hello-server-protected.js](src/approov-protected-server/token-check/hello-server-protected.js). Take a look at the `verifyApproovToken()` function to see the simple code for the check. - -For more background on Approov, see the [Approov Overview](/OVERVIEW.md#how-it-works) at the root of this repo. - -[TOC](#toc---table-of-contents) - - -## Requirements - -To run this example you will need to have NodeJS installed. If you don't have then please follow the official installation instructions from [here](https://nodejs.org/en/download/) to download and install it. - -[TOC](#toc---table-of-contents) - - -## Try the Approov Integration Example - -First, you need to set the dummy secret in the `src/approov-protected-server/token-check/.env` file as explained [here](/TESTING.md#the-dummy-secret). - -Second, you need to install the dependencies. From the `src/approov-protected-server/token-check` folder execute: - -```bash -npm install -``` - -Now, you can run this example from the `src/approov-protected-server/token-check` folder with: - -```bash -npm start -``` - -Next, you can test that it works with: - -```bash -curl -iX GET 'http://localhost:8002' -``` - -The response will be a `401` unauthorized request: - -```text -HTTP/1.1 401 Unauthorized -Content-Type: application/json -Date: Wed, 06 Apr 2022 12:01:58 GMT -Connection: keep-alive -Keep-Alive: timeout=5 -Content-Length: 2 - -{} -``` - -The reason you got a `401` is because the Approoov token isn't provided in the headers of the request. - -Finally, you can test that the Approov integration example works as expected with this [Postman collection](/TESTING.md#testing-with-postman) or with some cURL requests [examples](/TESTING.md#testing-with-curl). - -[TOC](#toc---table-of-contents) - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-nodejs-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - - -[TOC](#toc---table-of-contents) - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) - -[TOC](#toc---table-of-contents) diff --git a/src/approov-protected-server/token-check/hello-server-protected.js b/src/approov-protected-server/token-check/hello-server-protected.js deleted file mode 100644 index e887afa..0000000 --- a/src/approov-protected-server/token-check/hello-server-protected.js +++ /dev/null @@ -1,66 +0,0 @@ -const http = require('http'); -const dotenv = require('dotenv').config() -const jwt = require('jsonwebtoken') - - -if (dotenv.error) { - console.debug('FAILED TO PARSE `.env` FILE | ' + dotenv.error) -} - -// To run in a docker container add to the .env file `SERVER_HOSTNAME=0.0.0.0`. -const hostname = dotenv.parsed.SERVER_HOSTNAME || 'localhost'; - -// The port for the Quickstart Postman collection and cURL examples is 8002 -const port = dotenv.parsed.HTTP_PORT || 8002; - -const approovBase64Secret = dotenv.parsed.APPROOV_BASE64_SECRET; - -const approovSecret = Buffer.from(approovBase64Secret, 'base64') - -const verifyApproovToken = function(req) { - - const approovToken = req.headers['approov-token'] - - if (!approovToken) { - // You may want to add some logging here. - console.debug("Missing Approov token") - return false - } - - // decode token, verify secret and check exp - return jwt.verify(approovToken, approovSecret, { algorithms: ['HS256'] }, function(err, decoded) { - - if (err) { - // You may want to add some logging here. - console.debug("Approov token error: " + err) - return false - } - - // The Approov token was successfully verified. We will add the claims to - // the request object to allow further use of them during the request - // processing. - req.approovTokenClaims = decoded - - return true - }) -} - -const server = http.createServer((req, res) => { - - console.debug("<--- / GET") - - res.setHeader('Content-Type', 'application/json'); - - if (!verifyApproovToken(req)) { - res.statusCode = 401 - res.end(JSON.stringify({})) - return - } - - res.statusCode = 200; - res.end(JSON.stringify({message: "Hello, World!"})) -}); - -server.listen(port, hostname, () => { - console.log(`Approov protected server running at http://${hostname}:${port}/`); -}); diff --git a/src/approov-protected-server/token-check/package-lock.json b/src/approov-protected-server/token-check/package-lock.json deleted file mode 100644 index e81c7bc..0000000 --- a/src/approov-protected-server/token-check/package-lock.json +++ /dev/null @@ -1,218 +0,0 @@ -{ - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "dotenv": "^8.2.0", - "jsonwebtoken": "^9.0.0" - }, - "devDependencies": {} - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "engines": { - "node": ">=10" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "dependencies": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - }, - "dependencies": { - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "requires": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - } - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } -} diff --git a/src/approov-protected-server/token-check/package.json b/src/approov-protected-server/token-check/package.json deleted file mode 100644 index cc46170..0000000 --- a/src/approov-protected-server/token-check/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "description": "Approov quickstart for NodeJS without using a framework.", - "main": "src/index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node hello-server-protected.js" - }, - "author": "paulos@criticalblue.com", - "license": "MIT", - "dependencies": { - "dotenv": "^8.2.0", - "jsonwebtoken": "^9.0.0" - }, - "devDependencies": {} -} diff --git a/src/unprotected-server/.env.example b/src/unprotected-server/.env.example deleted file mode 100644 index 85c98af..0000000 --- a/src/unprotected-server/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# The port for the Quickstart Postman collection and cURL examples is 8002 -HTTP_PORT=8002 - -# Setting to 0.0.0.0 is only necessary to run from inside a docker container -SERVER_HOSTNAME=0.0.0.0 diff --git a/src/unprotected-server/README.md b/src/unprotected-server/README.md deleted file mode 100644 index 9af372d..0000000 --- a/src/unprotected-server/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# Unprotected Server Example - -The unprotected example is the base reference to build the [Approov protected servers](/src/approov-protected-server/). This a very basic Hello World server. - - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Try It](#try-it) - - -## Why? - -To be the starting building block for the [Approov protected servers](/src/approov-protected-server/), that will show you how to lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product/) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -The NodeJS server is very simple and is defined in the file [src/unprotected-server/hello-server-unprotected.js](/src/unprotected-server/hello-server-unprotected.js). - -The server only replies to the endpoint `/` with the message: - -```json -{"message": "Hello, World!"} -``` - -[TOC](#toc---table-of-contents) - - -## Requirements - -To run this example you will need to have NodeJS installed. If you don't have then please follow the official installation instructions from [here](https://nodejs.org/en/download/) to download and install it. - -[TOC](#toc---table-of-contents) - - -## Try It - -First install the dependencies. From the `src/unprotected-server` folder execute: - -```text -npm install -``` - -Now, you can run this example from the `src/unprotected-server` folder with: - -```text -npm start -``` - -Finally, you can test that it works with: - -```text -curl -iX GET 'http://localhost:8002' -``` - -The response will be: - -```text -HTTP/1.1 200 OK -Content-Type: application/json -Date: Tue, 08 Sep 2020 16:05:53 GMT -Content-Length: 28 - -{"message": "Hello, World!"} -``` - -[TOC](#toc---table-of-contents) - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-nodejs-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - - -[TOC](#toc---table-of-contents) - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) - -[TOC](#toc---table-of-contents) diff --git a/src/unprotected-server/hello-server-unprotected.js b/src/unprotected-server/hello-server-unprotected.js deleted file mode 100644 index 1980eb1..0000000 --- a/src/unprotected-server/hello-server-unprotected.js +++ /dev/null @@ -1,22 +0,0 @@ -const http = require('http'); -const dotenv = require('dotenv').config() - -if (dotenv.error) { - console.debug('FAILED TO PARSE `.env` FILE | ' + dotenv.error) -} - -// To run in a docker container add to the .env file `SERVER_HOSTNAME=0.0.0.0`. -const hostname = dotenv.parsed.SERVER_HOSTNAME || 'localhost'; - -// The port for the Quickstart Postman collection and cURL examples is 8002 -const port = dotenv.parsed.HTTP_PORT || 8002; - -const server = http.createServer((req, res) => { - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({message: "Hello, World!"})) -}); - -server.listen(port, hostname, () => { - console.log(`Server running at http://${hostname}:${port}/`); -}); diff --git a/src/unprotected-server/package-lock.json b/src/unprotected-server/package-lock.json deleted file mode 100644 index ed1116b..0000000 --- a/src/unprotected-server/package-lock.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "dotenv": "^8.2.0" - }, - "devDependencies": {} - }, - "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "engines": { - "node": ">=10" - } - } - }, - "dependencies": { - "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" - } - } -} diff --git a/src/unprotected-server/package.json b/src/unprotected-server/package.json deleted file mode 100644 index 2c63656..0000000 --- a/src/unprotected-server/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "description": "Approov quickstart for NodeJS without using a framework.", - "main": "src/index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node hello-server-unprotected.js" - }, - "author": "paulos@criticalblue.com", - "license": "MIT", - "dependencies": { - "dotenv": "^8.2.0" - }, - "devDependencies": {} -} diff --git a/test-message-signing.sh b/test-message-signing.sh new file mode 100755 index 0000000..c140cf4 --- /dev/null +++ b/test-message-signing.sh @@ -0,0 +1,320 @@ +#!/usr/bin/env bash +set -o errexit +set -o nounset +set -o pipefail + +####################################### +# Approov message signing tests. +# +# Description: +# Generates a valid Approov token with an ipk claim and a matching +# HTTP message signature, then exercises /token-check-signature with +# curl requests. +# +# Dependencies: +# - bash +# - curl +# - node (>=18) +# +# Environment: +# BASE_URL: +# Base URL of the API under test. Default: http://localhost:8111 +# TOKDIR: +# Directory where temporary token files are stored. Default: .config +# APPROOV_BASE64URL_SECRET: +# Approov shared secret (base64url). +####################################### + +readonly BASE_URL="${BASE_URL:-http://localhost:8111}" +readonly TOKDIR="${TOKDIR:-.config}" +readonly LOGDIR="${TOKDIR}/logs" +readonly LOGFILE="${LOGDIR}/message-signing-$(date '+%Y-%m-%d_%H-%M-%S').log" + +success_code=200 +failure_code=401 +is_approov_disabled=false + +err() { + echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2 +} + +requirement_check() { + local cmd="$1" + if ! command -v "${cmd}" >/dev/null 2>&1; then + err "Missing required command: ${cmd}" + exit 1 + fi +} + +load_env() { + if [[ ! -f ".env" ]]; then + return + fi + + while IFS= read -r line || [[ -n "${line}" ]]; do + local trimmed + trimmed="$(echo "${line}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + if [[ -z "${trimmed}" || "${trimmed}" == \#* ]]; then + continue + fi + if [[ "${trimmed}" != *"="* ]]; then + continue + fi + local key="${trimmed%%=*}" + local value="${trimmed#*=}" + key="$(echo "${key}" | sed -e 's/[[:space:]]*$//')" + value="$(echo "${value}" | sed -e 's/^[[:space:]]*//')" + if [[ "${value}" =~ ^\".*\"$ || "${value}" =~ ^\'.*\'$ ]]; then + value="${value:1:${#value}-2}" + fi + export "${key}=${value}" + done < ".env" +} + +print_test_result() { + local name="$1" + local expected="$2" + local status="$3" + local resp="$4" + + local result="Failed" + if [[ "${status}" == "${expected}" ]]; then + result="Passed" + fi + + echo "${name}: ${result} (status: ${status}, expected: ${expected})" + + { + echo "Test: ${name}" + echo "Expected status: ${expected}" + echo "Actual status: ${status}" + if [[ "${is_approov_disabled}" == "false" ]]; then + echo "Approov State: enabled, token checks performed." + else + echo "Approov State: disabled, no checks performed." + fi + echo + echo "HTTP exchange:" + echo "${resp}" + echo + } >>"${LOGFILE}" 2>&1 +} + +run_test() { + local name="$1"; shift + local expected="$1"; shift + + local resp + local status + local curl_rc + + set +o errexit + resp="$(curl -i -s "$@")" + curl_rc=$? + set -o errexit + + if ((curl_rc != 0)); then + err "curl failed for ${name} (rc=${curl_rc})" + return "${curl_rc}" + fi + + status="$( + printf '%s\n' "${resp}" | + grep -m1 '^HTTP/' | + awk '{print $2}' + )" + + print_test_result "${name}" "${expected}" "${status}" "${resp}" +} + +generate_message_signature_payload() { + env BASE_URL="${BASE_URL}" node <<'NODE' +const crypto = require('crypto'); +const { httpbis, createSigner } = require('./vendor/http-message-signatures/lib'); + +function base64UrlDecode(value) { + const normalized = value.replace(/-/g, '+').replace(/_/g, '/'); + const padded = normalized.padEnd( + normalized.length + ((4 - (normalized.length % 4)) % 4), + '=' + ); + return Buffer.from(padded, 'base64'); +} + +function toBase64Url(value) { + return Buffer.from(JSON.stringify(value)).toString('base64url'); +} + +function signJwtHS256(secret, header, payload) { + const headerB64 = toBase64Url(header); + const payloadB64 = toBase64Url(payload); + const signingInput = `${headerB64}.${payloadB64}`; + const signature = crypto + .createHmac('sha256', secret) + .update(signingInput) + .digest('base64url'); + return `${signingInput}.${signature}`; +} + +const secretB64 = process.env.APPROOV_BASE64URL_SECRET; +if (!secretB64) { + console.error('APPROOV_BASE64URL_SECRET not set'); + process.exit(1); +} +const secret = base64UrlDecode(secretB64.trim()); + +const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', { + namedCurve: 'prime256v1', +}); +const ipk = publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); + +const now = Math.floor(Date.now() / 1000); +const header = { alg: 'HS256', typ: 'JWT' }; +const payload = { + aud: '', + exp: now + 600, + iat: now, + ip: '1.2.3.4', + did: 'ExampleApproovTokenDID==', + ipk, +}; +const token = signJwtHS256(secret, header, payload); + +const signer = createSigner(privateKey, 'ecdsa-p256-sha256'); +const request = { + method: 'GET', + url: `${process.env.BASE_URL}/token-check-signature`, + headers: { + 'approov-token': token, + }, +}; + +httpbis.signMessage( + { + key: signer, + name: 'install', + fields: ['@method', 'approov-token'], + }, + request +).then((signed) => { + const signatureHeader = signed.headers['signature'] || signed.headers['Signature']; + const signatureInputHeader = signed.headers['signature-input'] || signed.headers['Signature-Input']; + console.log(token); + console.log(signatureHeader); + console.log(signatureInputHeader); + console.log(ipk); +}).catch((error) => { + console.error(error.message || error); + process.exit(1); +}); +NODE +} + +main() { + requirement_check "curl" + requirement_check "node" + + load_env + + if [[ -z "${APPROOV_BASE64URL_SECRET:-}" ]]; then + err "APPROOV_BASE64URL_SECRET is not set (check .env)." + exit 1 + fi + + if [[ -n "${APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64URL:-}" || -n "${APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_BASE64:-}" || -n "${APPROOV_ACCOUNT_MESSAGE_SIGNING_SECRET_RAW:-}" || -n "${APPROOV_ACCOUNT_MESSAGE_SIGNING_KEY_ID:-}" ]]; then + err "Account message signing env is set; this script only signs the install key." + err "Unset it or extend the script to sign account signatures as well." + exit 1 + fi + + mkdir -p "${TOKDIR}" "${LOGDIR}" + + echo "Approov state check:" + local state_response + state_response="$(curl -i -s "${BASE_URL}/approov-state")" + local state_http_code + state_http_code="$( + printf '%s\n' "${state_response}" | + grep -m1 '^HTTP/' | + awk '{print $2}' + )" + + if [[ "${state_http_code}" != "200" || -z "${state_http_code}" ]]; then + err "Failed to get Approov state from ${BASE_URL}/approov-state (status=${state_http_code:-unknown})" + exit 1 + fi + + if grep -q '"approovEnabled":true' <<<"${state_response}"; then + echo " Approov service: ENABLED" + is_approov_disabled=false + else + echo " Approov service: DISABLED" + is_approov_disabled=true + failure_code=200 + fi + echo + + local generated=() + local line + while IFS= read -r line; do + generated+=("${line}") + done < <(generate_message_signature_payload) + + if [[ "${#generated[@]}" -lt 3 ]]; then + err "Failed to generate message signature payload." + exit 1 + fi + + local token="${generated[0]}" + local signature_header="${generated[1]}" + local signature_input_header="${generated[2]}" + + run_test \ + "Message signature - valid install signature" \ + "${success_code}" \ + -H "Approov-Token: ${token}" \ + -H "Signature: ${signature_header}" \ + -H "Signature-Input: ${signature_input_header}" \ + "${BASE_URL}/token-check-signature" + + run_test \ + "Message signature - missing signature headers" \ + "${failure_code}" \ + -H "Approov-Token: ${token}" \ + "${BASE_URL}/token-check-signature" + + local sig_prefix="install=:" + local sig_suffix=":" + local sig_b64="${signature_header#${sig_prefix}}" + sig_b64="${sig_b64%${sig_suffix}}" + local tampered_sig_b64="${sig_b64/a/b}" + if [[ "${tampered_sig_b64}" == "${sig_b64}" ]]; then + tampered_sig_b64="A${sig_b64:1}" + fi + local tampered_signature="${sig_prefix}${tampered_sig_b64}${sig_suffix}" + + run_test \ + "Message signature - invalid signature bytes" \ + "${failure_code}" \ + -H "Approov-Token: ${token}" \ + -H "Signature: ${tampered_signature}" \ + -H "Signature-Input: ${signature_input_header}" \ + "${BASE_URL}/token-check-signature" + + local tampered_token + tampered_token="${token}x" + + run_test \ + "Message signature - invalid token signature" \ + "${failure_code}" \ + -H "Approov-Token: ${tampered_token}" \ + -H "Signature: ${signature_header}" \ + -H "Signature-Input: ${signature_input_header}" \ + "${BASE_URL}/token-check-signature" + + echo + echo "Full request and response details are saved in:" + echo " ${LOGFILE}" +} + +main "$@" diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..8919179 --- /dev/null +++ b/test.sh @@ -0,0 +1,382 @@ +#!/bin/bash +set -o errexit +set -o nounset +set -o pipefail + +####################################### +# Approov demo API test harness. +# +# Description: +# Calls unprotected and protected endpoints of the Approov demo API, +# validates HTTP status codes and logs complete HTTP exchanges +# (request + response) to a timestamped log file. +# +# Dependencies: +# - bash +# - curl +# - approov CLI available on PATH and configured +# +# Environment: +# BASE_URL: +# Base URL of the API under test. Default: http://localhost:8111 +# TOKDIR: +# Directory where temporary token files are stored. Default: .config +# LOGDIR=${TOKDIR}/logs, LOGFILE=${LOGDIR}/.log +####################################### + +# Constants +readonly BASE_URL="${BASE_URL:-http://localhost:8111}" +readonly TOKDIR="${TOKDIR:-.config}" +readonly LOGDIR="${TOKDIR}/logs" +readonly LOGFILE="${LOGDIR}/$(date '+%Y-%m-%d_%H-%M-%S').log" + +# Globals +# is_approov_disabled: +# Boolean flag indicating if Approov checks appear disabled +# based on /approov-state endpoint. +is_approov_disabled=false +success_code=200 +failure_code=401 + +# state_http_code: +# HTTP status code from /approov-state endpoint. +state_http_code='' + +####################################### +# Print error message to STDERR with timestamp. +# Globals: +# None +# Arguments: +# All arguments are printed as the error message. +# Outputs: +# Writes formatted error message to STDERR. +# Returns: +# 0 +####################################### +err() { + echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2 +} + +####################################### +# Ensure a required command exists on PATH. +# Globals: +# None +# Arguments: +# command name to check. +# Outputs: +# Error message to STDERR if command is missing. +# Returns: +# Exits the script with code 1 if the command is missing. +####################################### +requirement_check() { + local cmd="$1" + + if ! command -v "${cmd}" >/dev/null 2>&1; then + err "Missing required command: ${cmd}" + exit 1 + fi +} + +####################################### +# Generate an Approov token into an output file. +# Globals: +# None +# Arguments: +# output file path. +# arguments passed to "approov token". +# Outputs: +# Captures stdout+stderr from "approov token", takes the last non-empty +# line as the token, and writes only that line to the output file. +# Returns: +# 0 on success. +# 1 on failure (CLI error or no token produced). +####################################### +gen_token() { + local outfile="$1" + shift + + set +o errexit + local cli_output + cli_output="$(approov token "$@" 2>&1)" + local rc=$? + set -o errexit + + if ((rc != 0)); then + err "Approov CLI failed: approov token $*" + printf '%s\n' "${cli_output}" >&2 + return 1 + fi + + # Prints notices before the token, grab the last non-empty line. + local token + token="$(printf '%s\n' "${cli_output}" | awk 'NF{last=$0} END{print last}')" + if [[ -z "${token}" ]]; then + err "Approov CLI produced no token output" + return 1 + fi + + printf '%s\n' "${token}" >"${outfile}" +} + +####################################### +# Print test result and append full HTTP exchange to a log file. +# Globals: +# LOGFILE +# is_approov_disabled +# Arguments: +# $1 - test name. +# $2 - expected HTTP status code. +# $3 - actual HTTP status code. +# $4 - full HTTP response (headers + body). +# Outputs: +# Human-readable result to STDOUT. +# Detailed log entry appended to LOGFILE. +# Returns: +# 0 +####################################### +print_test_result() { + local name="$1" + local expected="$2" + local status="$3" + local resp="$4" + + local result="Failed" + if [[ "${status}" == "${expected}" ]]; then + result="Passed" + fi + + echo "${name}: ${result} (status: ${status}, expected: ${expected})" + + { + echo "Test: ${name}" + echo "Expected status: ${expected}" + echo "Actual status: ${status}" + if [[ "${is_approov_disabled}" == "false" ]]; then + echo "Approov State: enabled, token checks performed." + else + echo "Approov State: disabled, no checks performed." + fi + echo + echo "HTTP exchange:" + echo "${resp}" + echo + } >>"${LOGFILE}" 2>&1 +} + +####################################### +# Execute a curl call for a test and evaluate the result. +# Globals: +# None +# Notes: +# Uses print_test_result, which logs to LOGFILE and reads +# is_approov_disabled. +# Arguments: +# test name. +# expected HTTP status code. +# arguments passed to curl. +# Outputs: +# Short result to STDOUT, full HTTP exchange appended to LOGFILE. +# Returns: +# 0 on success, curl's exit code on failure. +####################################### +run_test() { + # shift after each grab so $1 advances (name -> expected -> rest) + local name="$1"; shift + local expected="$1"; shift + + local resp + local status + local curl_rc + + # -i: include headers, -s: silent + set +o errexit + resp="$(curl -i -s "$@")" + curl_rc=$? + set -o errexit + + if ((curl_rc != 0)); then + err "curl failed for ${name} (rc=${curl_rc})" + return "${curl_rc}" + fi + + status="$( + printf '%s\n' "${resp}" | + grep -m1 '^HTTP/' | + awk '{print $2}' + )" + + print_test_result "${name}" "${expected}" "${status}" "${resp}" +} + +main() { + requirement_check "approov" + requirement_check "curl" + + mkdir -p "${TOKDIR}" "${LOGDIR}" + + echo "Listing Approov API configuration:" + approov api -list + echo + + echo "Approov state check:" + local state_response + state_response="$(curl -i -s "${BASE_URL}/approov-state")" + state_http_code="$( + printf '%s\n' "${state_response}" | + grep -m1 '^HTTP/' | + awk '{print $2}' + )" + + if [[ "${state_http_code}" != "200" || -z "${state_http_code}" ]]; then + err "Failed to get Approov state from ${BASE_URL}/approov-state (status=${state_http_code:-unknown})" + exit 1 + fi + + if grep -q '"approovEnabled":true' <<<"${state_response}"; then + echo " Approov service: ENABLED" + is_approov_disabled=false + else + echo " Approov service: DISABLED" + is_approov_disabled=true + failure_code=200 + fi + echo + + # 0) Unprotected endpoint. + run_test \ + "Unprotected request - no approov protection" \ + "${success_code}" \ + "${BASE_URL}/unprotected" + + # 1) Token check. + gen_token \ + "${TOKDIR}/approov_token_valid" \ + -genExample \ + example.com + + # 1.1) Valid Token. + run_test \ + "Token check - valid token" \ + "${success_code}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_valid")" \ + "${BASE_URL}/token-check" + + # 1.2) Invalid Token. + gen_token \ + "${TOKDIR}/approov_token_invalid" \ + -genExample \ + example.com \ + -type invalid || true + + run_test \ + "Token check - invalid token" \ + "${failure_code}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_invalid")" \ + "${BASE_URL}/token-check" + + # 2) Token Binding ["Authorization"]. + local AUTH_VAL="ExampleAuthToken==" + export HASH_INPUT="${AUTH_VAL}" + + gen_token \ + "${TOKDIR}/approov_token_bind_auth_valid" \ + -setDataHashInToken "${HASH_INPUT}" \ + -genExample \ + example.com + + # 2.1) Valid Token. + run_test \ + "Single Binding - valid token and header" \ + "${success_code}" \ + -H "Authorization: ${AUTH_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_valid")" \ + "${BASE_URL}/token-binding" + + # 2.2) Missing Header. + run_test \ + "Single Binding - missing Authorization header" \ + "${failure_code}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_valid")" \ + "${BASE_URL}/token-binding" + + # 2.3) Incorrect Header. + run_test \ + "Single Binding - incorrect Authorization header" \ + "${failure_code}" \ + -H "Authorization: BadAuthToken==" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_valid")" \ + "${BASE_URL}/token-binding" + + # 2.4) Invalid Token. + gen_token \ + "${TOKDIR}/approov_token_bind_auth_invalid" \ + -setDataHashInToken "${HASH_INPUT}" \ + -genExample \ + example.com \ + -type invalid || true + + run_test \ + "Single Binding - invalid token" \ + "${failure_code}" \ + -H "Authorization: ${AUTH_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_invalid")" \ + "${BASE_URL}/token-binding" + + # 3) Token Binding ["Authorization", "Session-Id"]. + local AUTH_VAL2="ExampleAuthToken==" + local SI_VAL="Session-123" + export HASH_INPUT="${AUTH_VAL2}${SI_VAL}" + + gen_token \ + "${TOKDIR}/approov_token_bind_auth_si_valid" \ + -setDataHashInToken "${HASH_INPUT}" \ + -genExample \ + example.com + + # 3.1) Valid. + run_test \ + "Double Binding - valid token and headers" \ + "${success_code}" \ + -H "Authorization: ${AUTH_VAL2}" \ + -H "Session-Id: ${SI_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_si_valid")" \ + "${BASE_URL}/token-double-binding" + + # 3.2) Missing headers. + run_test \ + "Double Binding - missing binding headers" \ + "${failure_code}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_si_valid")" \ + "${BASE_URL}/token-double-binding" + + # 3.3) Incorrect headers. + run_test \ + "Double Binding - incorrect binding headers" \ + "${failure_code}" \ + -H "Authorization: BadAuthToken==" \ + -H "Session-Id: BadSession" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_si_valid")" \ + "${BASE_URL}/token-double-binding" + + # 3.4) Invalid token. + gen_token \ + "${TOKDIR}/approov_token_bind_auth_si_invalid" \ + -setDataHashInToken "${HASH_INPUT}" \ + -genExample \ + example.com \ + -type invalid || true + + run_test \ + "Double Binding - invalid token" \ + "${failure_code}" \ + -H "Authorization: ${AUTH_VAL2}" \ + -H "Session-Id: ${SI_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_si_invalid")" \ + "${BASE_URL}/token-double-binding" + + echo + echo "Full request and response details are saved in:" + echo " ${LOGFILE}" +} + +main "$@" diff --git a/vendor/http-message-signatures/LICENSE.md b/vendor/http-message-signatures/LICENSE.md new file mode 100644 index 0000000..79daf87 --- /dev/null +++ b/vendor/http-message-signatures/LICENSE.md @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2021, Daniel Hensby + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/vendor/http-message-signatures/lib/algorithm/index.d.ts b/vendor/http-message-signatures/lib/algorithm/index.d.ts new file mode 100644 index 0000000..bdfcd95 --- /dev/null +++ b/vendor/http-message-signatures/lib/algorithm/index.d.ts @@ -0,0 +1,24 @@ +import { BinaryLike, KeyLike, SignKeyObjectInput, SignPrivateKeyInput, VerifyKeyObjectInput, VerifyPublicKeyInput } from 'crypto'; +import { SigningKey, Algorithm, Verifier } from '../types'; +/** + * A helper method for easier consumption of the library. + * + * Consumers of the library can use this function to create a signer "out of the box" using a PEM + * file they have access to. + * + * @todo - read the key and determine its type automatically to make usage even easier + */ +export declare function createSigner(key: BinaryLike | KeyLike | SignKeyObjectInput | SignPrivateKeyInput, alg: Algorithm, id?: string): SigningKey; +/** + * A helper method for easier consumption of the library. + * + * Consumers of the library can use this function to create a verifier "out of the box" using a PEM + * file they have access to. + * + * Verifiers are a little trickier as they will need to be produced "on demand" and the consumer will + * need to implement some logic for looking up keys by id (or other aspects of the request if no keyid + * is supplied) and then returning a validator + * + * @todo - attempt to look up algorithm automatically + */ +export declare function createVerifier(key: BinaryLike | KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput, alg: Algorithm): Verifier; diff --git a/vendor/http-message-signatures/lib/algorithm/index.js b/vendor/http-message-signatures/lib/algorithm/index.js new file mode 100644 index 0000000..3b2c9e4 --- /dev/null +++ b/vendor/http-message-signatures/lib/algorithm/index.js @@ -0,0 +1,124 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createSigner = createSigner; +exports.createVerifier = createVerifier; +const crypto_1 = require("crypto"); +const constants_1 = require("constants"); +const errors_1 = require("../errors"); +/** + * A helper method for easier consumption of the library. + * + * Consumers of the library can use this function to create a signer "out of the box" using a PEM + * file they have access to. + * + * @todo - read the key and determine its type automatically to make usage even easier + */ +function createSigner(key, alg, id) { + const signer = { alg }; + switch (alg) { + case 'hmac-sha256': + signer.sign = async (data) => (0, crypto_1.createHmac)('sha256', key).update(data).digest(); + break; + case 'rsa-pss-sha512': + signer.sign = async (data) => (0, crypto_1.createSign)('sha512').update(data).sign({ + key, + padding: constants_1.RSA_PKCS1_PSS_PADDING, + }); + break; + case 'rsa-v1_5-sha256': + signer.sign = async (data) => (0, crypto_1.createSign)('sha256').update(data).sign({ + key, + padding: constants_1.RSA_PKCS1_PADDING, + }); + break; + case 'rsa-v1_5-sha1': + // this is legacy for cavage + signer.sign = async (data) => (0, crypto_1.createSign)('sha1').update(data).sign({ + key, + padding: constants_1.RSA_PKCS1_PADDING, + }); + break; + case 'ecdsa-p256-sha256': + signer.sign = async (data) => (0, crypto_1.createSign)('sha256').update(data).sign({ + key: key, + dsaEncoding: 'ieee-p1363', + }); + break; + case 'ecdsa-p384-sha384': + signer.sign = async (data) => (0, crypto_1.createSign)('sha384').update(data).sign({ + key: key, + dsaEncoding: 'ieee-p1363', + }); + break; + case 'ed25519': + signer.sign = async (data) => (0, crypto_1.sign)(null, data, key); + // signer.sign = async (data: Buffer) => createSign('ed25519').update(data).sign(key as KeyLike); + break; + default: + throw new errors_1.UnknownAlgorithmError(`Unsupported signing algorithm ${alg}`); + } + if (id) { + signer.id = id; + } + return signer; +} +/** + * A helper method for easier consumption of the library. + * + * Consumers of the library can use this function to create a verifier "out of the box" using a PEM + * file they have access to. + * + * Verifiers are a little trickier as they will need to be produced "on demand" and the consumer will + * need to implement some logic for looking up keys by id (or other aspects of the request if no keyid + * is supplied) and then returning a validator + * + * @todo - attempt to look up algorithm automatically + */ +function createVerifier(key, alg) { + let verifier; + switch (alg) { + case 'hmac-sha256': + verifier = async (data, signature) => { + const expected = (0, crypto_1.createHmac)('sha256', key).update(data).digest(); + return signature.length === expected.length && (0, crypto_1.timingSafeEqual)(signature, expected); + }; + break; + case 'rsa-pss-sha512': + verifier = async (data, signature) => (0, crypto_1.createVerify)('sha512').update(data).verify({ + key, + padding: constants_1.RSA_PKCS1_PSS_PADDING, + }, signature); + break; + case 'rsa-v1_5-sha1': + verifier = async (data, signature) => (0, crypto_1.createVerify)('sha1').update(data).verify({ + key, + padding: constants_1.RSA_PKCS1_PADDING, + }, signature); + break; + case 'rsa-v1_5-sha256': + verifier = async (data, signature) => (0, crypto_1.createVerify)('sha256').update(data).verify({ + key, + padding: constants_1.RSA_PKCS1_PADDING, + }, signature); + break; + case 'ecdsa-p256-sha256': + verifier = async (data, signature) => (0, crypto_1.createVerify)('sha256').update(data).verify({ + key: key, + dsaEncoding: 'ieee-p1363', + }, signature); + break; + case 'ecdsa-p384-sha384': + verifier = async (data, signature) => (0, crypto_1.createVerify)('sha384').update(data).verify({ + key: key, + dsaEncoding: 'ieee-p1363', + }, signature); + break; + case 'ed25519': + verifier = async (data, signature) => (0, crypto_1.verify)(null, data, key, signature); + break; + default: + throw new errors_1.UnknownAlgorithmError(`Unsupported signing algorithm ${alg}`); + } + return Object.assign(verifier, { alg }); +} +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/algorithm/index.js.map b/vendor/http-message-signatures/lib/algorithm/index.js.map new file mode 100644 index 0000000..29fa073 --- /dev/null +++ b/vendor/http-message-signatures/lib/algorithm/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/algorithm/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/cavage/index.d.ts b/vendor/http-message-signatures/lib/cavage/index.d.ts new file mode 100644 index 0000000..0b7c1e6 --- /dev/null +++ b/vendor/http-message-signatures/lib/cavage/index.d.ts @@ -0,0 +1,14 @@ +import { Request, Response, SignConfig, VerifyConfig } from '../types'; +/** + * Components can be derived from requests or responses (which can also be bound to their request). + * The signature is essentially (component, signingSubject, supplementaryData) + * + * @todo - Allow consumers to register their own component parser somehow + */ +export declare function deriveComponent(component: string, message: Request | Response): string[]; +export declare function extractHeader(header: string, { headers }: Request | Response): string[]; +export declare function formatSignatureBase(base: [string, string[]][]): string; +export declare function createSigningParameters(config: SignConfig): Map; +export declare function createSignatureBase(fields: string[], message: Request | Response, signingParameters: Map): [string, string[]][]; +export declare function signMessage(config: SignConfig, message: T): Promise; +export declare function verifyMessage(config: VerifyConfig, message: Request | Response): Promise; diff --git a/vendor/http-message-signatures/lib/cavage/index.js b/vendor/http-message-signatures/lib/cavage/index.js new file mode 100644 index 0000000..8e87ad2 --- /dev/null +++ b/vendor/http-message-signatures/lib/cavage/index.js @@ -0,0 +1,310 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.deriveComponent = deriveComponent; +exports.extractHeader = extractHeader; +exports.formatSignatureBase = formatSignatureBase; +exports.createSigningParameters = createSigningParameters; +exports.createSignatureBase = createSignatureBase; +exports.signMessage = signMessage; +exports.verifyMessage = verifyMessage; +const structured_headers_1 = require("structured-headers"); +const types_1 = require("../types"); +const structured_header_1 = require("../structured-header"); +function mapCavageAlgorithm(alg) { + switch (alg.toLowerCase()) { + case 'hs2019': + return 'rsa-pss-sha512'; + case 'rsa-sha1': + return 'rsa-v1_5-sha1'; + case 'rsa-sha256': + return 'rsa-v1_5-sha256'; + case 'ecdsa-sha256': + return 'ecdsa-p256-sha256'; + default: + return alg; + } +} +function mapHttpbisAlgorithm(alg) { + switch (alg.toLowerCase()) { + case 'rsa-pss-sha512': + return 'hs2019'; + case 'rsa-v1_5-sha1': + return 'rsa-sha1'; + case 'rsa-v1_5-sha256': + return 'rsa-sha256'; + case 'ecdsa-p256-sha256': + return 'ecdsa-sha256'; + default: + return alg; + } +} +/** + * Components can be derived from requests or responses (which can also be bound to their request). + * The signature is essentially (component, signingSubject, supplementaryData) + * + * @todo - Allow consumers to register their own component parser somehow + */ +function deriveComponent(component, message) { + const [componentName, params] = (0, structured_headers_1.parseItem)((0, structured_header_1.quoteString)(component)); + if (params.size) { + throw new Error('Component parameters are not supported in cavage'); + } + switch (componentName.toString().toLowerCase()) { + case '@request-target': { + if (!(0, types_1.isRequest)(message)) { + throw new Error('Cannot derive @request-target on response'); + } + const { pathname, search } = typeof message.url === 'string' ? new URL(message.url) : message.url; + // this is really sketchy because the request-target is actually what is in the raw HTTP header + // so one should avoid signing this value as the application layer just can't know how this + // is formatted + return [`${message.method.toLowerCase()} ${pathname}${search}`]; + } + default: + throw new Error(`Unsupported component "${component}"`); + } +} +function extractHeader(header, { headers }) { + const [headerName, params] = (0, structured_headers_1.parseItem)((0, structured_header_1.quoteString)(header)); + if (params.size) { + throw new Error('Field parameters are not supported in cavage'); + } + const lcHeaderName = headerName.toString().toLowerCase(); + const headerTuple = Object.entries(headers).find(([name]) => name.toLowerCase() === lcHeaderName); + if (!headerTuple) { + throw new Error(`No header ${headerName} found in headers`); + } + return [(Array.isArray(headerTuple[1]) ? headerTuple[1] : [headerTuple[1]]).map((val) => val.trim().replace(/\n\s*/gm, ' ')).join(', ')]; +} +function formatSignatureBase(base) { + return base.reduce((accum, [key, value]) => { + const [keyName] = (0, structured_headers_1.parseItem)((0, structured_header_1.quoteString)(key)); + const lcKey = keyName.toLowerCase(); + if (lcKey.startsWith('@')) { + accum.push(`(${lcKey.slice(1)}): ${value.join(', ')}`); + } + else { + accum.push(`${key.toLowerCase()}: ${value.join(', ')}`); + } + return accum; + }, []).join('\n'); +} +function createSigningParameters(config) { + const now = new Date(); + return (config.params ?? types_1.defaultParams).reduce((params, paramName) => { + let value = ''; + switch (paramName.toLowerCase()) { + case 'created': + // created is optional but recommended. If created is supplied but is null, that's an explicit + // instruction to *not* include the created parameter + if (config.paramValues?.created !== null) { + const created = config.paramValues?.created ?? now; + value = Math.floor(created.getTime() / 1000); + } + break; + case 'expires': + // attempt to obtain an explicit expires time, otherwise create one that is 300 seconds after + // creation. Don't add an expires time if there is no created time + if (config.paramValues?.expires || config.paramValues?.created !== null) { + const expires = config.paramValues?.expires ?? new Date((config.paramValues?.created ?? now).getTime() + 300000); + value = Math.floor(expires.getTime() / 1000); + } + break; + case 'keyid': { + // attempt to obtain the keyid omit if missing + const kid = config.paramValues?.keyid ?? config.key.id ?? null; + if (kid) { + value = kid.toString(); + } + break; + } + case 'alg': { + const alg = config.paramValues?.alg ?? config.key.alg ?? null; + if (alg) { + value = alg.toString(); + } + break; + } + default: + if (config.paramValues?.[paramName] instanceof Date) { + value = Math.floor(config.paramValues[paramName].getTime() / 1000).toString(); + } + else if (config.paramValues?.[paramName]) { + value = config.paramValues[paramName]; + } + } + if (value) { + params.set(paramName, value); + } + return params; + }, new Map()); +} +function createSignatureBase(fields, message, signingParameters) { + return fields.reduce((base, fieldName) => { + const [field, params] = (0, structured_headers_1.parseItem)((0, structured_header_1.quoteString)(fieldName)); + if (params.size) { + throw new Error('Field parameters are not supported'); + } + const lcFieldName = field.toString().toLowerCase(); + switch (lcFieldName) { + case '@created': + if (signingParameters.has('created')) { + base.push(['(created)', [signingParameters.get('created')]]); + } + break; + case '@expires': + if (signingParameters.has('expires')) { + base.push(['(expires)', [signingParameters.get('expires')]]); + } + break; + case '@request-target': { + if (!(0, types_1.isRequest)(message)) { + throw new Error('Cannot read target of response'); + } + const { pathname, search } = typeof message.url === 'string' ? new URL(message.url) : message.url; + base.push(['(request-target)', [`${message.method.toLowerCase()} ${pathname}${search}`]]); + break; + } + default: + base.push([lcFieldName, extractHeader(lcFieldName, message)]); + } + return base; + }, []); +} +async function signMessage(config, message) { + const signingParameters = createSigningParameters(config); + // NB: In spec versions 11 & 12 (the last 2), if no set of fields to sign has been provided, the default should be (created) + // other versions relied on the Date header - perhaps this should be configurable + const signatureBase = createSignatureBase(config.fields ?? ['@created'], message, signingParameters); + const base = formatSignatureBase(signatureBase); + // call sign + const signature = await config.key.sign(Buffer.from(base)); + const headerNames = signatureBase.map(([key]) => key); + // there is a somewhat deliberate and intentional deviation from spec here: + // If no headers (config.fields) are specified, the spec allows for it to be *inferred* + // that the (created) value is used, I don't like that and would rather be explicit + const header = [ + ...Array.from(signingParameters.entries()).map(([name, value]) => { + if (name === 'alg') { + return `algorithm="${mapHttpbisAlgorithm(value)}"`; + } + if (name === 'keyid') { + return `keyId="${value}"`; + } + if (typeof value === 'number') { + return `${name}=${value}`; + } + return `${name}="${value.toString()}"`; + }), + `headers="${headerNames.join(' ')}"`, + `signature="${signature.toString('base64')}"`, + ].join(','); + return { + ...message, + headers: { + ...message.headers, + Signature: header, + }, + }; +} +async function verifyMessage(config, message) { + const header = Object.entries(message.headers).find(([name]) => name.toLowerCase() === 'signature'); + if (!header) { + return null; + } + const parsedHeader = (Array.isArray(header[1]) ? header[1].join(', ') : header[1]).split(',').reduce((parts, value) => { + const [key, ...values] = value.trim().split('='); + if (parts.has(key)) { + throw new Error('Same parameter defined repeatedly'); + } + const val = values.join('=').replace(/^"(.*)"$/, '$1'); + switch (key.toLowerCase()) { + case 'created': + case 'expires': + parts.set(key, parseInt(val, 10)); + break; + default: + parts.set(key, val); + } + return parts; + }, new Map()); + if (!parsedHeader.has('signature')) { + throw new Error('Missing signature from header'); + } + const baseParts = new Map(createSignatureBase((parsedHeader.get('headers') ?? '(created)').split(' ').map((component) => { + return component.toLowerCase().replace(/^\((.*)\)$/, '@$1'); + }), message, parsedHeader)); + const base = formatSignatureBase(Array.from(baseParts.entries())); + const now = Math.floor(Date.now() / 1000); + const tolerance = config.tolerance ?? 0; + const notAfter = config.notAfter instanceof Date ? Math.floor(config.notAfter.getTime() / 1000) : config.notAfter ?? now; + const maxAge = config.maxAge ?? null; + const requiredParams = config.requiredParams ?? []; + const requiredFields = config.requiredFields ?? []; + const hasRequiredParams = requiredParams.every((param) => baseParts.has(param)); + if (!hasRequiredParams) { + return false; + } + // this could be tricky, what if we say "@method" but there is "@method;req" + const hasRequiredFields = requiredFields.every((field) => { + return parsedHeader.has(field.toLowerCase().replace(/^@(.*)/, '($1)')); + }); + if (!hasRequiredFields) { + return false; + } + if (parsedHeader.has('created')) { + const created = parsedHeader.get('created') - tolerance; + // maxAge overrides expires. + // signature is older than maxAge + if (maxAge && created - now > maxAge) { + return false; + } + // created after the allowed time (ie: created in the future) + if (created > notAfter) { + return false; + } + } + if (parsedHeader.has('expires')) { + const expires = parsedHeader.get('expires') + tolerance; + // expired signature + if (expires > now) { + return false; + } + } + // now look to verify the signature! Build the expected "signing base" and verify it! + const params = Array.from(parsedHeader.entries()).reduce((params, [key, value]) => { + let keyName = key; + let val; + switch (key.toLowerCase()) { + case 'created': + case 'expires': + val = new Date(value * 1000); + break; + case 'signature': + case 'headers': + return params; + case 'algorithm': + keyName = 'alg'; + val = mapCavageAlgorithm(value); + break; + case 'keyid': + keyName = 'keyid'; + val = value; + break; + default: { + if (typeof value === 'string' || typeof value === 'number') { + val = value; + } + else { + val = value.toString(); + } + } + } + return Object.assign(params, { + [keyName]: val, + }); + }, {}); + const key = await config.keyLookup(params); + return key?.verify(Buffer.from(base), Buffer.from(parsedHeader.get('signature'), 'base64'), params) ?? null; +} +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/cavage/index.js.map b/vendor/http-message-signatures/lib/cavage/index.js.map new file mode 100644 index 0000000..7084ecd --- /dev/null +++ b/vendor/http-message-signatures/lib/cavage/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cavage/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/expired-error.d.ts b/vendor/http-message-signatures/lib/errors/expired-error.d.ts new file mode 100644 index 0000000..bf9a85c --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/expired-error.d.ts @@ -0,0 +1,3 @@ +import { VerificationError } from './verification-error'; +export declare class ExpiredError extends VerificationError { +} diff --git a/vendor/http-message-signatures/lib/errors/expired-error.js b/vendor/http-message-signatures/lib/errors/expired-error.js new file mode 100644 index 0000000..f5b9840 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/expired-error.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ExpiredError = void 0; +const verification_error_1 = require("./verification-error"); +class ExpiredError extends verification_error_1.VerificationError { +} +exports.ExpiredError = ExpiredError; +//# sourceMappingURL=expired-error.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/expired-error.js.map b/vendor/http-message-signatures/lib/errors/expired-error.js.map new file mode 100644 index 0000000..cf51f18 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/expired-error.js.map @@ -0,0 +1 @@ +{"version":3,"file":"expired-error.js","sourceRoot":"","sources":["../../src/errors/expired-error.ts"],"names":[],"mappings":";;;AAAA,6DAAyD;AAEzD,MAAa,YAAa,SAAQ,sCAAiB;CAClD;AADD,oCACC"} \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/index.d.ts b/vendor/http-message-signatures/lib/errors/index.d.ts new file mode 100644 index 0000000..6977746 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/index.d.ts @@ -0,0 +1,7 @@ +export { ExpiredError } from './expired-error'; +export { MalformedSignatureError } from './malformed-signature-error'; +export { UnacceptableSignatureError } from './unacceptable-signature-error'; +export { UnknownAlgorithmError } from './unknown-algorithm-error'; +export { UnknownKeyError } from './unknown-key-error'; +export { UnsupportedAlgorithmError } from './unsupported-algorithm-error'; +export { VerificationError } from './verification-error'; diff --git a/vendor/http-message-signatures/lib/errors/index.js b/vendor/http-message-signatures/lib/errors/index.js new file mode 100644 index 0000000..08a7a25 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/index.js @@ -0,0 +1,18 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.VerificationError = exports.UnsupportedAlgorithmError = exports.UnknownKeyError = exports.UnknownAlgorithmError = exports.UnacceptableSignatureError = exports.MalformedSignatureError = exports.ExpiredError = void 0; +var expired_error_1 = require("./expired-error"); +Object.defineProperty(exports, "ExpiredError", { enumerable: true, get: function () { return expired_error_1.ExpiredError; } }); +var malformed_signature_error_1 = require("./malformed-signature-error"); +Object.defineProperty(exports, "MalformedSignatureError", { enumerable: true, get: function () { return malformed_signature_error_1.MalformedSignatureError; } }); +var unacceptable_signature_error_1 = require("./unacceptable-signature-error"); +Object.defineProperty(exports, "UnacceptableSignatureError", { enumerable: true, get: function () { return unacceptable_signature_error_1.UnacceptableSignatureError; } }); +var unknown_algorithm_error_1 = require("./unknown-algorithm-error"); +Object.defineProperty(exports, "UnknownAlgorithmError", { enumerable: true, get: function () { return unknown_algorithm_error_1.UnknownAlgorithmError; } }); +var unknown_key_error_1 = require("./unknown-key-error"); +Object.defineProperty(exports, "UnknownKeyError", { enumerable: true, get: function () { return unknown_key_error_1.UnknownKeyError; } }); +var unsupported_algorithm_error_1 = require("./unsupported-algorithm-error"); +Object.defineProperty(exports, "UnsupportedAlgorithmError", { enumerable: true, get: function () { return unsupported_algorithm_error_1.UnsupportedAlgorithmError; } }); +var verification_error_1 = require("./verification-error"); +Object.defineProperty(exports, "VerificationError", { enumerable: true, get: function () { return verification_error_1.VerificationError; } }); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/index.js.map b/vendor/http-message-signatures/lib/errors/index.js.map new file mode 100644 index 0000000..5b7d7c2 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":";;;AAAA,iDAA+C;AAAtC,6GAAA,YAAY,OAAA;AACrB,yEAAsE;AAA7D,oIAAA,uBAAuB,OAAA;AAChC,+EAA4E;AAAnE,0IAAA,0BAA0B,OAAA;AACnC,qEAAkE;AAAzD,gIAAA,qBAAqB,OAAA;AAC9B,yDAAsD;AAA7C,oHAAA,eAAe,OAAA;AACxB,6EAA0E;AAAjE,wIAAA,yBAAyB,OAAA;AAClC,2DAAyD;AAAhD,uHAAA,iBAAiB,OAAA"} \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/malformed-signature-error.d.ts b/vendor/http-message-signatures/lib/errors/malformed-signature-error.d.ts new file mode 100644 index 0000000..7f3ea73 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/malformed-signature-error.d.ts @@ -0,0 +1,3 @@ +import { VerificationError } from './verification-error'; +export declare class MalformedSignatureError extends VerificationError { +} diff --git a/vendor/http-message-signatures/lib/errors/malformed-signature-error.js b/vendor/http-message-signatures/lib/errors/malformed-signature-error.js new file mode 100644 index 0000000..a8d72b1 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/malformed-signature-error.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MalformedSignatureError = void 0; +const verification_error_1 = require("./verification-error"); +class MalformedSignatureError extends verification_error_1.VerificationError { +} +exports.MalformedSignatureError = MalformedSignatureError; +//# sourceMappingURL=malformed-signature-error.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/malformed-signature-error.js.map b/vendor/http-message-signatures/lib/errors/malformed-signature-error.js.map new file mode 100644 index 0000000..a3a0a76 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/malformed-signature-error.js.map @@ -0,0 +1 @@ +{"version":3,"file":"malformed-signature-error.js","sourceRoot":"","sources":["../../src/errors/malformed-signature-error.ts"],"names":[],"mappings":";;;AAAA,6DAAyD;AAEzD,MAAa,uBAAwB,SAAQ,sCAAiB;CAC7D;AADD,0DACC"} \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/unacceptable-signature-error.d.ts b/vendor/http-message-signatures/lib/errors/unacceptable-signature-error.d.ts new file mode 100644 index 0000000..510cb2d --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/unacceptable-signature-error.d.ts @@ -0,0 +1,3 @@ +import { VerificationError } from './verification-error'; +export declare class UnacceptableSignatureError extends VerificationError { +} diff --git a/vendor/http-message-signatures/lib/errors/unacceptable-signature-error.js b/vendor/http-message-signatures/lib/errors/unacceptable-signature-error.js new file mode 100644 index 0000000..a1c53d3 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/unacceptable-signature-error.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.UnacceptableSignatureError = void 0; +const verification_error_1 = require("./verification-error"); +class UnacceptableSignatureError extends verification_error_1.VerificationError { +} +exports.UnacceptableSignatureError = UnacceptableSignatureError; +//# sourceMappingURL=unacceptable-signature-error.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/unacceptable-signature-error.js.map b/vendor/http-message-signatures/lib/errors/unacceptable-signature-error.js.map new file mode 100644 index 0000000..bb37a59 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/unacceptable-signature-error.js.map @@ -0,0 +1 @@ +{"version":3,"file":"unacceptable-signature-error.js","sourceRoot":"","sources":["../../src/errors/unacceptable-signature-error.ts"],"names":[],"mappings":";;;AAAA,6DAAyD;AAEzD,MAAa,0BAA2B,SAAQ,sCAAiB;CAChE;AADD,gEACC"} \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/unknown-algorithm-error.d.ts b/vendor/http-message-signatures/lib/errors/unknown-algorithm-error.d.ts new file mode 100644 index 0000000..fdcc6b1 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/unknown-algorithm-error.d.ts @@ -0,0 +1,5 @@ +/** + * Thrown when a verifier/signer is created with an unknown algorithm + */ +export declare class UnknownAlgorithmError extends Error { +} diff --git a/vendor/http-message-signatures/lib/errors/unknown-algorithm-error.js b/vendor/http-message-signatures/lib/errors/unknown-algorithm-error.js new file mode 100644 index 0000000..3a8859d --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/unknown-algorithm-error.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.UnknownAlgorithmError = void 0; +/** + * Thrown when a verifier/signer is created with an unknown algorithm + */ +class UnknownAlgorithmError extends Error { +} +exports.UnknownAlgorithmError = UnknownAlgorithmError; +//# sourceMappingURL=unknown-algorithm-error.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/unknown-algorithm-error.js.map b/vendor/http-message-signatures/lib/errors/unknown-algorithm-error.js.map new file mode 100644 index 0000000..07a764e --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/unknown-algorithm-error.js.map @@ -0,0 +1 @@ +{"version":3,"file":"unknown-algorithm-error.js","sourceRoot":"","sources":["../../src/errors/unknown-algorithm-error.ts"],"names":[],"mappings":";;;AAAA;;GAEG;AACH,MAAa,qBAAsB,SAAQ,KAAK;CAC/C;AADD,sDACC"} \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/unknown-key-error.d.ts b/vendor/http-message-signatures/lib/errors/unknown-key-error.d.ts new file mode 100644 index 0000000..c11221e --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/unknown-key-error.d.ts @@ -0,0 +1,3 @@ +import { VerificationError } from './verification-error'; +export declare class UnknownKeyError extends VerificationError { +} diff --git a/vendor/http-message-signatures/lib/errors/unknown-key-error.js b/vendor/http-message-signatures/lib/errors/unknown-key-error.js new file mode 100644 index 0000000..d4bb35e --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/unknown-key-error.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.UnknownKeyError = void 0; +const verification_error_1 = require("./verification-error"); +class UnknownKeyError extends verification_error_1.VerificationError { +} +exports.UnknownKeyError = UnknownKeyError; +//# sourceMappingURL=unknown-key-error.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/unknown-key-error.js.map b/vendor/http-message-signatures/lib/errors/unknown-key-error.js.map new file mode 100644 index 0000000..96310e2 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/unknown-key-error.js.map @@ -0,0 +1 @@ +{"version":3,"file":"unknown-key-error.js","sourceRoot":"","sources":["../../src/errors/unknown-key-error.ts"],"names":[],"mappings":";;;AAAA,6DAAyD;AAEzD,MAAa,eAAgB,SAAQ,sCAAiB;CACrD;AADD,0CACC"} \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/unsupported-algorithm-error.d.ts b/vendor/http-message-signatures/lib/errors/unsupported-algorithm-error.d.ts new file mode 100644 index 0000000..f0cae28 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/unsupported-algorithm-error.d.ts @@ -0,0 +1,7 @@ +import { VerificationError } from './verification-error'; +/** + * Thrown when a key is presented to verify a signature with + * an algorithm that is not supported + */ +export declare class UnsupportedAlgorithmError extends VerificationError { +} diff --git a/vendor/http-message-signatures/lib/errors/unsupported-algorithm-error.js b/vendor/http-message-signatures/lib/errors/unsupported-algorithm-error.js new file mode 100644 index 0000000..b5a728b --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/unsupported-algorithm-error.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.UnsupportedAlgorithmError = void 0; +const verification_error_1 = require("./verification-error"); +/** + * Thrown when a key is presented to verify a signature with + * an algorithm that is not supported + */ +class UnsupportedAlgorithmError extends verification_error_1.VerificationError { +} +exports.UnsupportedAlgorithmError = UnsupportedAlgorithmError; +//# sourceMappingURL=unsupported-algorithm-error.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/unsupported-algorithm-error.js.map b/vendor/http-message-signatures/lib/errors/unsupported-algorithm-error.js.map new file mode 100644 index 0000000..bc711dd --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/unsupported-algorithm-error.js.map @@ -0,0 +1 @@ +{"version":3,"file":"unsupported-algorithm-error.js","sourceRoot":"","sources":["../../src/errors/unsupported-algorithm-error.ts"],"names":[],"mappings":";;;AAAA,6DAAyD;AAEzD;;;GAGG;AACH,MAAa,yBAA0B,SAAQ,sCAAiB;CAC/D;AADD,8DACC"} \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/verification-error.d.ts b/vendor/http-message-signatures/lib/errors/verification-error.d.ts new file mode 100644 index 0000000..25f4f45 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/verification-error.d.ts @@ -0,0 +1,2 @@ +export declare class VerificationError extends Error { +} diff --git a/vendor/http-message-signatures/lib/errors/verification-error.js b/vendor/http-message-signatures/lib/errors/verification-error.js new file mode 100644 index 0000000..a37a2e0 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/verification-error.js @@ -0,0 +1,7 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.VerificationError = void 0; +class VerificationError extends Error { +} +exports.VerificationError = VerificationError; +//# sourceMappingURL=verification-error.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/errors/verification-error.js.map b/vendor/http-message-signatures/lib/errors/verification-error.js.map new file mode 100644 index 0000000..70b5591 --- /dev/null +++ b/vendor/http-message-signatures/lib/errors/verification-error.js.map @@ -0,0 +1 @@ +{"version":3,"file":"verification-error.js","sourceRoot":"","sources":["../../src/errors/verification-error.ts"],"names":[],"mappings":";;;AAAA,MAAa,iBAAkB,SAAQ,KAAK;CAC3C;AADD,8CACC"} \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/httpbis/index.d.ts b/vendor/http-message-signatures/lib/httpbis/index.d.ts new file mode 100644 index 0000000..b50f46b --- /dev/null +++ b/vendor/http-message-signatures/lib/httpbis/index.d.ts @@ -0,0 +1,19 @@ +import { Parameters } from 'structured-headers'; +import { Request, Response, SignConfig, VerifyConfig, CommonConfig } from '../types'; +export declare function deriveComponent(component: string, params: Map, res: Response, req?: Request): string[]; +export declare function deriveComponent(component: string, params: Map, req: Request): string[]; +export declare function extractHeader(header: string, params: Map, res: Response, req?: Request): string[]; +export declare function extractHeader(header: string, params: Map, req: Request): string[]; +export declare function createSignatureBase(config: CommonConfig & { + fields: string[]; +}, res: Response, req?: Request): [string, string[]][]; +export declare function createSignatureBase(config: CommonConfig & { + fields: string[]; +}, req: Request): [string, string[]][]; +export declare function formatSignatureBase(base: [string, string[]][]): string; +export declare function createSigningParameters(config: SignConfig): Parameters; +export declare function augmentHeaders(headers: Record, signature: Buffer, signatureInput: string, name?: string): Record; +export declare function signMessage(config: SignConfig, res: T, req?: U): Promise; +export declare function signMessage(config: SignConfig, req: T): Promise; +export declare function verifyMessage(config: VerifyConfig, response: Response, request?: Request): Promise; +export declare function verifyMessage(config: VerifyConfig, request: Request): Promise; diff --git a/vendor/http-message-signatures/lib/httpbis/index.js b/vendor/http-message-signatures/lib/httpbis/index.js new file mode 100644 index 0000000..b52418c --- /dev/null +++ b/vendor/http-message-signatures/lib/httpbis/index.js @@ -0,0 +1,465 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.deriveComponent = deriveComponent; +exports.extractHeader = extractHeader; +exports.createSignatureBase = createSignatureBase; +exports.formatSignatureBase = formatSignatureBase; +exports.createSigningParameters = createSigningParameters; +exports.augmentHeaders = augmentHeaders; +exports.signMessage = signMessage; +exports.verifyMessage = verifyMessage; +const structured_headers_1 = require("structured-headers"); +const structured_header_1 = require("../structured-header"); +const types_1 = require("../types"); +const errors_1 = require("../errors"); +/** + * Derives a component value defined by HTTP Message Signatures (RFC 9421). + * Components like @method, @authority, and @target-uri are canonicalized inputs + * to the signature base. Approov uses this machinery to validate that a client + * signed the exact request metadata it sent, including method and target URL. + * + * Note: derived components can be bound to the associated request when signing + * responses (using the "req" parameter), which is why we accept both message + * and request objects. + */ +function deriveComponent(component, params, message, req) { + // switch the context of the signing data depending on if the `req` flag was passed + const context = params.has('req') ? req : message; + if (!context) { + throw new Error('Missing request in request-response bound component'); + } + switch (component) { + case '@method': + if (!(0, types_1.isRequest)(context)) { + throw new Error('Cannot derive @method from response'); + } + return [context.method.toUpperCase()]; + case '@target-uri': { + if (!(0, types_1.isRequest)(context)) { + throw new Error('Cannot derive @target-uri on response'); + } + return [context.url.toString()]; + } + case '@authority': { + if (!(0, types_1.isRequest)(context)) { + throw new Error('Cannot derive @authority on response'); + } + const { port, protocol, hostname } = typeof context.url === 'string' ? new URL(context.url) : context.url; + let authority = hostname.toLowerCase(); + if (port && (protocol === 'http:' && port !== '80' || protocol === 'https:' && port !== '443')) { + authority += `:${port}`; + } + return [authority]; + } + case '@scheme': { + if (!(0, types_1.isRequest)(context)) { + throw new Error('Cannot derive @scheme on response'); + } + const { protocol } = typeof context.url === 'string' ? new URL(context.url) : context.url; + return [protocol.slice(0, -1)]; + } + case '@request-target': { + if (!(0, types_1.isRequest)(context)) { + throw new Error('Cannot derive @request-target on response'); + } + const { pathname, search } = typeof context.url === 'string' ? new URL(context.url) : context.url; + // this is really sketchy because the request-target is actually what is in the raw HTTP header + // so one should avoid signing this value as the application layer just can't know how this + // is formatted + return [`${pathname}${search}`]; + } + case '@path': { + if (!(0, types_1.isRequest)(context)) { + throw new Error('Cannot derive @scheme on response'); + } + const { pathname } = typeof context.url === 'string' ? new URL(context.url) : context.url; + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-2.2.6 + // empty path means use `/` + return [pathname || '/']; + } + case '@query': { + if (!(0, types_1.isRequest)(context)) { + throw new Error('Cannot derive @scheme on response'); + } + const { search } = typeof context.url === 'string' ? new URL(context.url) : context.url; + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-2.2.7 + // absent query params means use `?` + return [search || '?']; + } + case '@query-param': { + if (!(0, types_1.isRequest)(context)) { + throw new Error('Cannot derive @scheme on response'); + } + const { searchParams } = typeof context.url === 'string' ? new URL(context.url) : context.url; + if (!params.has('name')) { + throw new Error('@query-param must have a named parameter'); + } + const name = decodeURIComponent(params.get('name').toString()); + if (!searchParams.has(name)) { + throw new Error(`Expected query parameter "${name}" not found`); + } + return searchParams.getAll(name).map((value) => encodeURIComponent(value)); + } + case '@status': { + if ((0, types_1.isRequest)(context)) { + throw new Error('Cannot obtain @status component for requests'); + } + return [context.status.toString()]; + } + default: + throw new Error(`Unsupported component "${component}"`); + } +} +/** + * Extracts and canonicalizes a header value for the signature base. + * Supports structured-field parsing ("sf"/"key" parameters) and binary + * serialization ("bs" parameter) as described by RFC 9421. + * Approov signatures commonly include headers like "approov-token" or + * "content-digest", so this logic must preserve their exact semantics. + */ +function extractHeader(header, params, { headers }, req) { + const context = params.has('req') ? req?.headers : headers; + if (!context) { + throw new Error('Missing request in request-response bound component'); + } + const headerTuple = Object.entries(context).find(([name]) => name.toLowerCase() === header); + if (!headerTuple) { + throw new Error(`No header "${header}" found in headers`); + } + const values = (Array.isArray(headerTuple[1]) ? headerTuple[1] : [headerTuple[1]]); + if (params.has('bs') && (params.has('sf') || params.has('key'))) { + throw new Error('Cannot have both `bs` and (implicit) `sf` parameters'); + } + if (params.has('sf') || params.has('key')) { + // strict encoding of field + const value = values.join(', '); + const parsed = (0, structured_header_1.parseHeader)(value); + if (params.has('key') && !(parsed instanceof structured_header_1.Dictionary)) { + throw new Error('Unable to parse header as dictionary'); + } + if (params.has('key')) { + const key = params.get('key').toString(); + if (!parsed.has(key)) { + throw new Error(`Unable to find key "${key}" in structured field`); + } + return [parsed.get(key)]; + } + return [parsed.toString()]; + } + if (params.has('bs')) { + return [values.map((val) => { + const encoded = Buffer.from(val.trim().replace(/\n\s*/gm, ' ')); + return `:${encoded.toString('base64')}:`; + }).join(', ')]; + } + // raw encoding + return [values.map((val) => val.trim().replace(/\n\s*/gm, ' ')).join(', ')]; +} +/** + * Normalizes structured-header parameter values into primitives. + * The signature base requires deterministic formatting, so ByteSequence and + * Token instances are converted to their string forms before use. + */ +function normaliseParams(params) { + const map = new Map; + params.forEach((value, key) => { + if (value instanceof structured_headers_1.ByteSequence) { + map.set(key, value.toBase64()); + } + else if (value instanceof structured_headers_1.Token) { + map.set(key, value.toString()); + } + else { + map.set(key, value); + } + }); + return map; +} +/** + * Builds the ordered signature base (list of component/value pairs). + * Each field is parsed as a structured item, then expanded into a concrete + * value derived from the request/response or headers. Approov relies on this + * canonical base to validate that a signed request was not tampered with. + */ +function createSignatureBase(config, res, req) { + return (config.fields).reduce((base, fieldName) => { + const [field, params] = (0, structured_headers_1.parseItem)((0, structured_header_1.quoteString)(fieldName)); + const fieldParams = normaliseParams(params); + const lcFieldName = field.toLowerCase(); + if (lcFieldName !== '@signature-params') { + let value = null; + if (config.componentParser) { + value = config.componentParser(lcFieldName, fieldParams, res, req) ?? null; + } + if (value === null) { + value = field.startsWith('@') ? deriveComponent(lcFieldName, fieldParams, res, req) : extractHeader(lcFieldName, fieldParams, res, req); + } + base.push([(0, structured_headers_1.serializeItem)([field, params]), value]); + } + return base; + }, []); +} +/** + * Formats the signature base into the canonical string for signing/verifying. + * Each line is ": " and the final string is what gets + * signed or verified by the cryptographic algorithm. + */ +function formatSignatureBase(base) { + return base.map(([key, value]) => { + const quotedKey = (0, structured_headers_1.serializeItem)((0, structured_headers_1.parseItem)((0, structured_header_1.quoteString)(key))); + return value.map((val) => `${quotedKey}: ${val}`).join('\n'); + }).join('\n'); +} +/** + * Produces the signature parameters (created, expires, keyid, alg, etc). + * These parameters are embedded in the Signature-Input header and are part of + * the signature base. Approov clients and servers must agree on their values + * to validate message signatures. + */ +function createSigningParameters(config) { + const now = new Date(); + return (config.params ?? types_1.defaultParams).reduce((params, paramName) => { + let value = ''; + switch (paramName.toLowerCase()) { + case 'created': + // created is optional but recommended. If created is supplied but is null, that's an explicit + // instruction to *not* include the created parameter + if (config.paramValues?.created !== null) { + const created = config.paramValues?.created ?? now; + value = Math.floor(created.getTime() / 1000); + } + break; + case 'expires': + // attempt to obtain an explicit expires time, otherwise create one that is 300 seconds after + // creation. Don't add an expires time if there is no created time + if (config.paramValues?.expires || config.paramValues?.created !== null) { + const expires = config.paramValues?.expires ?? new Date((config.paramValues?.created ?? now).getTime() + 300000); + value = Math.floor(expires.getTime() / 1000); + } + break; + case 'keyid': { + // attempt to obtain the keyid omit if missing + const kid = config.paramValues?.keyid ?? config.key.id ?? null; + if (kid) { + value = kid.toString(); + } + break; + } + case 'alg': { + // if there is no alg, but it's listed as a required parameter, we should probably + // throw an error - the problem is that if it's in the default set of params, do we + // really want to throw if there's no keyid? + const alg = config.paramValues?.alg ?? config.key.alg ?? null; + if (alg) { + value = alg.toString(); + } + break; + } + default: + if (config.paramValues?.[paramName] instanceof Date) { + value = Math.floor(config.paramValues[paramName].getTime() / 1000); + } + else if (config.paramValues?.[paramName]) { + value = config.paramValues[paramName]; + } + } + if (value) { + params.set(paramName, value); + } + return params; + }, new Map()); +} +/** + * Inserts or appends Signature and Signature-Input headers onto a message. + * When existing signatures are present, we preserve their names and add a + * unique suffix so multiple signatures can coexist on the same request. + */ +function augmentHeaders(headers, signature, signatureInput, name) { + let signatureHeaderName = 'Signature'; + let signatureInputHeaderName = 'Signature-Input'; + let signatureHeader = new Map(); + let inputHeader = new Map(); + // check to see if there are already signature/signature-input headers + // if there are we want to store the current (case-sensitive) name of the header + // and we want to parse out the current values so we can append our new signature + for (const header in headers) { + switch (header.toLowerCase()) { + case 'signature': { + signatureHeaderName = header; + signatureHeader = (0, structured_headers_1.parseDictionary)(Array.isArray(headers[header]) ? headers[header].join(', ') : headers[header]); + break; + } + case 'signature-input': + signatureInputHeaderName = header; + inputHeader = (0, structured_headers_1.parseDictionary)(Array.isArray(headers[header]) ? headers[header].join(', ') : headers[header]); + break; + } + } + // find a unique signature name for the header. Check if any existing headers already use + // the name we intend to use, if there are, add incrementing numbers to the signature name + // until we have a unique name to use + let signatureName = name ?? 'sig'; + if (signatureHeader.has(signatureName) || inputHeader.has(signatureName)) { + let count = 0; + while (signatureHeader.has(`${signatureName}${count}`) || inputHeader.has(`${signatureName}${count}`)) { + count++; + } + signatureName += count.toString(); + } + // append our signature and signature-inputs to the headers and return + signatureHeader.set(signatureName, [new structured_headers_1.ByteSequence(signature.toString('base64')), new Map()]); + inputHeader.set(signatureName, (0, structured_headers_1.parseList)(signatureInput)[0]); + return { + ...headers, + [signatureHeaderName]: (0, structured_headers_1.serializeDictionary)(signatureHeader), + [signatureInputHeaderName]: (0, structured_headers_1.serializeDictionary)(inputHeader), + }; +} +/** + * High-level signing helper: builds the signature base, signs it, and returns + * a new message with updated Signature/Signature-Input headers. Approov SDKs + * use this flow to attach message signatures to outbound requests. + */ +async function signMessage(config, message, req) { + const signingParameters = createSigningParameters(config); + const signatureBase = createSignatureBase({ + fields: config.fields ?? [], + componentParser: config.componentParser, + }, message, req); + const signatureInput = (0, structured_headers_1.serializeList)([ + [ + signatureBase.map(([item]) => (0, structured_headers_1.parseItem)(item)), + signingParameters, + ], + ]); + signatureBase.push(['"@signature-params"', [signatureInput]]); + const base = formatSignatureBase(signatureBase); + // call sign + const signature = await config.key.sign(Buffer.from(base)); + return { + ...message, + headers: augmentHeaders({ ...message.headers }, signature, signatureInput, config.name), + }; +} +/** + * High-level verification helper: parses signature headers, validates required + * parameters and fields, rebuilds the signature base, and verifies the crypto. + * Returns null when no signature headers exist, true/false when verification + * succeeds/fails, or throws when the signature is malformed. + */ +async function verifyMessage(config, message, req) { + const { signatures, signatureInputs } = Object.entries(message.headers).reduce((accum, [name, value]) => { + switch (name.toLowerCase()) { + case 'signature': + return Object.assign(accum, { + signatures: (0, structured_headers_1.parseDictionary)(Array.isArray(value) ? value.join(', ') : value), + }); + case 'signature-input': + return Object.assign(accum, { + signatureInputs: (0, structured_headers_1.parseDictionary)(Array.isArray(value) ? value.join(', ') : value), + }); + default: + return accum; + } + }, {}); + // no signatures means an indeterminate result + if (!signatures?.size && !signatureInputs?.size) { + return null; + } + // a missing header means we can't verify the signatures + if (!signatures?.size || !signatureInputs?.size) { + throw new Error('Incomplete signature headers'); + } + const now = Math.floor(Date.now() / 1000); + const tolerance = config.tolerance ?? 0; + const notAfter = config.notAfter instanceof Date ? Math.floor(config.notAfter.getTime() / 1000) : config.notAfter ?? now; + const maxAge = config.maxAge ?? null; + const requiredParams = config.requiredParams ?? []; + const requiredFields = config.requiredFields ?? []; + return Array.from(signatureInputs.entries()).reduce(async (prev, [name, input]) => { + const signatureParams = Array.from(input[1].entries()).reduce((params, [key, value]) => { + if (value instanceof structured_headers_1.ByteSequence) { + Object.assign(params, { + [key]: value.toBase64(), + }); + } + else if (value instanceof structured_headers_1.Token) { + Object.assign(params, { + [key]: value.toString(), + }); + } + // Vendored bug note: this repo previously normalized "expired" instead of + // the RFC 9421 "expires" parameter. We now accept both for backward + // compatibility and to avoid breaking clients that relied on the bug. + else if (key === 'created' || key === 'expires' || key === 'expired') { + Object.assign(params, { + [key]: new Date(value * 1000), + }); + } + else { + Object.assign(params, { + [key]: value, + }); + } + return params; + }, {}); + const [result, key] = await Promise.all([ + prev.catch((e) => e), + config.keyLookup(signatureParams), + ]); + // @todo - confirm this is all working as expected + if (config.all && !key) { + throw new errors_1.UnknownKeyError('Unknown key'); + } + if (!key) { + if (result instanceof Error) { + throw result; + } + return result; + } + if (input[1].has('alg') && key.algs?.includes(input[1].get('alg')) === false) { + throw new errors_1.UnsupportedAlgorithmError('Unsupported key algorithm'); + } + if (!(0, structured_headers_1.isInnerList)(input)) { + throw new errors_1.MalformedSignatureError('Malformed signature input'); + } + const hasRequiredParams = requiredParams.every((param) => input[1].has(param)); + if (!hasRequiredParams) { + throw new errors_1.UnacceptableSignatureError('Missing required signature parameters'); + } + // this could be tricky, what if we say "@method" but there is "@method;req" + const hasRequiredFields = requiredFields.every((field) => input[0].some(([fieldName]) => fieldName === field)); + if (!hasRequiredFields) { + throw new errors_1.UnacceptableSignatureError('Missing required signed fields'); + } + if (input[1].has('created')) { + const created = input[1].get('created') - tolerance; + // maxAge overrides expires. + // signature is older than maxAge + if ((maxAge && now - created > maxAge) || created > notAfter) { + throw new errors_1.ExpiredError('Signature is too old'); + } + } + if (input[1].has('expires')) { + const expires = input[1].get('expires') + tolerance; + // expired signature + if (now > expires) { + throw new errors_1.ExpiredError('Signature has expired'); + } + } + // now look to verify the signature! Build the expected "signing base" and verify it! + const fields = input[0].map((item) => (0, structured_headers_1.serializeItem)(item)); + const signingBase = createSignatureBase({ fields, componentParser: config.componentParser }, message, req); + signingBase.push(['"@signature-params"', [(0, structured_headers_1.serializeList)([input])]]); + const base = formatSignatureBase(signingBase); + const signature = signatures.get(name); + if (!signature) { + throw new errors_1.MalformedSignatureError('No corresponding signature for input'); + } + if (!(0, structured_headers_1.isByteSequence)(signature[0])) { + throw new errors_1.MalformedSignatureError('Malformed signature'); + } + return key.verify(Buffer.from(base), Buffer.from(signature[0].toBase64(), 'base64'), signatureParams); + }, Promise.resolve(null)); +} +//# sourceMappingURL=index.js.map diff --git a/vendor/http-message-signatures/lib/httpbis/index.js.map b/vendor/http-message-signatures/lib/httpbis/index.js.map new file mode 100644 index 0000000..63810bc --- /dev/null +++ b/vendor/http-message-signatures/lib/httpbis/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/httpbis/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/index.d.ts b/vendor/http-message-signatures/lib/index.d.ts new file mode 100644 index 0000000..bc92f4b --- /dev/null +++ b/vendor/http-message-signatures/lib/index.d.ts @@ -0,0 +1,6 @@ +export * from './algorithm'; +export * from './types'; +export * from './errors'; +export * as default from './httpbis'; +export * as httpbis from './httpbis'; +export * as cavage from './cavage'; diff --git a/vendor/http-message-signatures/lib/index.js b/vendor/http-message-signatures/lib/index.js new file mode 100644 index 0000000..574db0a --- /dev/null +++ b/vendor/http-message-signatures/lib/index.js @@ -0,0 +1,46 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.cavage = exports.httpbis = exports.default = void 0; +__exportStar(require("./algorithm"), exports); +__exportStar(require("./types"), exports); +__exportStar(require("./errors"), exports); +exports.default = __importStar(require("./httpbis")); +exports.httpbis = __importStar(require("./httpbis")); +exports.cavage = __importStar(require("./cavage")); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/index.js.map b/vendor/http-message-signatures/lib/index.js.map new file mode 100644 index 0000000..9b17fb2 --- /dev/null +++ b/vendor/http-message-signatures/lib/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8CAA4B;AAC5B,0CAAwB;AACxB,2CAAyB;AACzB,qDAAqC;AACrC,qDAAqC;AACrC,mDAAmC"} \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/structured-header.d.ts b/vendor/http-message-signatures/lib/structured-header.d.ts new file mode 100644 index 0000000..b66d732 --- /dev/null +++ b/vendor/http-message-signatures/lib/structured-header.d.ts @@ -0,0 +1,33 @@ +export declare class Dictionary { + private readonly parsed; + private readonly raw; + constructor(input: string); + toString(): string; + serialize(): string; + has(key: string): boolean; + get(key: string): string | undefined; +} +export declare class List { + private readonly parsed; + private readonly raw; + constructor(input: string); + toString(): string; + serialize(): string; +} +export declare class Item { + private readonly parsed; + private readonly raw; + constructor(input: string); + toString(): string; + serialize(): string; +} +export declare function parseHeader(header: string): List | Dictionary | Item; +/** + * This allows consumers of the library to supply field specifications that aren't + * strictly "structured fields". Really a string must start with a `"` but that won't + * tend to happen in our configs. + * + * @param {string} input + * @returns {string} + */ +export declare function quoteString(input: string): string; diff --git a/vendor/http-message-signatures/lib/structured-header.js b/vendor/http-message-signatures/lib/structured-header.js new file mode 100644 index 0000000..00ce779 --- /dev/null +++ b/vendor/http-message-signatures/lib/structured-header.js @@ -0,0 +1,93 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Item = exports.List = exports.Dictionary = void 0; +exports.parseHeader = parseHeader; +exports.quoteString = quoteString; +const structured_headers_1 = require("structured-headers"); +class Dictionary { + constructor(input) { + this.raw = input; + this.parsed = (0, structured_headers_1.parseDictionary)(input); + } + toString() { + return this.serialize(); + } + serialize() { + return (0, structured_headers_1.serializeDictionary)(this.parsed); + } + has(key) { + return this.parsed.has(key); + } + get(key) { + const value = this.parsed.get(key); + if (!value) { + return value; + } + if ((0, structured_headers_1.isInnerList)(value)) { + return (0, structured_headers_1.serializeInnerList)(value); + } + return (0, structured_headers_1.serializeItem)(value); + } +} +exports.Dictionary = Dictionary; +class List { + constructor(input) { + this.raw = input; + this.parsed = (0, structured_headers_1.parseList)(input); + } + toString() { + return this.serialize(); + } + serialize() { + return (0, structured_headers_1.serializeList)(this.parsed); + } +} +exports.List = List; +class Item { + constructor(input) { + this.raw = input; + this.parsed = (0, structured_headers_1.parseItem)(input); + } + toString() { + return this.serialize(); + } + serialize() { + return (0, structured_headers_1.serializeItem)(this.parsed); + } +} +exports.Item = Item; +function parseHeader(header) { + const classes = [List, Dictionary, Item]; + for (let i = 0; i < classes.length; i++) { + try { + return new classes[i](header); + } + catch (e) { + // noop + } + } + throw new Error('Unable to parse header as structured field'); +} +/** + * This allows consumers of the library to supply field specifications that aren't + * strictly "structured fields". Really a string must start with a `"` but that won't + * tend to happen in our configs. + * + * @param {string} input + * @returns {string} + */ +function quoteString(input) { + // if it's not quoted, attempt to quote + if (!input.startsWith('"')) { + // try to split the structured field + const [name, ...rest] = input.split(';'); + // no params, just quote the whole thing + if (!rest.length) { + return `"${name}"`; + } + // quote the first part and put the rest back as it was + return `"${name}";${rest.join(';')}`; + } + return input; +} +//# sourceMappingURL=structured-header.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/structured-header.js.map b/vendor/http-message-signatures/lib/structured-header.js.map new file mode 100644 index 0000000..da57c95 --- /dev/null +++ b/vendor/http-message-signatures/lib/structured-header.js.map @@ -0,0 +1 @@ +{"version":3,"file":"structured-header.js","sourceRoot":"","sources":["../src/structured-header.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/types/index.d.ts b/vendor/http-message-signatures/lib/types/index.d.ts new file mode 100644 index 0000000..acf6266 --- /dev/null +++ b/vendor/http-message-signatures/lib/types/index.d.ts @@ -0,0 +1,161 @@ +export interface Request { + method: string; + url: string | URL; + headers: Record; +} +export interface Response { + status: number; + headers: Record; +} +export type Signer = (data: Buffer) => Promise; +export type Verifier = (data: Buffer, signature: Buffer, parameters?: SignatureParameters) => Promise; +export type VerifierFinder = (parameters: SignatureParameters) => Promise; +export type Algorithm = 'rsa-v1_5-sha256' | 'ecdsa-p256-sha256' | 'ecdsa-p384-sha384' | 'ed25519' | 'hmac-sha256' | 'rsa-pss-sha512' | string; +export interface SigningKey { + /** + * The ID of this key + */ + id?: string; + /** + * The algorithm to sign with + */ + alg?: Algorithm; + /** + * The Signer function + */ + sign: Signer; +} +export interface VerifyingKey { + /** + * The ID of this key + */ + id?: string; + /** + * The supported algorithms for this key + */ + algs?: Algorithm[]; + /** + * The Verify function + */ + verify: Verifier; +} +/** + * The signature parameters to include in signing + */ +export interface SignatureParameters { + /** + * The created time for the signature. `null` indicates not to populate the `created` time + * default: Date.now() + */ + created?: Date | null; + /** + * The time the signature should be deemed to have expired + * default: Date.now() + 5 mins + */ + expires?: Date; + /** + * A nonce for the request + */ + nonce?: string; + /** + * The algorithm the signature is signed with (overrides the alg provided by the signing key) + */ + alg?: string; + /** + * The key id the signature is signed with (overrides the keyid provided by the signing key) + */ + keyid?: string; + /** + * A tag parameter for the signature + */ + tag?: string; + [param: string]: Date | number | string | null | undefined; +} +/** + * Default parameters to use when signing a request if none are supplied by the consumer + */ +export declare const defaultParams: string[]; +/** + * A component parser supplied by the consumer to allow applications to define their own logic for + * extracting components for use in the signature base. + * + * This can be useful in circumstances where the application has agreed a specific standard or way + * of extracting components from messages and/or when new components are added to the specification + * but not yet supported by the library. + * + * Return null to defer to internal logic + */ +export type ComponentParser = (name: string, params: Map, message: Request | Response, req?: Request) => string[] | null; +export interface CommonConfig { + /** + * A component user supplied component parser + */ + componentParser?: ComponentParser; +} +export interface SignConfig extends CommonConfig { + key: SigningKey; + /** + * The name to try to use for the signature + * Default: 'sig' + */ + name?: string; + /** + * The parameters to add to the signature + * Default: see defaultParams + */ + params?: string[]; + /** + * The HTTP fields / derived component names to sign + * Default: none + */ + fields?: string[]; + /** + * Specified parameter values to use (eg: created time, expires time, etc) + * This can be used by consumers to override the default expiration time or explicitly opt-out + * of adding creation time (by setting `created: null`) + */ + paramValues?: SignatureParameters; + /** + * A list of supported algorithms + */ + algs?: Algorithm[]; +} +/** + * Options when verifying signatures + */ +export interface VerifyConfig extends CommonConfig { + keyLookup: VerifierFinder; + /** + * A date that the signature can't have been marked as `created` after + * Default: Date.now() + tolerance + */ + notAfter?: Date | number; + /** + * The maximum age of the signature - this effectively overrides the `expires` value for the + * signature (unless the expires age is less than the maxAge specified) + * if provided + */ + maxAge?: number; + /** + * A clock tolerance when verifying created/expires times + * Default: 0 + */ + tolerance?: number; + /** + * Any parameters that *must* be in the signature (eg: require a created time) + * Default: [] + */ + requiredParams?: string[]; + /** + * Any fields that *must* be in the signature (eg: Authorization, Digest, etc) + * Default: [] + */ + requiredFields?: string[]; + /** + * Verify every signature in the request. By default, only 1 signature will need to be valid + * for the verification to pass. + * Default: false + */ + all?: boolean; +} +export declare function isRequest(obj: Request | Response): obj is Request; diff --git a/vendor/http-message-signatures/lib/types/index.js b/vendor/http-message-signatures/lib/types/index.js new file mode 100644 index 0000000..6903ed1 --- /dev/null +++ b/vendor/http-message-signatures/lib/types/index.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.defaultParams = void 0; +exports.isRequest = isRequest; +/** + * Default parameters to use when signing a request if none are supplied by the consumer + */ +exports.defaultParams = [ + 'keyid', + 'alg', + 'created', + 'expires', +]; +function isRequest(obj) { + return !!obj.method; +} +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/lib/types/index.js.map b/vendor/http-message-signatures/lib/types/index.js.map new file mode 100644 index 0000000..ed9e495 --- /dev/null +++ b/vendor/http-message-signatures/lib/types/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":";;;AAiLA,8BAEC;AAnGD;;GAEG;AACU,QAAA,aAAa,GAAG;IACzB,OAAO;IACP,KAAK;IACL,SAAS;IACT,SAAS;CACZ,CAAC;AAyFF,SAAgB,SAAS,CAAC,GAAuB;IAC7C,OAAO,CAAC,CAAE,GAAe,CAAC,MAAM,CAAC;AACrC,CAAC"} \ No newline at end of file diff --git a/vendor/http-message-signatures/node_modules/structured-headers/LICENSE b/vendor/http-message-signatures/node_modules/structured-headers/LICENSE new file mode 100644 index 0000000..064d69d --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018-2023 Bad Gateway Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/build b/vendor/http-message-signatures/node_modules/structured-headers/dist/build new file mode 100644 index 0000000..e69de29 diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/index.d.ts b/vendor/http-message-signatures/node_modules/structured-headers/dist/index.d.ts new file mode 100644 index 0000000..1bed8df --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/index.d.ts @@ -0,0 +1,5 @@ +export * from './serializer'; +export * from './parser'; +export * from './types'; +export * from './util'; +export { Token } from './token'; diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/index.js b/vendor/http-message-signatures/node_modules/structured-headers/dist/index.js new file mode 100644 index 0000000..de9305a --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/index.js @@ -0,0 +1,24 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Token = void 0; +__exportStar(require("./serializer"), exports); +__exportStar(require("./parser"), exports); +__exportStar(require("./types"), exports); +__exportStar(require("./util"), exports); +var token_1 = require("./token"); +Object.defineProperty(exports, "Token", { enumerable: true, get: function () { return token_1.Token; } }); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/index.js.map b/vendor/http-message-signatures/node_modules/structured-headers/dist/index.js.map new file mode 100644 index 0000000..11eafa4 --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,+CAA6B;AAC7B,2CAAyB;AACzB,0CAAwB;AACxB,yCAAuB;AACvB,iCAAgC;AAAvB,8FAAA,KAAK,OAAA"} \ No newline at end of file diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/parser.d.ts b/vendor/http-message-signatures/node_modules/structured-headers/dist/parser.d.ts new file mode 100644 index 0000000..d6dc55c --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/parser.d.ts @@ -0,0 +1,40 @@ +import { Dictionary, List, Item } from './types'; +export declare function parseDictionary(input: string): Dictionary; +export declare function parseList(input: string): List; +export declare function parseItem(input: string): Item; +export declare class ParseError extends Error { + constructor(position: number, message: string); +} +export default class Parser { + input: string; + pos: number; + constructor(input: string); + parseDictionary(): Dictionary; + parseList(): List; + parseItem(standaloneItem?: boolean): Item; + private parseItemOrInnerList; + private parseInnerList; + private parseBareItem; + private parseParameters; + private parseIntegerOrDecimal; + private parseString; + private parseToken; + private parseByteSequence; + private parseBoolean; + private parseKey; + /** + * Looks at the next character without advancing the cursor. + * + * Returns undefined if we were at the end of the string. + */ + private lookChar; + /** + * Checks if the next character is 'char', and fail otherwise. + */ + private expectChar; + private getChar; + private eof; + private skipOWS; + private skipWS; + private checkTrail; +} diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/parser.js b/vendor/http-message-signatures/node_modules/structured-headers/dist/parser.js new file mode 100644 index 0000000..7848190 --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/parser.js @@ -0,0 +1,351 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ParseError = exports.parseItem = exports.parseList = exports.parseDictionary = void 0; +const types_1 = require("./types"); +const token_1 = require("./token"); +const util_1 = require("./util"); +function parseDictionary(input) { + const parser = new Parser(input); + return parser.parseDictionary(); +} +exports.parseDictionary = parseDictionary; +function parseList(input) { + const parser = new Parser(input); + return parser.parseList(); +} +exports.parseList = parseList; +function parseItem(input) { + const parser = new Parser(input); + return parser.parseItem(); +} +exports.parseItem = parseItem; +class ParseError extends Error { + constructor(position, message) { + super(`Parse error: ${message} at offset ${position}`); + } +} +exports.ParseError = ParseError; +class Parser { + constructor(input) { + this.input = input; + this.pos = 0; + } + parseDictionary() { + this.skipWS(); + const dictionary = new Map(); + while (!this.eof()) { + const thisKey = this.parseKey(); + let member; + if (this.lookChar() === '=') { + this.pos++; + member = this.parseItemOrInnerList(); + } + else { + member = [true, this.parseParameters()]; + } + dictionary.set(thisKey, member); + this.skipOWS(); + if (this.eof()) { + return dictionary; + } + this.expectChar(','); + this.pos++; + this.skipOWS(); + if (this.eof()) { + throw new ParseError(this.pos, 'Dictionary contained a trailing comma'); + } + } + return dictionary; + } + parseList() { + this.skipWS(); + const members = []; + while (!this.eof()) { + members.push(this.parseItemOrInnerList()); + this.skipOWS(); + if (this.eof()) { + return members; + } + this.expectChar(','); + this.pos++; + this.skipOWS(); + if (this.eof()) { + throw new ParseError(this.pos, 'A list may not end with a trailing comma'); + } + } + return members; + } + parseItem(standaloneItem = true) { + if (standaloneItem) + this.skipWS(); + const result = [ + this.parseBareItem(), + this.parseParameters() + ]; + if (standaloneItem) + this.checkTrail(); + return result; + } + parseItemOrInnerList() { + if (this.lookChar() === '(') { + return this.parseInnerList(); + } + else { + return this.parseItem(false); + } + } + parseInnerList() { + this.expectChar('('); + this.pos++; + const innerList = []; + while (!this.eof()) { + this.skipWS(); + if (this.lookChar() === ')') { + this.pos++; + return [ + innerList, + this.parseParameters() + ]; + } + innerList.push(this.parseItem(false)); + const nextChar = this.lookChar(); + if (nextChar !== ' ' && nextChar !== ')') { + throw new ParseError(this.pos, 'Expected a whitespace or ) after every item in an inner list'); + } + } + throw new ParseError(this.pos, 'Could not find end of inner list'); + } + parseBareItem() { + const char = this.lookChar(); + if (char === undefined) { + throw new ParseError(this.pos, 'Unexpected end of string'); + } + if (char.match(/^[-0-9]/)) { + return this.parseIntegerOrDecimal(); + } + if (char === '"') { + return this.parseString(); + } + if (char.match(/^[A-Za-z*]/)) { + return this.parseToken(); + } + if (char === ':') { + return this.parseByteSequence(); + } + if (char === '?') { + return this.parseBoolean(); + } + throw new ParseError(this.pos, 'Unexpected input'); + } + parseParameters() { + const parameters = new Map(); + while (!this.eof()) { + const char = this.lookChar(); + if (char !== ';') { + break; + } + this.pos++; + this.skipWS(); + const key = this.parseKey(); + let value = true; + if (this.lookChar() === '=') { + this.pos++; + value = this.parseBareItem(); + } + parameters.set(key, value); + } + return parameters; + } + parseIntegerOrDecimal() { + let type = 'integer'; + let sign = 1; + let inputNumber = ''; + if (this.lookChar() === '-') { + sign = -1; + this.pos++; + } + // The spec wants this check but it's unreachable code. + //if (this.eof()) { + // throw new ParseError(this.pos, 'Empty integer'); + //} + if (!isDigit(this.lookChar())) { + throw new ParseError(this.pos, 'Expected a digit (0-9)'); + } + while (!this.eof()) { + const char = this.getChar(); + if (isDigit(char)) { + inputNumber += char; + } + else if (type === 'integer' && char === '.') { + if (inputNumber.length > 12) { + throw new ParseError(this.pos, 'Exceeded maximum decimal length'); + } + inputNumber += '.'; + type = 'decimal'; + } + else { + // We need to 'prepend' the character, so it's just a rewind + this.pos--; + break; + } + if (type === 'integer' && inputNumber.length > 15) { + throw new ParseError(this.pos, 'Exceeded maximum integer length'); + } + if (type === 'decimal' && inputNumber.length > 16) { + throw new ParseError(this.pos, 'Exceeded maximum decimal length'); + } + } + if (type === 'integer') { + return parseInt(inputNumber, 10) * sign; + } + else { + if (inputNumber.endsWith('.')) { + throw new ParseError(this.pos, 'Decimal cannot end on a period'); + } + if (inputNumber.split('.')[1].length > 3) { + throw new ParseError(this.pos, 'Number of digits after the decimal point cannot exceed 3'); + } + return parseFloat(inputNumber) * sign; + } + } + parseString() { + let outputString = ''; + this.expectChar('"'); + this.pos++; + while (!this.eof()) { + const char = this.getChar(); + if (char === '\\') { + if (this.eof()) { + throw new ParseError(this.pos, 'Unexpected end of input'); + } + const nextChar = this.getChar(); + if (nextChar !== '\\' && nextChar !== '"') { + throw new ParseError(this.pos, 'A backslash must be followed by another backslash or double quote'); + } + outputString += nextChar; + } + else if (char === '"') { + return outputString; + } + else if (!(0, util_1.isAscii)(char)) { + throw new ParseError(this.pos, 'Strings must be in the ASCII range'); + } + else { + outputString += char; + } + } + throw new ParseError(this.pos, 'Unexpected end of input'); + } + parseToken() { + // The specification wants this check, but it's an unreachable code block. + // if (!/^[A-Za-z*]/.test(this.lookChar())) { + // throw new ParseError(this.pos, 'A token must begin with an asterisk or letter (A-Z, a-z)'); + //} + let outputString = ''; + while (!this.eof()) { + const char = this.lookChar(); + if (char === undefined || !/^[:/!#$%&'*+\-.^_`|~A-Za-z0-9]$/.test(char)) { + return new token_1.Token(outputString); + } + outputString += this.getChar(); + } + return new token_1.Token(outputString); + } + parseByteSequence() { + this.expectChar(':'); + this.pos++; + const endPos = this.input.indexOf(':', this.pos); + if (endPos === -1) { + throw new ParseError(this.pos, 'Could not find a closing ":" character to mark end of Byte Sequence'); + } + const b64Content = this.input.substring(this.pos, endPos); + this.pos += b64Content.length + 1; + if (!/^[A-Za-z0-9+/=]*$/.test(b64Content)) { + throw new ParseError(this.pos, 'ByteSequence does not contain a valid base64 string'); + } + return new types_1.ByteSequence(b64Content); + } + parseBoolean() { + this.expectChar('?'); + this.pos++; + const char = this.getChar(); + if (char === '1') { + return true; + } + if (char === '0') { + return false; + } + throw new ParseError(this.pos, 'Unexpected character. Expected a "1" or a "0"'); + } + parseKey() { + var _a; + if (!((_a = this.lookChar()) === null || _a === void 0 ? void 0 : _a.match(/^[a-z*]/))) { + throw new ParseError(this.pos, 'A key must begin with an asterisk or letter (a-z)'); + } + let outputString = ''; + while (!this.eof()) { + const char = this.lookChar(); + if (char === undefined || !/^[a-z0-9_\-.*]$/.test(char)) { + return outputString; + } + outputString += this.getChar(); + } + return outputString; + } + /** + * Looks at the next character without advancing the cursor. + * + * Returns undefined if we were at the end of the string. + */ + lookChar() { + return this.input[this.pos]; + } + /** + * Checks if the next character is 'char', and fail otherwise. + */ + expectChar(char) { + if (this.lookChar() !== char) { + throw new ParseError(this.pos, `Expected ${char}`); + } + } + getChar() { + return this.input[this.pos++]; + } + eof() { + return this.pos >= this.input.length; + } + // Advances the pointer to skip all whitespace. + skipOWS() { + while (true) { + const c = this.input.substr(this.pos, 1); + if (c === ' ' || c === '\t') { + this.pos++; + } + else { + break; + } + } + } + // Advances the pointer to skip all spaces + skipWS() { + while (this.lookChar() === ' ') { + this.pos++; + } + } + // At the end of parsing, we need to make sure there are no bytes after the + // header except whitespace. + checkTrail() { + this.skipWS(); + if (!this.eof()) { + throw new ParseError(this.pos, 'Unexpected characters at end of input'); + } + } +} +exports.default = Parser; +const isDigitRegex = /^[0-9]$/; +function isDigit(char) { + if (char === undefined) + return false; + return isDigitRegex.test(char); +} +//# sourceMappingURL=parser.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/parser.js.map b/vendor/http-message-signatures/node_modules/structured-headers/dist/parser.js.map new file mode 100644 index 0000000..f40fdc6 --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/parser.js.map @@ -0,0 +1 @@ +{"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/serializer.d.ts b/vendor/http-message-signatures/node_modules/structured-headers/dist/serializer.d.ts new file mode 100644 index 0000000..068212d --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/serializer.d.ts @@ -0,0 +1,17 @@ +import { BareItem, ByteSequence, Dictionary, InnerList, Item, List, Parameters } from './types'; +import { Token } from './token'; +export declare class SerializeError extends Error { +} +export declare function serializeList(input: List): string; +export declare function serializeDictionary(input: Dictionary): string; +export declare function serializeItem(input: Item): string; +export declare function serializeInnerList(input: InnerList): string; +export declare function serializeBareItem(input: BareItem): string; +export declare function serializeInteger(input: number): string; +export declare function serializeDecimal(input: number): string; +export declare function serializeString(input: string): string; +export declare function serializeBoolean(input: boolean): string; +export declare function serializeByteSequence(input: ByteSequence): string; +export declare function serializeToken(input: Token): string; +export declare function serializeParameters(input: Parameters): string; +export declare function serializeKey(input: string): string; diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/serializer.js b/vendor/http-message-signatures/node_modules/structured-headers/dist/serializer.js new file mode 100644 index 0000000..c9efbce --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/serializer.js @@ -0,0 +1,122 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.serializeKey = exports.serializeParameters = exports.serializeToken = exports.serializeByteSequence = exports.serializeBoolean = exports.serializeString = exports.serializeDecimal = exports.serializeInteger = exports.serializeBareItem = exports.serializeInnerList = exports.serializeItem = exports.serializeDictionary = exports.serializeList = exports.SerializeError = void 0; +const types_1 = require("./types"); +const token_1 = require("./token"); +const util_1 = require("./util"); +class SerializeError extends Error { +} +exports.SerializeError = SerializeError; +function serializeList(input) { + return input.map(value => { + if ((0, util_1.isInnerList)(value)) { + return serializeInnerList(value); + } + else { + return serializeItem(value); + } + }).join(', '); +} +exports.serializeList = serializeList; +function serializeDictionary(input) { + return Array.from(input.entries()).map(([key, value]) => { + let out = serializeKey(key); + if (value[0] === true) { + out += serializeParameters(value[1]); + } + else { + out += '='; + if ((0, util_1.isInnerList)(value)) { + out += serializeInnerList(value); + } + else { + out += serializeItem(value); + } + } + return out; + }).join(', '); +} +exports.serializeDictionary = serializeDictionary; +function serializeItem(input) { + return serializeBareItem(input[0]) + serializeParameters(input[1]); +} +exports.serializeItem = serializeItem; +function serializeInnerList(input) { + return `(${input[0].map(value => serializeItem(value)).join(' ')})${serializeParameters(input[1])}`; +} +exports.serializeInnerList = serializeInnerList; +function serializeBareItem(input) { + if (typeof input === 'number') { + if (Number.isInteger(input)) { + return serializeInteger(input); + } + return serializeDecimal(input); + } + if (typeof input === 'string') { + return serializeString(input); + } + if (input instanceof token_1.Token) { + return serializeToken(input); + } + if (input instanceof types_1.ByteSequence) { + return serializeByteSequence(input); + } + if (typeof input === 'boolean') { + return serializeBoolean(input); + } + throw new SerializeError(`Cannot serialize values of type ${typeof input}`); +} +exports.serializeBareItem = serializeBareItem; +function serializeInteger(input) { + if (input < -999999999999999 || input > 999999999999999) { + throw new SerializeError('Structured headers can only encode integers in the range range of -999,999,999,999,999 to 999,999,999,999,999 inclusive'); + } + return input.toString(); +} +exports.serializeInteger = serializeInteger; +function serializeDecimal(input) { + const out = input.toFixed(3).replace(/0+$/, ''); + const signifantDigits = out.split('.')[0].replace('-', '').length; + if (signifantDigits > 12) { + throw new SerializeError('Fractional numbers are not allowed to have more than 12 significant digits before the decimal point'); + } + return out; +} +exports.serializeDecimal = serializeDecimal; +function serializeString(input) { + if (!(0, util_1.isAscii)(input)) { + throw new SerializeError('Only ASCII strings may be serialized'); + } + return `"${input.replace(/("|\\)/g, (v) => '\\' + v)}"`; +} +exports.serializeString = serializeString; +function serializeBoolean(input) { + return input ? '?1' : '?0'; +} +exports.serializeBoolean = serializeBoolean; +function serializeByteSequence(input) { + return `:${input.toBase64()}:`; +} +exports.serializeByteSequence = serializeByteSequence; +function serializeToken(input) { + return input.toString(); +} +exports.serializeToken = serializeToken; +function serializeParameters(input) { + return Array.from(input).map(([key, value]) => { + let out = ';' + serializeKey(key); + if (value !== true) { + out += '=' + serializeBareItem(value); + } + return out; + }).join(''); +} +exports.serializeParameters = serializeParameters; +function serializeKey(input) { + if (!(0, util_1.isValidKeyStr)(input)) { + throw new SerializeError('Keys in dictionaries must only contain lowercase letter, numbers, _-*. and must start with a letter or *'); + } + return input; +} +exports.serializeKey = serializeKey; +//# sourceMappingURL=serializer.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/serializer.js.map b/vendor/http-message-signatures/node_modules/structured-headers/dist/serializer.js.map new file mode 100644 index 0000000..5031090 --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/serializer.js.map @@ -0,0 +1 @@ +{"version":3,"file":"serializer.js","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/token.d.ts b/vendor/http-message-signatures/node_modules/structured-headers/dist/token.d.ts new file mode 100644 index 0000000..e0e290d --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/token.d.ts @@ -0,0 +1,5 @@ +export declare class Token { + private value; + constructor(value: string); + toString(): string; +} diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/token.js b/vendor/http-message-signatures/node_modules/structured-headers/dist/token.js new file mode 100644 index 0000000..6c5450f --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/token.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Token = void 0; +const util_1 = require("./util"); +class Token { + constructor(value) { + if (!(0, util_1.isValidTokenStr)(value)) { + throw new TypeError('Invalid character in Token string. Tokens must start with *, A-Z and the rest of the string may only contain a-z, A-Z, 0-9, :/!#$%&\'*+-.^_`|~'); + } + this.value = value; + } + toString() { + return this.value; + } +} +exports.Token = Token; +//# sourceMappingURL=token.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/token.js.map b/vendor/http-message-signatures/node_modules/structured-headers/dist/token.js.map new file mode 100644 index 0000000..378256a --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/token.js.map @@ -0,0 +1 @@ +{"version":3,"file":"token.js","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":";;;AAAA,iCAAyC;AAEzC,MAAa,KAAK;IAGhB,YAAY,KAAa;QAEvB,IAAI,CAAC,IAAA,sBAAe,EAAC,KAAK,CAAC,EAAE;YAC3B,MAAM,IAAI,SAAS,CAAC,gJAAgJ,CAAC,CAAC;SACvK;QACD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IAErB,CAAC;IAED,QAAQ;QAEN,OAAO,IAAI,CAAC,KAAK,CAAC;IAEpB,CAAC;CAEF;AAlBD,sBAkBC"} \ No newline at end of file diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/types.d.ts b/vendor/http-message-signatures/node_modules/structured-headers/dist/types.d.ts new file mode 100644 index 0000000..cd06f0e --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/types.d.ts @@ -0,0 +1,34 @@ +import { Token } from './token'; +/** + * Lists are arrays of zero or more members, each of which can be an Item + * or an Inner List, both of which can be Parameterized + */ +export type List = (InnerList | Item)[]; +/** + * An Inner List is an array of zero or more Items. Both the individual Items + * and the Inner List itself can be Parameterized. + */ +export type InnerList = [Item[], Parameters]; +/** + * Parameters are an ordered map of key-value pairs that are associated with + * an Item or Inner List. The keys are unique within the scope of the + * Parameters they occur within, and the values are bare items (i.e., they + * themselves cannot be parameterized + */ +export type Parameters = Map; +/** + * Dictionaries are ordered maps of key-value pairs, where the keys are short + * textual strings and the values are Items or arrays of Items, both of which + * can be Parameterized. + * + * There can be zero or more members, and their keys are unique in the scope + * of the Dictionary they occur within. + */ +export type Dictionary = Map; +export declare class ByteSequence { + base64Value: string; + constructor(base64Value: string); + toBase64(): string; +} +export type BareItem = number | string | Token | ByteSequence | boolean; +export type Item = [BareItem, Parameters]; diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/types.js b/vendor/http-message-signatures/node_modules/structured-headers/dist/types.js new file mode 100644 index 0000000..904ab18 --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/types.js @@ -0,0 +1,13 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ByteSequence = void 0; +class ByteSequence { + constructor(base64Value) { + this.base64Value = base64Value; + } + toBase64() { + return this.base64Value; + } +} +exports.ByteSequence = ByteSequence; +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/types.js.map b/vendor/http-message-signatures/node_modules/structured-headers/dist/types.js.map new file mode 100644 index 0000000..8265887 --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AAgCA,MAAa,YAAY;IAGvB,YAAY,WAAmB;QAE7B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IAEjC,CAAC;IAED,QAAQ;QAEN,OAAO,IAAI,CAAC,WAAW,CAAC;IAE1B,CAAC;CAEF;AAfD,oCAeC"} \ No newline at end of file diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/util.d.ts b/vendor/http-message-signatures/node_modules/structured-headers/dist/util.d.ts new file mode 100644 index 0000000..73e021d --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/util.d.ts @@ -0,0 +1,6 @@ +import { Item, InnerList, BareItem, ByteSequence } from './types'; +export declare function isAscii(str: string): boolean; +export declare function isValidTokenStr(str: string): boolean; +export declare function isValidKeyStr(str: string): boolean; +export declare function isInnerList(input: Item | InnerList): input is InnerList; +export declare function isByteSequence(input: BareItem): input is ByteSequence; diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/util.js b/vendor/http-message-signatures/node_modules/structured-headers/dist/util.js new file mode 100644 index 0000000..acd80b6 --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/util.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isByteSequence = exports.isInnerList = exports.isValidKeyStr = exports.isValidTokenStr = exports.isAscii = void 0; +const asciiRe = /^[\x20-\x7E]*$/; +const tokenRe = /^[a-zA-Z*][:/!#$%&'*+\-.^_`|~A-Za-z0-9]*$/; +const keyRe = /^[a-z*][*\-_.a-z0-9]*$/; +function isAscii(str) { + return asciiRe.test(str); +} +exports.isAscii = isAscii; +function isValidTokenStr(str) { + return tokenRe.test(str); +} +exports.isValidTokenStr = isValidTokenStr; +function isValidKeyStr(str) { + return keyRe.test(str); +} +exports.isValidKeyStr = isValidKeyStr; +function isInnerList(input) { + return Array.isArray(input[0]); +} +exports.isInnerList = isInnerList; +function isByteSequence(input) { + return typeof input === 'object' && 'base64Value' in input; +} +exports.isByteSequence = isByteSequence; +//# sourceMappingURL=util.js.map \ No newline at end of file diff --git a/vendor/http-message-signatures/node_modules/structured-headers/dist/util.js.map b/vendor/http-message-signatures/node_modules/structured-headers/dist/util.js.map new file mode 100644 index 0000000..cdb77bc --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/dist/util.js.map @@ -0,0 +1 @@ +{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;AAEA,MAAM,OAAO,GAAG,gBAAgB,CAAC;AACjC,MAAM,OAAO,GAAG,2CAA2C,CAAC;AAC5D,MAAM,KAAK,GAAG,wBAAwB,CAAC;AAEvC,SAAgB,OAAO,CAAC,GAAW;IAEjC,OAAO,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE3B,CAAC;AAJD,0BAIC;AAED,SAAgB,eAAe,CAAC,GAAW;IAEzC,OAAO,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE3B,CAAC;AAJD,0CAIC;AAED,SAAgB,aAAa,CAAC,GAAW;IAEvC,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAEzB,CAAC;AAJD,sCAIC;AAGD,SAAgB,WAAW,CAAC,KAAuB;IAEjD,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAEjC,CAAC;AAJD,kCAIC;AAGD,SAAgB,cAAc,CAAC,KAAe;IAE5C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,aAAa,IAAI,KAAK,CAAC;AAE7D,CAAC;AAJD,wCAIC"} \ No newline at end of file diff --git a/vendor/http-message-signatures/node_modules/structured-headers/package.json b/vendor/http-message-signatures/node_modules/structured-headers/package.json new file mode 100644 index 0000000..28f6abe --- /dev/null +++ b/vendor/http-message-signatures/node_modules/structured-headers/package.json @@ -0,0 +1,79 @@ +{ + "name": "structured-headers", + "version": "1.0.1", + "description": "Implementation of RFC8941, structured headers for HTTP.", + "main": "dist/index.js", + "scripts": { + "test": "make test", + "prepare": "make build", + "build": "make build" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/evert/structured-header.git" + }, + "keywords": [ + "http", + "structured-header", + "structured-fields", + "structured fields", + "RFC8941", + "headers" + ], + "author": "Evert Pot ", + "license": "MIT", + "bugs": { + "url": "https://github.com/evert/structured-header/issues" + }, + "files": [ + "src/", + "dist/", + "browser/structured-header.min.js", + "browser/structured-header.min.js.map", + "LICENSE" + ], + "homepage": "https://github.com/evert/structured-header#readme", + "devDependencies": { + "@types/chai": "^4.3.3", + "@types/mocha": "^10.0.1", + "@types/node": "^12.20.13", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "base32-decode": "^1.0.0", + "base32-encode": "^1.1.1", + "chai": "^4.2.0", + "eslint": "^8.23.0", + "mocha": "^10.0.0", + "nyc": "^15.1.0", + "ts-node": "^10.0.0", + "typescript": "^5.1.3", + "webpack": "^5.86.0", + "webpack-cli": "^5.1.4" + }, + "nyc": { + "extension": [ + ".ts" + ] + }, + "mocha": { + "require": [ + "ts-node/register" + ], + "recursive": true, + "extension": [ + "ts", + "js", + "tsx" + ], + "exit": true + }, + "browserslist": [ + "last 2 versions", + "not ie 11", + "not op_mini all" + ], + "engines": { + "npm": ">=6", + "node": ">= 14" + } +}