@blamejs/blamejs-shop 0.5.12 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +632 -416
- package/lib/vendor/blamejs/.clusterfuzzlite/Dockerfile +21 -10
- package/lib/vendor/blamejs/.clusterfuzzlite/build.sh +30 -9
- package/lib/vendor/blamejs/.github/dependabot.yml +8 -0
- package/lib/vendor/blamejs/.github/workflows/actions-lint.yml +3 -3
- package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +7 -2
- package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +7 -2
- package/lib/vendor/blamejs/.github/workflows/ci.yml +21 -21
- package/lib/vendor/blamejs/.github/workflows/codeql.yml +3 -3
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +4 -4
- package/lib/vendor/blamejs/.github/workflows/release-container.yml +7 -7
- package/lib/vendor/blamejs/.github/workflows/scorecard.yml +3 -3
- package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +1 -1
- package/lib/vendor/blamejs/.gitignore +2 -0
- package/lib/vendor/blamejs/CHANGELOG.md +122 -0
- package/lib/vendor/blamejs/NOTICE +26 -1
- package/lib/vendor/blamejs/SECURITY.md +1 -1
- package/lib/vendor/blamejs/api-snapshot.json +207 -5
- package/lib/vendor/blamejs/docker/keycloak/realm-blamejs-test.json +29 -0
- package/lib/vendor/blamejs/examples/wiki/DEPLOY.md +1 -0
- package/lib/vendor/blamejs/examples/wiki/lib/build-app.js +31 -0
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +109 -0
- package/lib/vendor/blamejs/examples/wiki/nginx.conf +106 -0
- package/lib/vendor/blamejs/examples/wiki/package-lock.json +1 -1
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +92 -0
- package/lib/vendor/blamejs/fuzz/asn1-der.fuzz.js +28 -0
- package/lib/vendor/blamejs/fuzz/cms-codec.fuzz.js +17 -0
- package/lib/vendor/blamejs/fuzz/link-header.fuzz.js +18 -0
- package/lib/vendor/blamejs/index.js +6 -0
- package/lib/vendor/blamejs/lib/a2a.js +4 -4
- package/lib/vendor/blamejs/lib/agent-idempotency.js +18 -2
- package/lib/vendor/blamejs/lib/agent-saga.js +36 -8
- package/lib/vendor/blamejs/lib/agent-snapshot.js +63 -6
- package/lib/vendor/blamejs/lib/app-shutdown.js +23 -20
- package/lib/vendor/blamejs/lib/archive-read.js +8 -0
- package/lib/vendor/blamejs/lib/archive-tar.js +12 -2
- package/lib/vendor/blamejs/lib/arg-parser.js +49 -5
- package/lib/vendor/blamejs/lib/asn1-der.js +12 -0
- package/lib/vendor/blamejs/lib/audit-sign.js +47 -1
- package/lib/vendor/blamejs/lib/audit.js +71 -8
- package/lib/vendor/blamejs/lib/auth/ciba.js +14 -2
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +58 -47
- package/lib/vendor/blamejs/lib/auth/jwt.js +34 -0
- package/lib/vendor/blamejs/lib/auth/oauth.js +90 -13
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +20 -17
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +214 -23
- package/lib/vendor/blamejs/lib/auth/saml.js +38 -20
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +10 -4
- package/lib/vendor/blamejs/lib/backup/bundle.js +13 -3
- package/lib/vendor/blamejs/lib/backup/index.js +16 -5
- package/lib/vendor/blamejs/lib/backup/manifest.js +18 -5
- package/lib/vendor/blamejs/lib/base32.js +21 -0
- package/lib/vendor/blamejs/lib/break-glass.js +19 -10
- package/lib/vendor/blamejs/lib/bundler.js +5 -1
- package/lib/vendor/blamejs/lib/cache-status.js +3 -7
- package/lib/vendor/blamejs/lib/calendar.js +21 -1
- package/lib/vendor/blamejs/lib/cbor.js +12 -1
- package/lib/vendor/blamejs/lib/cert.js +29 -11
- package/lib/vendor/blamejs/lib/cli.js +94 -24
- package/lib/vendor/blamejs/lib/cloud-events.js +2 -2
- package/lib/vendor/blamejs/lib/cluster-provider-db.js +13 -3
- package/lib/vendor/blamejs/lib/codepoint-class.js +85 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act-prohibited.js +8 -4
- package/lib/vendor/blamejs/lib/compliance-ai-act-transparency.js +19 -4
- package/lib/vendor/blamejs/lib/compliance-ai-act.js +6 -8
- package/lib/vendor/blamejs/lib/content-credentials.js +1 -1
- package/lib/vendor/blamejs/lib/content-digest.js +2 -1
- package/lib/vendor/blamejs/lib/cookies.js +13 -3
- package/lib/vendor/blamejs/lib/cose.js +31 -16
- package/lib/vendor/blamejs/lib/crypto-field.js +32 -0
- package/lib/vendor/blamejs/lib/crypto-oprf.js +11 -2
- package/lib/vendor/blamejs/lib/crypto-xwing.js +22 -4
- package/lib/vendor/blamejs/lib/crypto.js +314 -19
- package/lib/vendor/blamejs/lib/csp.js +11 -3
- package/lib/vendor/blamejs/lib/daemon.js +270 -24
- package/lib/vendor/blamejs/lib/dark-patterns.js +5 -2
- package/lib/vendor/blamejs/lib/db-declare-view.js +2 -2
- package/lib/vendor/blamejs/lib/db-query.js +34 -7
- package/lib/vendor/blamejs/lib/db.js +18 -6
- package/lib/vendor/blamejs/lib/dbsc.js +17 -0
- package/lib/vendor/blamejs/lib/dev.js +21 -2
- package/lib/vendor/blamejs/lib/dsr.js +157 -102
- package/lib/vendor/blamejs/lib/external-db.js +41 -2
- package/lib/vendor/blamejs/lib/file-upload.js +25 -7
- package/lib/vendor/blamejs/lib/flag-providers.js +27 -15
- package/lib/vendor/blamejs/lib/forms.js +113 -27
- package/lib/vendor/blamejs/lib/gate-contract.js +23 -10
- package/lib/vendor/blamejs/lib/gdpr-ropa.js +8 -1
- package/lib/vendor/blamejs/lib/guard-agent-registry.js +1 -1
- package/lib/vendor/blamejs/lib/guard-archive.js +2 -2
- package/lib/vendor/blamejs/lib/guard-cidr.js +27 -1
- package/lib/vendor/blamejs/lib/guard-graphql.js +30 -11
- package/lib/vendor/blamejs/lib/guard-html.js +26 -53
- package/lib/vendor/blamejs/lib/guard-image.js +24 -5
- package/lib/vendor/blamejs/lib/guard-imap-command.js +10 -2
- package/lib/vendor/blamejs/lib/guard-jmap.js +19 -3
- package/lib/vendor/blamejs/lib/guard-jwt.js +18 -2
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +2 -2
- package/lib/vendor/blamejs/lib/guard-managesieve-command.js +10 -2
- package/lib/vendor/blamejs/lib/guard-markdown.js +11 -24
- package/lib/vendor/blamejs/lib/guard-pdf.js +18 -2
- package/lib/vendor/blamejs/lib/guard-pop3-command.js +10 -2
- package/lib/vendor/blamejs/lib/guard-regex.js +8 -5
- package/lib/vendor/blamejs/lib/guard-svg.js +31 -32
- package/lib/vendor/blamejs/lib/guard-time.js +1 -1
- package/lib/vendor/blamejs/lib/guard-uuid.js +2 -2
- package/lib/vendor/blamejs/lib/html-balance.js +3 -3
- package/lib/vendor/blamejs/lib/http-client-cache.js +82 -4
- package/lib/vendor/blamejs/lib/http-client-cookie-jar.js +16 -1
- package/lib/vendor/blamejs/lib/http-client.js +39 -7
- package/lib/vendor/blamejs/lib/http-message-signature.js +35 -1
- package/lib/vendor/blamejs/lib/i18n-messageformat.js +31 -6
- package/lib/vendor/blamejs/lib/i18n.js +35 -21
- package/lib/vendor/blamejs/lib/incident-report.js +8 -1
- package/lib/vendor/blamejs/lib/ip-utils.js +11 -1
- package/lib/vendor/blamejs/lib/json-path.js +23 -1
- package/lib/vendor/blamejs/lib/jwk.js +20 -3
- package/lib/vendor/blamejs/lib/link-header.js +2 -1
- package/lib/vendor/blamejs/lib/log-stream-otlp-grpc.js +5 -1
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +5 -1
- package/lib/vendor/blamejs/lib/log-stream.js +8 -3
- package/lib/vendor/blamejs/lib/mail-auth.js +123 -22
- package/lib/vendor/blamejs/lib/mail-bimi.js +1 -1
- package/lib/vendor/blamejs/lib/mail-bounce.js +8 -0
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +46 -10
- package/lib/vendor/blamejs/lib/mail-dav.js +22 -3
- package/lib/vendor/blamejs/lib/mail-dkim.js +96 -30
- package/lib/vendor/blamejs/lib/mail-helo.js +6 -1
- package/lib/vendor/blamejs/lib/mail-scan.js +23 -16
- package/lib/vendor/blamejs/lib/mail-send-deliver.js +34 -8
- package/lib/vendor/blamejs/lib/mail-server-imap.js +3 -4
- package/lib/vendor/blamejs/lib/mail-server-managesieve.js +7 -11
- package/lib/vendor/blamejs/lib/mail-server-mx.js +93 -7
- package/lib/vendor/blamejs/lib/mail-server-registry.js +8 -2
- package/lib/vendor/blamejs/lib/mail.js +15 -5
- package/lib/vendor/blamejs/lib/metrics.js +268 -63
- package/lib/vendor/blamejs/lib/middleware/age-gate.js +7 -1
- package/lib/vendor/blamejs/lib/middleware/asyncapi-serve.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +19 -4
- package/lib/vendor/blamejs/lib/middleware/bot-guard.js +5 -9
- package/lib/vendor/blamejs/lib/middleware/csp-report.js +40 -8
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +24 -10
- package/lib/vendor/blamejs/lib/middleware/dpop.js +10 -5
- package/lib/vendor/blamejs/lib/middleware/index.js +6 -1
- package/lib/vendor/blamejs/lib/middleware/nel.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/openapi-serve.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +12 -1
- package/lib/vendor/blamejs/lib/middleware/require-bound-key.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/scim-server.js +1 -1
- package/lib/vendor/blamejs/lib/middleware/tus-upload.js +6 -2
- package/lib/vendor/blamejs/lib/mtls-ca.js +18 -2
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +11 -1
- package/lib/vendor/blamejs/lib/network-dns.js +21 -2
- package/lib/vendor/blamejs/lib/network-tls.js +69 -7
- package/lib/vendor/blamejs/lib/nonce-store.js +9 -1
- package/lib/vendor/blamejs/lib/object-store/gcs.js +7 -0
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +31 -0
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +1 -1
- package/lib/vendor/blamejs/lib/outbox.js +1 -1
- package/lib/vendor/blamejs/lib/parsers/safe-ini.js +9 -1
- package/lib/vendor/blamejs/lib/parsers/safe-toml.js +10 -1
- package/lib/vendor/blamejs/lib/permissions.js +6 -3
- package/lib/vendor/blamejs/lib/pid-probe.js +55 -0
- package/lib/vendor/blamejs/lib/pqc-agent.js +8 -1
- package/lib/vendor/blamejs/lib/pqc-software.js +3 -3
- package/lib/vendor/blamejs/lib/pubsub.js +20 -9
- package/lib/vendor/blamejs/lib/queue-local.js +8 -1
- package/lib/vendor/blamejs/lib/queue-redis.js +5 -0
- package/lib/vendor/blamejs/lib/redact.js +54 -0
- package/lib/vendor/blamejs/lib/restore-bundle.js +10 -2
- package/lib/vendor/blamejs/lib/restore.js +19 -0
- package/lib/vendor/blamejs/lib/retention.js +12 -4
- package/lib/vendor/blamejs/lib/router.js +60 -6
- package/lib/vendor/blamejs/lib/safe-buffer.js +69 -0
- package/lib/vendor/blamejs/lib/safe-decompress.js +8 -2
- package/lib/vendor/blamejs/lib/safe-dns.js +25 -20
- package/lib/vendor/blamejs/lib/safe-ical.js +7 -3
- package/lib/vendor/blamejs/lib/safe-mime.js +12 -1
- package/lib/vendor/blamejs/lib/safe-object.js +80 -0
- package/lib/vendor/blamejs/lib/safe-path.js +52 -4
- package/lib/vendor/blamejs/lib/safe-redirect.js +10 -2
- package/lib/vendor/blamejs/lib/safe-schema.js +4 -1
- package/lib/vendor/blamejs/lib/safe-sieve.js +33 -1
- package/lib/vendor/blamejs/lib/safe-sql.js +88 -0
- package/lib/vendor/blamejs/lib/safe-vcard.js +7 -3
- package/lib/vendor/blamejs/lib/scheduler.js +8 -1
- package/lib/vendor/blamejs/lib/security-assert.js +6 -6
- package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +74 -27
- package/lib/vendor/blamejs/lib/self-update.js +517 -87
- package/lib/vendor/blamejs/lib/server-timing.js +2 -4
- package/lib/vendor/blamejs/lib/session.js +34 -12
- package/lib/vendor/blamejs/lib/sql.js +15 -1
- package/lib/vendor/blamejs/lib/ssrf-guard.js +52 -0
- package/lib/vendor/blamejs/lib/static.js +55 -13
- package/lib/vendor/blamejs/lib/storage.js +8 -2
- package/lib/vendor/blamejs/lib/subject.js +6 -1
- package/lib/vendor/blamejs/lib/tenant-quota.js +52 -13
- package/lib/vendor/blamejs/lib/tls-exporter.js +3 -3
- package/lib/vendor/blamejs/lib/tsa.js +34 -13
- package/lib/vendor/blamejs/lib/uri-template.js +8 -1
- package/lib/vendor/blamejs/lib/vault/index.js +13 -0
- package/lib/vendor/blamejs/lib/vc.js +44 -5
- package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +26 -26
- package/lib/vendor/blamejs/lib/vendor/noble-ciphers.cjs +792 -1
- package/lib/vendor/blamejs/lib/vendor/noble-curves.cjs +3443 -1
- package/lib/vendor/blamejs/lib/vendor/noble-post-quantum.cjs +2737 -1
- package/lib/vendor/blamejs/lib/vendor/pki.cjs +39550 -13
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.dat +55 -53
- package/lib/vendor/blamejs/lib/vendor/public-suffix-list.data.js +5551 -5554
- package/lib/vendor/blamejs/lib/vendor/simplewebauthn-server.cjs +22459 -32
- package/lib/vendor/blamejs/lib/watcher.js +89 -17
- package/lib/vendor/blamejs/lib/webhook-dispatcher.js +25 -3
- package/lib/vendor/blamejs/lib/webhook.js +12 -2
- package/lib/vendor/blamejs/lib/ws-client.js +32 -1
- package/lib/vendor/blamejs/lib/xml-c14n.js +29 -1
- package/lib/vendor/blamejs/oss-fuzz/projects/blamejs/Dockerfile +1 -1
- package/lib/vendor/blamejs/package-lock.json +2 -2
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.16.x.json +1462 -0
- package/lib/vendor/blamejs/release-notes/v0.17.0.json +31 -0
- package/lib/vendor/blamejs/release-notes/v0.17.1.json +31 -0
- package/lib/vendor/blamejs/release-notes/v0.17.10.json +30 -0
- package/lib/vendor/blamejs/release-notes/v0.17.11.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.17.12.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.17.13.json +77 -0
- package/lib/vendor/blamejs/release-notes/v0.17.14.json +40 -0
- package/lib/vendor/blamejs/release-notes/v0.17.15.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.17.16.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.17.17.json +31 -0
- package/lib/vendor/blamejs/release-notes/v0.17.18.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.17.19.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.17.2.json +26 -0
- package/lib/vendor/blamejs/release-notes/v0.17.20.json +43 -0
- package/lib/vendor/blamejs/release-notes/v0.17.21.json +26 -0
- package/lib/vendor/blamejs/release-notes/v0.17.22.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.17.3.json +43 -0
- package/lib/vendor/blamejs/release-notes/v0.17.4.json +35 -0
- package/lib/vendor/blamejs/release-notes/v0.17.5.json +26 -0
- package/lib/vendor/blamejs/release-notes/v0.17.6.json +26 -0
- package/lib/vendor/blamejs/release-notes/v0.17.7.json +35 -0
- package/lib/vendor/blamejs/release-notes/v0.17.8.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.17.9.json +26 -0
- package/lib/vendor/blamejs/scripts/validate-source-comment-blocks.js +4 -0
- package/lib/vendor/blamejs/scripts/vendor-update.sh +293 -9
- package/lib/vendor/blamejs/socket.yml +94 -0
- package/lib/vendor/blamejs/test/00-primitives.js +605 -0
- package/lib/vendor/blamejs/test/helpers/drivers.js +12 -9
- package/lib/vendor/blamejs/test/integration/audit-stack-mysql.test.js +90 -1
- package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +122 -1
- package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +30 -0
- package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +29 -0
- package/lib/vendor/blamejs/test/integration/dsr-cluster-mysql.test.js +479 -0
- package/lib/vendor/blamejs/test/integration/dsr-cluster-pg.test.js +487 -0
- package/lib/vendor/blamejs/test/integration/federation-auth.test.js +313 -5
- package/lib/vendor/blamejs/test/integration/mail-dkim.test.js +80 -0
- package/lib/vendor/blamejs/test/integration/openid-federation-chain.test.js +275 -0
- package/lib/vendor/blamejs/test/integration/webhook-dispatcher-pg.test.js +96 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/a2a-tasks.test.js +401 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/acme.test.js +1649 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/age-gate.test.js +33 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-saga.test.js +60 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-snapshot.test.js +524 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +891 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +128 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-adapters.test.js +271 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +628 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-tar-read.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-wrap.test.js +227 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/arg-parser.test.js +15 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/asn1-der.test.js +73 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file.test.js +114 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-sign-anchor.test.js +141 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools.test.js +624 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit.test.js +769 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +436 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-oauth.test.js +1697 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-oid4vp.test.js +495 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-password.test.js +500 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-saml.test.js +1809 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-status-list.test.js +243 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-index.test.js +2226 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-manifest-signature.test.js +183 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/base32.test.js +105 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +693 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cache.test.js +603 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +557 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/case-insensitive-security-token-guard.test.js +214 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cbor.test.js +25 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cert.test.js +843 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/ciba-authreqid-binding.test.js +77 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/ciba.test.js +866 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/clear-site-data.test.js +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-erase.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cli.test.js +2790 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cluster-lease-renewal-ttl.test.js +114 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cluster.test.js +1032 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cms-codec.test.js +575 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +739 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/codepoint-class.test.js +8 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +194 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-lifecycle-proto-key.test.js +104 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-sanctions.test.js +254 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +613 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/cookies.test.js +140 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cose.test.js +407 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-adversarial.test.js +352 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field.test.js +575 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hash-files-parallel.test.js +10 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-hash-stream.test.js +24 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-oprf.test.js +17 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-spki-pin.test.js +183 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-xwing.test.js +48 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto.test.js +345 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-report.test.js +58 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csv.test.js +48 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/daemon.test.js +459 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/dark-patterns.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query.test.js +1019 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +77 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db.test.js +1621 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ddl-change-control.test.js +462 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/dnssec.test.js +323 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-verify.test.js +562 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +901 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/dual-control.test.js +249 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +561 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db.test.js +1214 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +21 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fedcm-dbsc.test.js +46 -20
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +147 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3.test.js +907 -38
- package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/file-upload.test.js +1135 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/flag.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/forms.test.js +747 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/gate-contract.test.js +1855 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/gdpr-ropa.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-archive.test.js +86 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-auth.test.js +117 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-cidr.test.js +151 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-domain.test.js +422 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-filename.test.js +424 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/{guard-gate-disposition-coverage.test.js → guard-gate-disposition.test.js} +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-graphql.test.js +340 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-html.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-idempotency-key.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-image.test.js +519 -45
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-imap-command.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-jmap.test.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-jsonpath.test.js +66 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-jwt.test.js +436 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-managesieve-command.test.js +25 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-markdown.test.js +16 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-mime.test.js +86 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-oauth.test.js +140 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-pdf.test.js +80 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-pop3-command.test.js +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-regex.test.js +99 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-shell.test.js +76 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/{guard-sql-coverage.test.js → guard-sql.test.js} +3 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-svg.test.js +524 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-template.test.js +71 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-time.test.js +92 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-uuid.test.js +86 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/html-balance.test.js +38 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-cache-authorization.test.js +347 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-cache.test.js +908 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client.test.js +2874 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-message-signature.test.js +643 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/i18n-messageformat.test.js +38 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/i18n.test.js +1153 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +494 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ip-utils.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/jsdoc-example-execution.test.js +177 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-path.test.js +103 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/json-schema.test.js +214 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/jwk.test.js +41 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/jwt-external.test.js +276 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/keychain.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/log-stream.test.js +241 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/log.test.js +106 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-agent.test.js +441 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +2136 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi.test.js +759 -12
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-bounce.test.js +811 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-pgp-experimental.test.js +401 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-pgp.test.js +751 -8
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +864 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto.test.js +110 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +830 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-deploy.test.js +635 -86
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim.test.js +818 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-helo.test.js +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-scan.test.js +199 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-send-deliver.test.js +686 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-imap.test.js +778 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +1438 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-managesieve.test.js +449 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +773 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-pop3.test.js +623 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-rate-limit.test.js +55 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-registry.test.js +13 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-submission.test.js +877 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-sieve.test.js +326 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store-fts.test.js +55 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +242 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail.test.js +1596 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp.test.js +440 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/metrics-snapshot.test.js +89 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/metrics.test.js +1230 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-assetlinks.test.js +113 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-body-parser.test.js +1169 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-bot-disclose.test.js +123 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-cookies.test.js +108 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-gpc.test.js +121 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-headers.test.js +163 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-host-allowlist.test.js +154 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-require-content-type.test.js +127 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/{middleware-tus-upload-coverage.test.js → middleware-tus-upload.test.js} +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-web-app-manifest.test.js +132 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mtls-ca-crl-fingerprint-only.test.js +92 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver.test.js +65 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns.test.js +1727 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/network-nts.test.js +699 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-smtp-policy.test.js +1181 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-tls.test.js +1876 -3
- package/lib/vendor/blamejs/test/layer-0-primitives/network.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/nonce-store-enforce-replay-nonboolean.test.js +81 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ntp-check.test.js +122 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/observability-tracing.test.js +378 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/observability.test.js +77 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oid4vci.test.js +1016 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/openid-federation.test.js +946 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/outbox.test.js +818 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/own-property-membership-guards.test.js +109 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/parsers-safe-toml.test.js +543 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/{parsers-safe-yaml-coverage.test.js → parsers-safe-yaml.test.js} +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +73 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/passkey.test.js +649 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/permissions.test.js +524 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pubsub.test.js +295 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-flow-repeat.test.js +36 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/queue.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-xff-spoofing.test.js +34 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redact.test.js +1031 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +94 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/require-bound-key.test.js +401 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/restore-empty-manifest-wipe.test.js +154 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention.test.js +600 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-host-path-injection.test.js +231 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-tls0rtt.test.js +21 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router.test.js +1277 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-async.test.js +133 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer-linear-scans.test.js +14 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-buffer.test.js +80 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-decompress.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-dns.test.js +245 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-ini.test.js +376 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-json.test.js +175 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-mime.test.js +518 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-object.test.js +62 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-path.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-schema.test.js +104 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-sql.test.js +142 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-url.test.js +112 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-vcard.test.js +26 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scheduler.test.js +157 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scim-server.test.js +396 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +930 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-assert.test.js +755 -82
- package/lib/vendor/blamejs/test/layer-0-primitives/self-update.test.js +1053 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +200 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-strict-binding-missing-failclosed.test.js +110 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +636 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql-offset-without-limit.test.js +94 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +1190 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +21 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +850 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/storage-presigned-url.test.js +146 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/storage.test.js +621 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/subject.test.js +47 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/template.test.js +582 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tenant-quota.test.js +587 -25
- package/lib/vendor/blamejs/test/layer-0-primitives/time.test.js +11 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tsa.test.js +524 -63
- package/lib/vendor/blamejs/test/layer-0-primitives/uri-template.test.js +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vault-rotate.test.js +866 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vault.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vc.test.js +215 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/watcher.test.js +550 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook-dispatcher.test.js +388 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook.test.js +262 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket.test.js +1294 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +629 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/xml-c14n-attr-normalization.test.js +164 -0
- package/lib/vendor/blamejs/test/smoke.js +2 -0
- package/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.16.0.json +0 -44
- package/lib/vendor/blamejs/release-notes/v0.16.1.json +0 -36
- package/lib/vendor/blamejs/release-notes/v0.16.2.json +0 -71
- package/lib/vendor/blamejs/release-notes/v0.16.3.json +0 -71
- package/lib/vendor/blamejs/test/layer-0-primitives/acme-coverage.test.js +0 -441
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-oauth-coverage.test.js +0 -482
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-saml-coverage.test.js +0 -671
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-index-coverage.test.js +0 -690
- package/lib/vendor/blamejs/test/layer-0-primitives/cli-coverage.test.js +0 -238
- package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials-coverage.test.js +0 -319
- package/lib/vendor/blamejs/test/layer-0-primitives/keychain-coverage.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-coverage.test.js +0 -437
- package/lib/vendor/blamejs/test/layer-0-primitives/mcp-coverage.test.js +0 -443
- package/lib/vendor/blamejs/test/layer-0-primitives/metrics-coverage.test.js +0 -395
- package/lib/vendor/blamejs/test/layer-0-primitives/network-smtp-policy-coverage.test.js +0 -565
- package/lib/vendor/blamejs/test/layer-0-primitives/router-coverage.test.js +0 -592
- package/lib/vendor/blamejs/test/layer-0-primitives/sql-coverage.test.js +0 -422
- package/lib/vendor/blamejs/test/layer-0-primitives/test-coverage.test.js +0 -573
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* b.network.smtp.policy — coverage for the input-validation, option-default,
|
|
6
|
+
* error-handling, and pure-logic branches that smtp-policy.test.js leaves
|
|
7
|
+
* uncovered.
|
|
8
|
+
*
|
|
9
|
+
* Everything here is in-memory: DNS is driven through the `dnsLookup` opt,
|
|
10
|
+
* HTTPS submission through an operator-supplied `httpClient` fake, and the
|
|
11
|
+
* receive-side `parseReport` runs on Buffers built in-process. No branch
|
|
12
|
+
* needs a live network or DNS backend (those belong to the integration
|
|
13
|
+
* suite). The uncovered surface is: MTA-STS parse rejects (empty / bad-mode /
|
|
14
|
+
* non-string), matchMx defensive returns, mtaSts.fetch precondition + domain
|
|
15
|
+
* guard, dane.tlsa / recordShape guards + every RFC 6698 label, verifyChain
|
|
16
|
+
* PKIX-opt-in + unsupported-usage + SHA-512 + SPKI-no-bytes paths, tlsRpt
|
|
17
|
+
* recordShape type guards + policy defaults, fetchPolicy rua-required +
|
|
18
|
+
* no-record returns, submit scheme dispatch + failure branches, and the
|
|
19
|
+
* entire tlsRpt.parseReport receive-side parser.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
var helpers = require("../helpers");
|
|
23
|
+
var b = helpers.b;
|
|
24
|
+
var check = helpers.check;
|
|
25
|
+
|
|
26
|
+
var nodeCrypto = require("crypto");
|
|
27
|
+
var zlib = require("zlib");
|
|
28
|
+
var nodeDns = require("node:dns");
|
|
29
|
+
var C = require("../../lib/constants");
|
|
30
|
+
var asn1 = require("../../lib/asn1-der");
|
|
31
|
+
var httpClientMod = require("../../lib/http-client");
|
|
32
|
+
|
|
33
|
+
// ---- small local helpers -------------------------------------------------
|
|
34
|
+
|
|
35
|
+
function throwsWithCode(fn, codeRe) {
|
|
36
|
+
var threw = null;
|
|
37
|
+
try { fn(); } catch (e) { threw = e; }
|
|
38
|
+
return threw !== null && codeRe.test(threw.code || "");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function asyncThrowsWithCode(fn, codeRe) {
|
|
42
|
+
var threw = null;
|
|
43
|
+
try { await fn(); } catch (e) { threw = e; }
|
|
44
|
+
return threw !== null && codeRe.test(threw.code || "");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// mtaSts.fetch uses the framework httpClient() singleton directly (no
|
|
48
|
+
// opts.httpClient seam like tlsRpt.submit has), so the successful-fetch
|
|
49
|
+
// path is driven by swapping the singleton's `.request` for a responder
|
|
50
|
+
// and restoring it in finally. The layer-0 runner forks one process per
|
|
51
|
+
// file, so this patch never leaks across test files. `dnsLookup` still
|
|
52
|
+
// drives the _mta-sts TXT precondition lookup as the operator seam.
|
|
53
|
+
async function withFakeHttpRequest(responder, fn) {
|
|
54
|
+
var orig = httpClientMod.request;
|
|
55
|
+
httpClientMod.request = responder;
|
|
56
|
+
try { return await fn(); }
|
|
57
|
+
finally { httpClientMod.request = orig; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// dane.tlsa reads node:dns.promises.resolveTlsa (captured once at module
|
|
61
|
+
// load as a stable object reference). Swapping the method on that same
|
|
62
|
+
// object injects the TLSA answer without a live DNS TYPE-52 query.
|
|
63
|
+
async function withFakeResolveTlsa(impl, fn) {
|
|
64
|
+
var orig = nodeDns.promises.resolveTlsa;
|
|
65
|
+
nodeDns.promises.resolveTlsa = impl;
|
|
66
|
+
try { return await fn(); }
|
|
67
|
+
finally { nodeDns.promises.resolveTlsa = orig; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Minimal-but-parseable X.509 DER for the DANE-TA chain-order checks.
|
|
71
|
+
// verifyChain extracts the TA cert's Subject and the child cert's Issuer
|
|
72
|
+
// via asn1-der and requires TA.subject === child.issuer (RFC 7672
|
|
73
|
+
// §3.1.1). A real peerCertificate chain is always DER; these fixtures let
|
|
74
|
+
// the ASN.1 walk succeed so the ordered-match and mismatch branches run.
|
|
75
|
+
function _derName(label) {
|
|
76
|
+
return asn1.writeSequence([ asn1.writeNode(asn1.TAG.PRINTABLE_STRING, Buffer.from(label, "ascii")) ]);
|
|
77
|
+
}
|
|
78
|
+
// Shared SubjectPublicKeyInfo fixture — embedded verbatim in every
|
|
79
|
+
// _derCert, so a selector=1 (SPKI) TLSA match can be built against its
|
|
80
|
+
// exact bytes (readNode's .raw slices these back out byte-identically).
|
|
81
|
+
var _SPKI_DER = asn1.writeSequence([
|
|
82
|
+
asn1.writeSequence([ asn1.writeOid("1.2.840.10045.2.1"), asn1.writeOid("1.2.840.10045.3.1.7") ]),
|
|
83
|
+
asn1.writeNode(asn1.TAG.BIT_STRING, Buffer.from([0x00, 0x04, 0x01, 0x02, 0x03, 0x04])),
|
|
84
|
+
]);
|
|
85
|
+
function _derCert(issuerName, subjectName) {
|
|
86
|
+
var sigAlg = asn1.writeSequence([ asn1.writeOid("1.2.840.113549.1.1.11"), asn1.writeNull() ]);
|
|
87
|
+
var validity = asn1.writeSequence([
|
|
88
|
+
asn1.writeNode(asn1.TAG.UTC_TIME, Buffer.from("260101000000Z")),
|
|
89
|
+
asn1.writeNode(asn1.TAG.UTC_TIME, Buffer.from("270101000000Z")),
|
|
90
|
+
]);
|
|
91
|
+
var spki = _SPKI_DER;
|
|
92
|
+
var tbs = asn1.writeSequence([
|
|
93
|
+
asn1.writeContextExplicit(0, asn1.writeInteger(Buffer.from([0x02]))), // [0] version
|
|
94
|
+
asn1.writeInteger(Buffer.from([0x01])), // serialNumber
|
|
95
|
+
sigAlg, // signature
|
|
96
|
+
issuerName, // issuer
|
|
97
|
+
validity, // validity
|
|
98
|
+
subjectName, // subject
|
|
99
|
+
spki, // subjectPublicKeyInfo
|
|
100
|
+
]);
|
|
101
|
+
return asn1.writeSequence([ tbs, sigAlg, asn1.writeNode(asn1.TAG.BIT_STRING, Buffer.from([0x00, 0xaa])) ]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- MTA-STS parse (mtaSts.parsePolicy) ----------------------------------
|
|
105
|
+
|
|
106
|
+
function testParseStsEmptyAndNonString() {
|
|
107
|
+
check("parsePolicy: empty text → mta-sts-empty",
|
|
108
|
+
throwsWithCode(function () { b.network.smtp.mtaSts.parsePolicy(""); }, /mta-sts-empty/));
|
|
109
|
+
check("parsePolicy: non-string → mta-sts-empty",
|
|
110
|
+
throwsWithCode(function () { b.network.smtp.mtaSts.parsePolicy(null); }, /mta-sts-empty/));
|
|
111
|
+
check("parsePolicy: number → mta-sts-empty",
|
|
112
|
+
throwsWithCode(function () { b.network.smtp.mtaSts.parsePolicy(42); }, /mta-sts-empty/));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function testParseStsBadMode() {
|
|
116
|
+
check("parsePolicy: unknown mode → mta-sts-bad-mode",
|
|
117
|
+
throwsWithCode(function () {
|
|
118
|
+
b.network.smtp.mtaSts.parsePolicy("version: STSv1\nmode: bogus\n");
|
|
119
|
+
}, /mta-sts-bad-mode/));
|
|
120
|
+
// A version-only policy (no mode line) leaves mode null → bad-mode.
|
|
121
|
+
check("parsePolicy: missing mode → mta-sts-bad-mode",
|
|
122
|
+
throwsWithCode(function () {
|
|
123
|
+
b.network.smtp.mtaSts.parsePolicy("version: STSv1\nmax_age: 100\n");
|
|
124
|
+
}, /mta-sts-bad-mode/));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function testParseStsBadVersion() {
|
|
128
|
+
// A syntactically valid policy whose version token is not STSv1 is
|
|
129
|
+
// refused before the mode check (RFC 8461 §3.1 — version MUST be STSv1).
|
|
130
|
+
check("parsePolicy: version STSv2 → mta-sts-bad-version",
|
|
131
|
+
throwsWithCode(function () {
|
|
132
|
+
b.network.smtp.mtaSts.parsePolicy("version: STSv2\nmode: enforce\n");
|
|
133
|
+
}, /mta-sts-bad-version/));
|
|
134
|
+
// Absent version line leaves version null → bad-version (not bad-mode).
|
|
135
|
+
check("parsePolicy: missing version → mta-sts-bad-version",
|
|
136
|
+
throwsWithCode(function () {
|
|
137
|
+
b.network.smtp.mtaSts.parsePolicy("mode: enforce\nmax_age: 100\n");
|
|
138
|
+
}, /mta-sts-bad-version/));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function testParseStsModeVariantsAndMaxAge() {
|
|
142
|
+
var testing = b.network.smtp.mtaSts.parsePolicy("version: STSv1\nmode: testing\n");
|
|
143
|
+
check("parsePolicy: testing mode accepted", testing.mode === "testing");
|
|
144
|
+
|
|
145
|
+
var none = b.network.smtp.mtaSts.parsePolicy("version: STSv1\nmode: none\n");
|
|
146
|
+
check("parsePolicy: none mode accepted", none.mode === "none");
|
|
147
|
+
|
|
148
|
+
// mode is lowercased on parse.
|
|
149
|
+
var upper = b.network.smtp.mtaSts.parsePolicy("version: STSv1\nmode: ENFORCE\n");
|
|
150
|
+
check("parsePolicy: mode is case-normalized to lowercase", upper.mode === "enforce");
|
|
151
|
+
|
|
152
|
+
// mx hosts are lowercased and collected in order (repeats preserved).
|
|
153
|
+
var multi = b.network.smtp.mtaSts.parsePolicy(
|
|
154
|
+
"version: STSv1\nmode: enforce\nmx: MX1.Example.COM\nmx: mx2.example.com\n");
|
|
155
|
+
check("parsePolicy: mx hosts lowercased + ordered",
|
|
156
|
+
multi.mx.length === 2 && multi.mx[0] === "mx1.example.com" && multi.mx[1] === "mx2.example.com");
|
|
157
|
+
|
|
158
|
+
// With no max_age line the field stays null (falls back to the default TTL
|
|
159
|
+
// inside fetch; parse itself just records absence).
|
|
160
|
+
check("parsePolicy: absent max_age stays null", multi.max_age === null);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---- MTA-STS MX match (mtaSts.matchMx) — defensive returns ----------------
|
|
164
|
+
|
|
165
|
+
function testMatchMxDefensiveReturns() {
|
|
166
|
+
check("matchMx: non-string mxHost → false",
|
|
167
|
+
b.network.smtp.mtaSts.matchMx(123, ["mx.example.com"]) === false);
|
|
168
|
+
check("matchMx: non-array mxList → false",
|
|
169
|
+
b.network.smtp.mtaSts.matchMx("mx.example.com", "mx.example.com") === false);
|
|
170
|
+
check("matchMx: null mxList → false",
|
|
171
|
+
b.network.smtp.mtaSts.matchMx("mx.example.com", null) === false);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function testMatchMxCaseAndDotEdges() {
|
|
175
|
+
// Case-insensitive exact match.
|
|
176
|
+
check("matchMx: exact match is case-insensitive",
|
|
177
|
+
b.network.smtp.mtaSts.matchMx("MX1.EXAMPLE.COM", ["mx1.example.com"]) === true);
|
|
178
|
+
// A single-label host (no dot) cannot satisfy a wildcard entry.
|
|
179
|
+
check("matchMx: dotless host vs wildcard → false",
|
|
180
|
+
b.network.smtp.mtaSts.matchMx("localhost", ["*.example.com"]) === false);
|
|
181
|
+
// A bare "*." entry is too short to be a wildcard and never matches.
|
|
182
|
+
check("matchMx: bare '*.' entry never matches",
|
|
183
|
+
b.network.smtp.mtaSts.matchMx("a.example.com", ["*."]) === false);
|
|
184
|
+
// A non-matching-suffix wildcard → false.
|
|
185
|
+
check("matchMx: wildcard suffix must match",
|
|
186
|
+
b.network.smtp.mtaSts.matchMx("mx1.other.com", ["*.example.com"]) === false);
|
|
187
|
+
// A single-label wildcard that DOES match the host's suffix → true.
|
|
188
|
+
check("matchMx: single-label wildcard matches a subdomain",
|
|
189
|
+
b.network.smtp.mtaSts.matchMx("mx1.example.com", ["*.example.com"]) === true);
|
|
190
|
+
// A wildcard entry preceding an exact entry: the exact host still matches.
|
|
191
|
+
check("matchMx: exact host matches even when a wildcard precedes it",
|
|
192
|
+
b.network.smtp.mtaSts.matchMx("mail.example.com", ["*.other.com", "mail.example.com"]) === true);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---- MTA-STS fetch (mtaSts.fetch) — precondition + domain guard -----------
|
|
196
|
+
|
|
197
|
+
async function testMtaStsFetchDomainGuard() {
|
|
198
|
+
check("mtaSts.fetch: non-string domain → bad-domain",
|
|
199
|
+
await asyncThrowsWithCode(function () { return b.network.smtp.mtaSts.fetch(123); }, /bad-domain/));
|
|
200
|
+
check("mtaSts.fetch: empty domain → bad-domain",
|
|
201
|
+
await asyncThrowsWithCode(function () { return b.network.smtp.mtaSts.fetch(""); }, /bad-domain/));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function testMtaStsFetchNoTxtRecord() {
|
|
205
|
+
// A domain whose _mta-sts TXT does not carry v=STSv1 has no rotation
|
|
206
|
+
// signal → fetch refuses to pull the HTTPS policy (RFC 8461 §3.1) and
|
|
207
|
+
// returns null WITHOUT touching the network.
|
|
208
|
+
var dnsLookup = async function () { return [["some-unrelated-txt-value"]]; };
|
|
209
|
+
var rv = await b.network.smtp.mtaSts.fetch("example.com", { dnsLookup: dnsLookup });
|
|
210
|
+
check("mtaSts.fetch: TXT without v=STSv1 → null", rv === null);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function testMtaStsFetchTxtAbsent() {
|
|
214
|
+
// ENOTFOUND from the resolver → safeResolveTxt returns null → fetch null.
|
|
215
|
+
var dnsLookup = async function () { var e = new Error("nx"); e.code = "ENOTFOUND"; throw e; };
|
|
216
|
+
var rv = await b.network.smtp.mtaSts.fetch("no-such.example.com", { dnsLookup: dnsLookup });
|
|
217
|
+
check("mtaSts.fetch: absent _mta-sts TXT → null", rv === null);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function testMtaStsFetchTxtHardLookupFailure() {
|
|
221
|
+
// A hard resolver failure (not ENOTFOUND/ENODATA) on the _mta-sts TXT
|
|
222
|
+
// lookup surfaces a structured SmtpPolicyError — a spoofable/absent TXT
|
|
223
|
+
// is null, but a genuine resolver fault is an error, not silent success.
|
|
224
|
+
var dnsLookup = async function () { var e = new Error("servfail"); e.code = "ESERVFAIL"; throw e; };
|
|
225
|
+
check("mtaSts.fetch: hard TXT resolver failure → mta-sts-txt-lookup-failed",
|
|
226
|
+
await asyncThrowsWithCode(function () {
|
|
227
|
+
return b.network.smtp.mtaSts.fetch("servfail.example.com", { dnsLookup: dnsLookup });
|
|
228
|
+
}, /mta-sts-txt-lookup-failed/));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---- DANE tlsa (dane.tlsa) — domain guard ---------------------------------
|
|
232
|
+
|
|
233
|
+
async function testDaneTlsaDomainGuard() {
|
|
234
|
+
check("dane.tlsa: non-string domain → bad-domain",
|
|
235
|
+
await asyncThrowsWithCode(function () { return b.network.smtp.dane.tlsa(42); }, /bad-domain/));
|
|
236
|
+
check("dane.tlsa: empty domain → bad-domain",
|
|
237
|
+
await asyncThrowsWithCode(function () { return b.network.smtp.dane.tlsa(""); }, /bad-domain/));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---- DANE recordShape (dane.recordShape) — guard + every label -----------
|
|
241
|
+
|
|
242
|
+
function testDaneRecordShapeGuard() {
|
|
243
|
+
check("dane.recordShape: non-object → dane-bad-record",
|
|
244
|
+
throwsWithCode(function () { b.network.smtp.dane.recordShape(null); }, /dane-bad-record/));
|
|
245
|
+
check("dane.recordShape: string → dane-bad-record",
|
|
246
|
+
throwsWithCode(function () { b.network.smtp.dane.recordShape("x"); }, /dane-bad-record/));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function testDaneRecordShapeAllLabels() {
|
|
250
|
+
var pkixTa = b.network.smtp.dane.recordShape({ usage: 0, selector: 0, mtype: 0, dataHex: "aa" });
|
|
251
|
+
check("recordShape: usage 0 / selector 0 / mtype 0 → PKIX-TA / Cert / Full",
|
|
252
|
+
pkixTa.usageLabel === "PKIX-TA" && pkixTa.selectorLabel === "Cert" && pkixTa.mtypeLabel === "Full");
|
|
253
|
+
|
|
254
|
+
var pkixEe = b.network.smtp.dane.recordShape({ usage: 1, selector: 1, mtype: 2, dataHex: "bb" });
|
|
255
|
+
check("recordShape: usage 1 / selector 1 / mtype 2 → PKIX-EE / SPKI / SHA-512",
|
|
256
|
+
pkixEe.usageLabel === "PKIX-EE" && pkixEe.selectorLabel === "SPKI" && pkixEe.mtypeLabel === "SHA-512");
|
|
257
|
+
|
|
258
|
+
var daneTa = b.network.smtp.dane.recordShape({ usage: 2, selector: 0, mtype: 1, dataHex: "cc" });
|
|
259
|
+
check("recordShape: usage 2 → DANE-TA", daneTa.usageLabel === "DANE-TA");
|
|
260
|
+
|
|
261
|
+
var unknown = b.network.smtp.dane.recordShape({ usage: 9, selector: 5, mtype: 7, dataHex: "dd" });
|
|
262
|
+
check("recordShape: out-of-range codes → unknown labels",
|
|
263
|
+
unknown.usageLabel === "unknown" && unknown.selectorLabel === "unknown" && unknown.mtypeLabel === "unknown");
|
|
264
|
+
|
|
265
|
+
// The raw fields pass through unchanged.
|
|
266
|
+
check("recordShape: raw fields pass through",
|
|
267
|
+
daneTa.usage === 2 && daneTa.selector === 0 && daneTa.mtype === 1 && daneTa.dataHex === "cc");
|
|
268
|
+
|
|
269
|
+
// usage 3 → DANE-EE (the last labeled arm before "unknown").
|
|
270
|
+
var daneEe = b.network.smtp.dane.recordShape({ usage: 3, selector: 1, mtype: 1, dataHex: "ee" });
|
|
271
|
+
check("recordShape: usage 3 → DANE-EE", daneEe.usageLabel === "DANE-EE");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---- DANE verifyChain (dane.verifyChain) — uncovered branches ------------
|
|
275
|
+
|
|
276
|
+
function testVerifyChainPkixOptIn() {
|
|
277
|
+
var leaf = Buffer.from("leaf-der-bytes", "utf8");
|
|
278
|
+
var ca = Buffer.from("ca-der-bytes", "utf8");
|
|
279
|
+
|
|
280
|
+
// PKIX-EE (usage 1) with allowPkixModes → matches leaf, flags pkixPathRequired.
|
|
281
|
+
var eeRec = { usage: 1, selector: 0, mtype: 0, dataHex: leaf.toString("hex") };
|
|
282
|
+
var eeRv = b.network.smtp.dane.verifyChain([leaf, ca], [eeRec], { allowPkixModes: true });
|
|
283
|
+
check("verifyChain: PKIX-EE opt-in matches leaf with pkixPathRequired",
|
|
284
|
+
eeRv.ok === true && eeRv.matches[0].usage === "PKIX-EE" &&
|
|
285
|
+
eeRv.matches[0].certIndex === 0 && eeRv.matches[0].pkixPathRequired === true);
|
|
286
|
+
|
|
287
|
+
// PKIX-TA (usage 0) with allowPkixModes → matches a non-leaf cert.
|
|
288
|
+
var taRec = { usage: 0, selector: 0, mtype: 0, dataHex: ca.toString("hex") };
|
|
289
|
+
var taRv = b.network.smtp.dane.verifyChain([leaf, ca], [taRec], { allowPkixModes: true });
|
|
290
|
+
check("verifyChain: PKIX-TA opt-in matches trust-anchor cert with pkixPathRequired",
|
|
291
|
+
taRv.ok === true && taRv.matches[0].usage === "PKIX-TA" &&
|
|
292
|
+
taRv.matches[0].certIndex === 1 && taRv.matches[0].pkixPathRequired === true);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function testVerifyChainUnsupportedUsage() {
|
|
296
|
+
var leaf = Buffer.from("leaf", "utf8");
|
|
297
|
+
var rec = { usage: 7, selector: 0, mtype: 0, dataHex: "00" }; // out-of-range usage
|
|
298
|
+
var rv = b.network.smtp.dane.verifyChain([leaf], [rec]);
|
|
299
|
+
check("verifyChain: unsupported usage → ok=false + unsupported-usage error",
|
|
300
|
+
rv.ok === false && rv.matches.length === 0 &&
|
|
301
|
+
rv.errors[0] && rv.errors[0].reason === "unsupported-usage" && rv.errors[0].usage === 7);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function testVerifyChainDaneEeSha512() {
|
|
305
|
+
var leaf = Buffer.from("leaf-bytes-for-sha512", "utf8");
|
|
306
|
+
var sha512Hex = nodeCrypto.createHash("sha512").update(leaf).digest("hex");
|
|
307
|
+
var rec = { usage: 3, selector: 0, mtype: 2, dataHex: sha512Hex };
|
|
308
|
+
var rv = b.network.smtp.dane.verifyChain([leaf], [rec]);
|
|
309
|
+
check("verifyChain: DANE-EE / Cert / SHA-512 matches via hash",
|
|
310
|
+
rv.ok === true && rv.matches[0].usage === "DANE-EE" && rv.matches[0].mtype === "SHA-512");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function testVerifyChainSpkiSelectorNoBytes() {
|
|
314
|
+
// selector=1 (SPKI) on a non-DER buffer → SPKI extraction returns null →
|
|
315
|
+
// no match (the `if (!bytes) return null` branch in _matchTlsaAgainstCert).
|
|
316
|
+
var fake = Buffer.from("definitely not DER", "utf8");
|
|
317
|
+
var rec = { usage: 3, selector: 1, mtype: 0, dataHex: fake.toString("hex") };
|
|
318
|
+
var rv = b.network.smtp.dane.verifyChain([fake], [rec]);
|
|
319
|
+
check("verifyChain: SPKI selector on non-DER cert → no match",
|
|
320
|
+
rv.ok === false && rv.matches.length === 0);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---- TLS-RPT recordShape (tlsRpt.recordShape) — type guards + defaults ----
|
|
324
|
+
|
|
325
|
+
function testRecordShapeTypeGuards() {
|
|
326
|
+
// Called with no opts at all → defaults to {} → the organization guard
|
|
327
|
+
// fires (proving the opts-default path, not a TypeError on undefined).
|
|
328
|
+
check("recordShape: no opts → tls-rpt-bad-organization",
|
|
329
|
+
throwsWithCode(function () { b.network.smtp.tlsRpt.recordShape(); }, /tls-rpt-bad-organization/));
|
|
330
|
+
check("recordShape: non-string organization → tls-rpt-bad-organization",
|
|
331
|
+
throwsWithCode(function () {
|
|
332
|
+
b.network.smtp.tlsRpt.recordShape({ organization: 123, policies: [] });
|
|
333
|
+
}, /tls-rpt-bad-organization/));
|
|
334
|
+
check("recordShape: non-array policies → tls-rpt-bad-policies",
|
|
335
|
+
throwsWithCode(function () {
|
|
336
|
+
b.network.smtp.tlsRpt.recordShape({ organization: "example.com", policies: "nope" });
|
|
337
|
+
}, /tls-rpt-bad-policies/));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function testRecordShapePolicyDefaults() {
|
|
341
|
+
var rpt = b.network.smtp.tlsRpt.recordShape({
|
|
342
|
+
organization: "example.com",
|
|
343
|
+
policies: [{ domain: "example.com" }], // everything else defaulted
|
|
344
|
+
});
|
|
345
|
+
var pol = rpt.policies[0];
|
|
346
|
+
check("recordShape: policy-type defaults to 'sts'", pol.policy["policy-type"] === "sts");
|
|
347
|
+
check("recordShape: policy-string defaults to []",
|
|
348
|
+
Array.isArray(pol.policy["policy-string"]) && pol.policy["policy-string"].length === 0);
|
|
349
|
+
check("recordShape: mx-host defaults to []",
|
|
350
|
+
Array.isArray(pol.policy["mx-host"]) && pol.policy["mx-host"].length === 0);
|
|
351
|
+
check("recordShape: session counts default to 0",
|
|
352
|
+
pol.summary["total-successful-session-count"] === 0 &&
|
|
353
|
+
pol.summary["total-failure-session-count"] === 0);
|
|
354
|
+
check("recordShape: failure-details defaults to []",
|
|
355
|
+
Array.isArray(pol["failure-details"]) && pol["failure-details"].length === 0);
|
|
356
|
+
// A generated report-id + a default date-range are always present.
|
|
357
|
+
check("recordShape: auto report-id present",
|
|
358
|
+
typeof rpt["report-id"] === "string" && rpt["report-id"].length > 0);
|
|
359
|
+
check("recordShape: default date-range present",
|
|
360
|
+
typeof rpt["date-range"]["start-datetime"] === "string" &&
|
|
361
|
+
typeof rpt["date-range"]["end-datetime"] === "string");
|
|
362
|
+
check("recordShape: contact-info defaults to null", rpt["contact-info"] === null);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---- TLS-RPT fetchPolicy (tlsRpt.fetchPolicy) — guard + no-record returns --
|
|
366
|
+
|
|
367
|
+
async function testFetchPolicyDomainGuard() {
|
|
368
|
+
check("fetchPolicy: non-string domain → tls-rpt-bad-domain",
|
|
369
|
+
await asyncThrowsWithCode(function () { return b.network.smtp.tlsRpt.fetchPolicy(42); }, /tls-rpt-bad-domain/));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function testFetchPolicyRuaRequired() {
|
|
373
|
+
// A v=TLSRPTv1 record with no rua= is malformed and MUST be ignored.
|
|
374
|
+
var dnsLookup = async function () { return [["v=TLSRPTv1;"]]; };
|
|
375
|
+
var rv = await b.network.smtp.tlsRpt.fetchPolicy("example.com", { dnsLookup: dnsLookup });
|
|
376
|
+
check("fetchPolicy: v=TLSRPTv1 without rua → null", rv === null);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function testFetchPolicyNoMatchingRecord() {
|
|
380
|
+
// No record begins with v=TLSRPTv1 → null.
|
|
381
|
+
var dnsLookup = async function () { return [["v=spf1 -all"]]; };
|
|
382
|
+
var rv = await b.network.smtp.tlsRpt.fetchPolicy("example.com", { dnsLookup: dnsLookup });
|
|
383
|
+
check("fetchPolicy: no v=TLSRPTv1 record → null", rv === null);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function testFetchPolicyHardLookupFailure() {
|
|
387
|
+
// A hard resolver failure on the _smtp._tls TXT lookup surfaces a
|
|
388
|
+
// structured SmtpPolicyError (absence is null; a fault is an error).
|
|
389
|
+
var dnsLookup = async function () { var e = new Error("servfail"); e.code = "ESERVFAIL"; throw e; };
|
|
390
|
+
check("fetchPolicy: hard TXT resolver failure → tls-rpt-lookup-failed",
|
|
391
|
+
await asyncThrowsWithCode(function () {
|
|
392
|
+
return b.network.smtp.tlsRpt.fetchPolicy("servfail.example.com", { dnsLookup: dnsLookup });
|
|
393
|
+
}, /tls-rpt-lookup-failed/));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function testFetchPolicyNoTxtAtAll() {
|
|
397
|
+
// safeResolveTxt returns null on ENODATA → fetchPolicy null.
|
|
398
|
+
var dnsLookup = async function () { var e = new Error("nodata"); e.code = "ENODATA"; throw e; };
|
|
399
|
+
var rv = await b.network.smtp.tlsRpt.fetchPolicy("bare.example.com", { dnsLookup: dnsLookup });
|
|
400
|
+
check("fetchPolicy: no TXT record at all → null", rv === null);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function testFetchPolicyTrimsAndSplitsRua() {
|
|
404
|
+
var dnsLookup = async function () {
|
|
405
|
+
return [["v=TLSRPTv1; rua=https://a.example.com/r , mailto:tls@example.com "]];
|
|
406
|
+
};
|
|
407
|
+
var rv = await b.network.smtp.tlsRpt.fetchPolicy("example.com", { dnsLookup: dnsLookup });
|
|
408
|
+
check("fetchPolicy: comma-split rua entries are trimmed",
|
|
409
|
+
rv && rv.rua.length === 2 &&
|
|
410
|
+
rv.rua[0] === "https://a.example.com/r" && rv.rua[1] === "mailto:tls@example.com");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function testFetchPolicyStringRecordAmongNonMatching() {
|
|
414
|
+
// A plain-string TXT record (not the array-of-chunks shape) is read
|
|
415
|
+
// directly, and a preceding non-TLSRPT record is skipped before the
|
|
416
|
+
// matching one is chosen.
|
|
417
|
+
var dnsLookup = async function () {
|
|
418
|
+
return ["v=spf1 -all", "v=TLSRPTv1; rua=https://reports.example.com/r"];
|
|
419
|
+
};
|
|
420
|
+
var rv = await b.network.smtp.tlsRpt.fetchPolicy("example.com", { dnsLookup: dnsLookup });
|
|
421
|
+
check("fetchPolicy: string record chosen past a non-matching record",
|
|
422
|
+
rv && rv.rua.length === 1 && rv.rua[0] === "https://reports.example.com/r");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ---- TLS-RPT submit (tlsRpt.submit) — dispatch + failure branches ----------
|
|
426
|
+
|
|
427
|
+
async function testSubmitRejectsNonObjectReport() {
|
|
428
|
+
check("submit: non-object report → tls-rpt-bad-report",
|
|
429
|
+
await asyncThrowsWithCode(function () {
|
|
430
|
+
return b.network.smtp.tlsRpt.submit("not-a-report", { rua: ["https://x.example/r"] });
|
|
431
|
+
}, /tls-rpt-bad-report/));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function testSubmitHttpsNon2xx() {
|
|
435
|
+
// The framework httpClient resolves { statusCode, headers, body } — the
|
|
436
|
+
// fake mirrors that real contract so this branch exercises the shape the
|
|
437
|
+
// production path actually returns.
|
|
438
|
+
var fakeHttp = { request: async function () { return { statusCode: 500, body: Buffer.from("") }; } };
|
|
439
|
+
var rv = await b.network.smtp.tlsRpt.submit(
|
|
440
|
+
{ "organization-name": "example.com", "report-id": "r1" },
|
|
441
|
+
{ rua: ["https://reports.example.com/submit"], httpClient: fakeHttp });
|
|
442
|
+
var e = rv.results[0];
|
|
443
|
+
check("submit: https non-2xx → ok=false with HTTP-status error",
|
|
444
|
+
e.kind === "https" && e.ok === false && e.error === "HTTP 500");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function testSubmitHttpsSuccess2xx() {
|
|
448
|
+
// A successful submission MUST be reported ok=true. The framework
|
|
449
|
+
// httpClient resolves { statusCode, headers, body } (see mtaSts.fetch,
|
|
450
|
+
// which reads res.statusCode); a submit path that reads a non-existent
|
|
451
|
+
// `status` field would mark every real 2xx POST as a failure.
|
|
452
|
+
var fakeHttp = { request: async function () { return { statusCode: 200, headers: {}, body: Buffer.from("") }; } };
|
|
453
|
+
var rv = await b.network.smtp.tlsRpt.submit(
|
|
454
|
+
{ "organization-name": "example.com", "report-id": "r1" },
|
|
455
|
+
{ rua: ["https://reports.example.com/submit"], httpClient: fakeHttp });
|
|
456
|
+
var e = rv.results[0];
|
|
457
|
+
check("submit: https 2xx → ok=true, status=200, no error",
|
|
458
|
+
e.kind === "https" && e.ok === true && e.status === 200 && e.error === null);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function testSubmitHttpsThrows() {
|
|
462
|
+
var fakeHttp = { request: async function () { throw new Error("connreset"); } };
|
|
463
|
+
var rv = await b.network.smtp.tlsRpt.submit(
|
|
464
|
+
{ "organization-name": "example.com" },
|
|
465
|
+
{ rua: ["https://reports.example.com/submit"], httpClient: fakeHttp });
|
|
466
|
+
var e = rv.results[0];
|
|
467
|
+
check("submit: https request throw is caught into entry.error",
|
|
468
|
+
e.ok === false && /connreset/.test(e.error || ""));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function testSubmitHttpsThrowsNonError() {
|
|
472
|
+
// An error with an empty (falsy) message is still captured into
|
|
473
|
+
// entry.error via the String(e) fallback rather than surfacing as
|
|
474
|
+
// "undefined".
|
|
475
|
+
var fakeHttp = { request: async function () { throw new Error(""); } };
|
|
476
|
+
var rv = await b.network.smtp.tlsRpt.submit(
|
|
477
|
+
{ "organization-name": "example.com" },
|
|
478
|
+
{ rua: ["https://reports.example.com/submit"], httpClient: fakeHttp });
|
|
479
|
+
var e = rv.results[0];
|
|
480
|
+
check("submit: message-less throw captured via String(e)",
|
|
481
|
+
e.ok === false && e.error === "Error");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function testSubmitInvalidMailtoAddr() {
|
|
485
|
+
var rv = await b.network.smtp.tlsRpt.submit(
|
|
486
|
+
{ "organization-name": "example.com" },
|
|
487
|
+
{ rua: ["mailto:not a valid address"] });
|
|
488
|
+
var e = rv.results[0];
|
|
489
|
+
check("submit: invalid mailto addr-spec → refused, not forwarded",
|
|
490
|
+
e.ok === false && /not a valid RFC 5322 addr-spec/.test(e.error || "") && !e.mailto);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function testSubmitUnsupportedScheme() {
|
|
494
|
+
var rv = await b.network.smtp.tlsRpt.submit(
|
|
495
|
+
{ "organization-name": "example.com" },
|
|
496
|
+
{ rua: ["ftp://reports.example.com/r"] });
|
|
497
|
+
var e = rv.results[0];
|
|
498
|
+
check("submit: unsupported rua scheme → error names the scheme",
|
|
499
|
+
e.ok === false && /unsupported rua URI scheme: ftp/.test(e.error || ""));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function testSubmitValidMailtoBody() {
|
|
503
|
+
var report = b.network.smtp.tlsRpt.recordShape({
|
|
504
|
+
organization: "example.com",
|
|
505
|
+
reportId: "rpt-777",
|
|
506
|
+
policies: [{ type: "sts", domain: "example.com" }],
|
|
507
|
+
});
|
|
508
|
+
var rv = await b.network.smtp.tlsRpt.submit(report, { rua: ["mailto:tls@example.com"] });
|
|
509
|
+
var e = rv.results[0];
|
|
510
|
+
check("submit: valid mailto → prepared gzip body + subject with Report-ID",
|
|
511
|
+
e.kind === "mailto" && e.ok === true &&
|
|
512
|
+
Buffer.isBuffer(e.mailto.body) && e.mailto.body[0] === 0x1f && e.mailto.body[1] === 0x8b &&
|
|
513
|
+
e.mailto.to === "tls@example.com" &&
|
|
514
|
+
e.mailto.subject.indexOf("rpt-777") !== -1);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ---- TLS-RPT parseReport (tlsRpt.parseReport) — receive-side parser -------
|
|
518
|
+
|
|
519
|
+
function _validReport() {
|
|
520
|
+
return {
|
|
521
|
+
"organization-name": "reporter.example",
|
|
522
|
+
"date-range": { "start-datetime": "2026-01-01T00:00:00Z", "end-datetime": "2026-01-02T00:00:00Z" },
|
|
523
|
+
"report-id": "report-abc-123",
|
|
524
|
+
"contact-info": "tls-reports@reporter.example",
|
|
525
|
+
"policies": [
|
|
526
|
+
{ summary: { "total-successful-session-count": 10, "total-failure-session-count": 3 } },
|
|
527
|
+
{ summary: { "total-successful-session-count": 5, "total-failure-session-count": 1 } },
|
|
528
|
+
],
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function testParseReportBadInput() {
|
|
533
|
+
check("parseReport: null body → tls-rpt-bad-input",
|
|
534
|
+
throwsWithCode(function () { b.network.smtp.tlsRpt.parseReport(null); }, /tls-rpt-bad-input/));
|
|
535
|
+
check("parseReport: undefined body → tls-rpt-bad-input",
|
|
536
|
+
throwsWithCode(function () { b.network.smtp.tlsRpt.parseReport(undefined); }, /tls-rpt-bad-input/));
|
|
537
|
+
check("parseReport: number body → tls-rpt-bad-input",
|
|
538
|
+
throwsWithCode(function () { b.network.smtp.tlsRpt.parseReport(12345); }, /tls-rpt-bad-input/));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function testParseReportBadJsonAndShape() {
|
|
542
|
+
check("parseReport: malformed JSON → tls-rpt-bad-json",
|
|
543
|
+
throwsWithCode(function () { b.network.smtp.tlsRpt.parseReport("{not json"); }, /tls-rpt-bad-json/));
|
|
544
|
+
check("parseReport: scalar JSON (number) → tls-rpt-bad-shape",
|
|
545
|
+
throwsWithCode(function () { b.network.smtp.tlsRpt.parseReport("123"); }, /tls-rpt-bad-shape/));
|
|
546
|
+
check("parseReport: JSON null → tls-rpt-bad-shape",
|
|
547
|
+
throwsWithCode(function () { b.network.smtp.tlsRpt.parseReport("null"); }, /tls-rpt-bad-shape/));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function testParseReportMissingRequiredField() {
|
|
551
|
+
var r = _validReport();
|
|
552
|
+
delete r["report-id"];
|
|
553
|
+
check("parseReport: missing required field → tls-rpt-missing-field",
|
|
554
|
+
throwsWithCode(function () { b.network.smtp.tlsRpt.parseReport(JSON.stringify(r)); }, /tls-rpt-missing-field/));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function testParseReportBadDateRange() {
|
|
558
|
+
var r = _validReport();
|
|
559
|
+
r["date-range"] = { "start-datetime": 1234, "end-datetime": "2026-01-02T00:00:00Z" };
|
|
560
|
+
check("parseReport: non-string date-range field → tls-rpt-bad-date-range",
|
|
561
|
+
throwsWithCode(function () { b.network.smtp.tlsRpt.parseReport(JSON.stringify(r)); }, /tls-rpt-bad-date-range/));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function testParseReportPoliciesNotArray() {
|
|
565
|
+
var r = _validReport();
|
|
566
|
+
r.policies = { "0": {} };
|
|
567
|
+
check("parseReport: policies not an array → tls-rpt-bad-policies",
|
|
568
|
+
throwsWithCode(function () { b.network.smtp.tlsRpt.parseReport(JSON.stringify(r)); }, /tls-rpt-bad-policies/));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function testParseReportTooManyPolicies() {
|
|
572
|
+
var r = _validReport();
|
|
573
|
+
r.policies = [];
|
|
574
|
+
for (var i = 0; i < 1025; i += 1) r.policies.push({ summary: {} }); // cap is 1024
|
|
575
|
+
check("parseReport: > 1024 policies → tls-rpt-too-many-policies",
|
|
576
|
+
throwsWithCode(function () { b.network.smtp.tlsRpt.parseReport(JSON.stringify(r)); }, /tls-rpt-too-many-policies/));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function testParseReportTooLarge() {
|
|
580
|
+
// A body larger than the 8 MiB cap is refused before any parse/decompress.
|
|
581
|
+
var big = Buffer.alloc(C.BYTES.mib(8) + 1, 0x20); // spaces, not gzip magic
|
|
582
|
+
check("parseReport: over-cap body → tls-rpt-too-large",
|
|
583
|
+
throwsWithCode(function () { b.network.smtp.tlsRpt.parseReport(big); }, /tls-rpt-too-large/));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function testParseReportHappyPathAggregates() {
|
|
587
|
+
var parsed = b.network.smtp.tlsRpt.parseReport(JSON.stringify(_validReport()));
|
|
588
|
+
check("parseReport: organization surfaced", parsed.organization === "reporter.example");
|
|
589
|
+
check("parseReport: report-id surfaced", parsed.reportId === "report-abc-123");
|
|
590
|
+
check("parseReport: contact surfaced", parsed.contact === "tls-reports@reporter.example");
|
|
591
|
+
check("parseReport: date-range surfaced",
|
|
592
|
+
parsed.dateRange.start === "2026-01-01T00:00:00Z" && parsed.dateRange.end === "2026-01-02T00:00:00Z");
|
|
593
|
+
check("parseReport: success/failure counts aggregated across policies",
|
|
594
|
+
parsed.totals.successful === 15 && parsed.totals.failure === 4);
|
|
595
|
+
check("parseReport: raw report echoed", parsed.raw && parsed.raw["report-id"] === "report-abc-123");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function testParseReportContactOptional() {
|
|
599
|
+
var r = _validReport();
|
|
600
|
+
delete r["contact-info"];
|
|
601
|
+
var parsed = b.network.smtp.tlsRpt.parseReport(JSON.stringify(r));
|
|
602
|
+
check("parseReport: absent contact-info → null", parsed.contact === null);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function testParseReportNonFiniteCountsSkipped() {
|
|
606
|
+
var r = _validReport();
|
|
607
|
+
r.policies = [
|
|
608
|
+
{ summary: { "total-successful-session-count": "not-a-number", "total-failure-session-count": 2 } },
|
|
609
|
+
{ summary: { "total-successful-session-count": 4, "total-failure-session-count": null } },
|
|
610
|
+
{ /* no summary at all */ },
|
|
611
|
+
];
|
|
612
|
+
var parsed = b.network.smtp.tlsRpt.parseReport(JSON.stringify(r));
|
|
613
|
+
check("parseReport: non-numeric counts ignored in aggregation",
|
|
614
|
+
parsed.totals.successful === 4 && parsed.totals.failure === 2);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function testParseReportGzipMagicSniff() {
|
|
618
|
+
var gz = zlib.gzipSync(Buffer.from(JSON.stringify(_validReport()), "utf8"));
|
|
619
|
+
var parsed = b.network.smtp.tlsRpt.parseReport(gz); // no contentType — sniffed by magic
|
|
620
|
+
check("parseReport: gzip body auto-detected by magic bytes",
|
|
621
|
+
parsed.organization === "reporter.example" && parsed.totals.successful === 15);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function testParseReportGzipViaContentType() {
|
|
625
|
+
var gz = zlib.gzipSync(Buffer.from(JSON.stringify(_validReport()), "utf8"));
|
|
626
|
+
var parsed = b.network.smtp.tlsRpt.parseReport(gz, { contentType: "application/tlsrpt+gzip" });
|
|
627
|
+
check("parseReport: gzip body decompressed via contentType hint",
|
|
628
|
+
parsed.reportId === "report-abc-123");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function testParseReportGunzipFailure() {
|
|
632
|
+
// gzip magic bytes but a truncated/garbage deflate stream → gunzip throws.
|
|
633
|
+
var bad = Buffer.from([0x1f, 0x8b, 0x08, 0x00, 0xde, 0xad, 0xbe, 0xef]);
|
|
634
|
+
check("parseReport: corrupt gzip → tls-rpt-gunzip-failed",
|
|
635
|
+
throwsWithCode(function () { b.network.smtp.tlsRpt.parseReport(bad); }, /tls-rpt-gunzip-failed/));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ---- MTA-STS fetch (mtaSts.fetch) — HTTPS-fetch success + cache-TTL clamp -
|
|
639
|
+
|
|
640
|
+
var _STS_ENFORCE = "version: STSv1\nmode: enforce\nmx: MX1.Example.COM\nmx: mx2.example.com\n";
|
|
641
|
+
|
|
642
|
+
async function testMtaStsFetchSuccessAndMaxAgeClamp() {
|
|
643
|
+
// A published _mta-sts TXT (v=STSv1 with an id) satisfies the RFC 8461
|
|
644
|
+
// §3.1 precondition, so fetch pulls + parses the HTTPS policy and caches
|
|
645
|
+
// it. max_age within the [1h, ~1y] window is honored verbatim as the
|
|
646
|
+
// cache TTL.
|
|
647
|
+
var body = _STS_ENFORCE + "max_age: 604800\n"; // 7 days, in-window
|
|
648
|
+
var rv = await withFakeHttpRequest(
|
|
649
|
+
async function () { return { statusCode: 200, headers: {}, body: Buffer.from(body, "utf8") }; },
|
|
650
|
+
function () {
|
|
651
|
+
return b.network.smtp.mtaSts.fetch("within.example", {
|
|
652
|
+
dnsLookup: async function () { return [["v=STSv1; id=WITHIN01"]]; },
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
check("mtaSts.fetch: enforce policy fetched + parsed",
|
|
656
|
+
rv && rv.mode === "enforce" && rv.version === "STSv1");
|
|
657
|
+
check("mtaSts.fetch: mx hosts lowercased from HTTPS body",
|
|
658
|
+
rv.mx.length === 2 && rv.mx[0] === "mx1.example.com");
|
|
659
|
+
check("mtaSts.fetch: policy id carried from the _mta-sts TXT", rv.id === "WITHIN01");
|
|
660
|
+
check("mtaSts.fetch: fetchedAt stamped", typeof rv.fetchedAt === "number" && rv.fetchedAt > 0);
|
|
661
|
+
check("mtaSts.fetch: in-window max_age honored as cache TTL",
|
|
662
|
+
rv._cacheTtlMs === 604800 * C.TIME.seconds(1));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function testMtaStsFetchMaxAgeCeilingClamp() {
|
|
666
|
+
// max_age above the RFC 8461 §3.2 ceiling (~1 year) clamps to the ceiling.
|
|
667
|
+
var body = _STS_ENFORCE + "max_age: 999999999\n";
|
|
668
|
+
var rv = await withFakeHttpRequest(
|
|
669
|
+
async function () { return { statusCode: 200, headers: {}, body: Buffer.from(body, "utf8") }; },
|
|
670
|
+
function () {
|
|
671
|
+
return b.network.smtp.mtaSts.fetch("ceil.example", {
|
|
672
|
+
dnsLookup: async function () { return [["v=STSv1; id=CEIL01"]]; },
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
check("mtaSts.fetch: over-ceiling max_age clamps to ~1y", rv._cacheTtlMs === C.TIME.weeks(52));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function testMtaStsFetchMaxAgeAbsentUsesDefault() {
|
|
679
|
+
// No max_age line → parsed.max_age null → default framework TTL (60 min).
|
|
680
|
+
var rv = await withFakeHttpRequest(
|
|
681
|
+
async function () { return { statusCode: 200, headers: {}, body: Buffer.from(_STS_ENFORCE, "utf8") }; },
|
|
682
|
+
function () {
|
|
683
|
+
return b.network.smtp.mtaSts.fetch("noage.example", {
|
|
684
|
+
dnsLookup: async function () { return [["v=STSv1;"]]; }, // no id= → policy id null
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
check("mtaSts.fetch: absent max_age → default cache TTL", rv._cacheTtlMs === C.TIME.minutes(60));
|
|
688
|
+
check("mtaSts.fetch: TXT without id= → policy id null", rv.id === null);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async function testMtaStsFetchTxtRecordShapes() {
|
|
692
|
+
// _fetchStsTxt tolerates a mixed TXT answer: a non-string chunk is
|
|
693
|
+
// skipped, and a plain-string record (not the array-of-chunks shape) is
|
|
694
|
+
// read directly. The id= token is extracted from whichever record
|
|
695
|
+
// carries v=STSv1.
|
|
696
|
+
var rv = await withFakeHttpRequest(
|
|
697
|
+
async function () { return { statusCode: 200, headers: {}, body: Buffer.from(_STS_ENFORCE + "max_age: 86400\n", "utf8") }; },
|
|
698
|
+
function () {
|
|
699
|
+
return b.network.smtp.mtaSts.fetch("mixed.example", {
|
|
700
|
+
dnsLookup: async function () { return [12345, "v=STSv1; id=MIXED42"]; }, // non-string then plain string
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
check("mtaSts.fetch: mixed TXT (non-string skipped, string record read) → id extracted",
|
|
704
|
+
rv && rv.id === "MIXED42" && rv.mode === "enforce");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function testMtaStsFetch404ReturnsNull() {
|
|
708
|
+
// A 404 at the well-known path means "no policy published" → null,
|
|
709
|
+
// NOT an error (RFC 8461 opportunistic behavior).
|
|
710
|
+
var rv = await withFakeHttpRequest(
|
|
711
|
+
async function () { return { statusCode: 404, headers: {}, body: Buffer.from("") }; },
|
|
712
|
+
function () {
|
|
713
|
+
return b.network.smtp.mtaSts.fetch("gone.example", {
|
|
714
|
+
dnsLookup: async function () { return [["v=STSv1; id=GONE01"]]; },
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
check("mtaSts.fetch: HTTPS 404 → null (no policy)", rv === null);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async function testMtaStsFetchNon2xxThrows() {
|
|
721
|
+
// A non-404 non-2xx (e.g. 500) is a hard fetch failure and surfaces a
|
|
722
|
+
// structured SmtpPolicyError.
|
|
723
|
+
var threw = await asyncThrowsWithCode(function () {
|
|
724
|
+
return withFakeHttpRequest(
|
|
725
|
+
async function () { return { statusCode: 500, headers: {}, body: Buffer.from("") }; },
|
|
726
|
+
function () {
|
|
727
|
+
return b.network.smtp.mtaSts.fetch("err5.example", {
|
|
728
|
+
dnsLookup: async function () { return [["v=STSv1; id=ERR501"]]; },
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
}, /mta-sts-fetch-failed/);
|
|
732
|
+
check("mtaSts.fetch: 5xx → mta-sts-fetch-failed", threw);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async function testMtaStsFetchHttpThrowIsOpportunisticNull() {
|
|
736
|
+
// A network / TLS error from the httpClient on an un-cached first fetch
|
|
737
|
+
// yields "no enforceable policy" → null (documented TOFU fallback).
|
|
738
|
+
var rv = await withFakeHttpRequest(
|
|
739
|
+
async function () { throw new Error("ECONNRESET at policy endpoint"); },
|
|
740
|
+
function () {
|
|
741
|
+
return b.network.smtp.mtaSts.fetch("reset.example", {
|
|
742
|
+
dnsLookup: async function () { return [["v=STSv1; id=RESET01"]]; },
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
check("mtaSts.fetch: httpClient throw → opportunistic null", rv === null);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// ---- DANE tlsa (dane.tlsa) — resolveTlsa dispatch + DNSSEC gate -----------
|
|
749
|
+
|
|
750
|
+
function _tlsaRec(usage, selector, match, data) {
|
|
751
|
+
return { certUsage: usage, selector: selector, match: match, data: data };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function testDaneTlsaMapsRecordsWhenDnssecAsserted() {
|
|
755
|
+
// With opts.dnssecValidated:true the raw node:dns TLSA rows are
|
|
756
|
+
// normalized to { usage, selector, mtype, dataHex }. Buffer data is
|
|
757
|
+
// hex-encoded; a non-Buffer data field is stringified.
|
|
758
|
+
var rv = await withFakeResolveTlsa(
|
|
759
|
+
async function () {
|
|
760
|
+
return [
|
|
761
|
+
_tlsaRec(3, 1, 1, Buffer.from([0xab, 0xcd])),
|
|
762
|
+
_tlsaRec(2, 0, 1, "deadbeef"),
|
|
763
|
+
];
|
|
764
|
+
},
|
|
765
|
+
function () { return b.network.smtp.dane.tlsa("example.com", 25, { dnssecValidated: true }); });
|
|
766
|
+
check("dane.tlsa: DNSSEC-asserted → records normalized",
|
|
767
|
+
rv.length === 2 && rv[0].usage === 3 && rv[0].selector === 1 && rv[0].mtype === 1);
|
|
768
|
+
check("dane.tlsa: Buffer data hex-encoded", rv[0].dataHex === "abcd");
|
|
769
|
+
check("dane.tlsa: non-Buffer data stringified", rv[1].dataHex === "deadbeef");
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function testDaneTlsaDefaultPortWhenOmitted() {
|
|
773
|
+
// Port omitted → defaults to 25 (the qname is _25._tcp.<domain>); the
|
|
774
|
+
// lookup still succeeds through the same DNSSEC-asserted path.
|
|
775
|
+
var seenQname = null;
|
|
776
|
+
var rv = await withFakeResolveTlsa(
|
|
777
|
+
async function (qname) { seenQname = qname; return [_tlsaRec(3, 0, 1, Buffer.from([0x01]))]; },
|
|
778
|
+
function () { return b.network.smtp.dane.tlsa("mail.example.com", undefined, { dnssecValidated: true }); });
|
|
779
|
+
check("dane.tlsa: omitted port defaults to 25 in the qname",
|
|
780
|
+
seenQname === "_25._tcp.mail.example.com" && rv.length === 1);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async function testDaneTlsaRefusesWithoutDnssec() {
|
|
784
|
+
// The default (no opts.dnssecValidated) REFUSES to use TLSA records —
|
|
785
|
+
// RFC 7672 §1.3 fail-closed: unvalidated records MUST NOT be used.
|
|
786
|
+
var threw = await asyncThrowsWithCode(function () {
|
|
787
|
+
return withFakeResolveTlsa(
|
|
788
|
+
async function () { return [_tlsaRec(3, 0, 1, Buffer.from([0x01]))]; },
|
|
789
|
+
function () { return b.network.smtp.dane.tlsa("example.com", 25); });
|
|
790
|
+
}, /dane-no-dnssec/);
|
|
791
|
+
check("dane.tlsa: no dnssecValidated → dane-no-dnssec (fail-closed)", threw);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
async function testDaneTlsaEmptyOnNxAndNoData() {
|
|
795
|
+
// ENOTFOUND / ENODATA are "domain publishes no TLSA" → empty array, not
|
|
796
|
+
// an error (and the DNSSEC gate is never reached because there are no
|
|
797
|
+
// records to use).
|
|
798
|
+
var nx = await withFakeResolveTlsa(
|
|
799
|
+
async function () { var e = new Error("nx"); e.code = "ENOTFOUND"; throw e; },
|
|
800
|
+
function () { return b.network.smtp.dane.tlsa("nx.example.com", 25, { dnssecValidated: true }); });
|
|
801
|
+
check("dane.tlsa: ENOTFOUND → []", Array.isArray(nx) && nx.length === 0);
|
|
802
|
+
|
|
803
|
+
var nd = await withFakeResolveTlsa(
|
|
804
|
+
async function () { var e = new Error("nodata"); e.code = "ENODATA"; throw e; },
|
|
805
|
+
function () { return b.network.smtp.dane.tlsa("nd.example.com", 25, { dnssecValidated: true }); });
|
|
806
|
+
check("dane.tlsa: ENODATA → []", Array.isArray(nd) && nd.length === 0);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async function testDaneTlsaLookupFailure() {
|
|
810
|
+
// A non-NX resolver error (SERVFAIL, timeout) is surfaced as a
|
|
811
|
+
// structured lookup failure.
|
|
812
|
+
var threw = await asyncThrowsWithCode(function () {
|
|
813
|
+
return withFakeResolveTlsa(
|
|
814
|
+
async function () { var e = new Error("SERVFAIL"); e.code = "ESERVFAIL"; throw e; },
|
|
815
|
+
function () { return b.network.smtp.dane.tlsa("srvfail.example.com", 25, { dnssecValidated: true }); });
|
|
816
|
+
}, /dane-lookup-failed/);
|
|
817
|
+
check("dane.tlsa: resolver SERVFAIL → dane-lookup-failed", threw);
|
|
818
|
+
|
|
819
|
+
// A resolver error with an empty (falsy) message still yields a
|
|
820
|
+
// structured failure — the detail falls back to String(e).
|
|
821
|
+
var threwNoMsg = await asyncThrowsWithCode(function () {
|
|
822
|
+
return withFakeResolveTlsa(
|
|
823
|
+
async function () { var e = new Error(""); e.code = "EWEIRD"; throw e; }, // falsy .message
|
|
824
|
+
function () { return b.network.smtp.dane.tlsa("weird.example.com", 25, { dnssecValidated: true }); });
|
|
825
|
+
}, /dane-lookup-failed/);
|
|
826
|
+
check("dane.tlsa: message-less resolver error → dane-lookup-failed", threwNoMsg);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function testDaneTlsaNullRecordsYieldEmpty() {
|
|
830
|
+
// A resolver that resolves null (rather than an array) still produces a
|
|
831
|
+
// safe empty result after the DNSSEC gate.
|
|
832
|
+
var rv = await withFakeResolveTlsa(
|
|
833
|
+
async function () { return null; },
|
|
834
|
+
function () { return b.network.smtp.dane.tlsa("nullrecs.example.com", 25, { dnssecValidated: true }); });
|
|
835
|
+
check("dane.tlsa: null resolver result → []", Array.isArray(rv) && rv.length === 0);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function testDaneTlsaUnavailableRuntime() {
|
|
839
|
+
// On a runtime without node:dns.resolveTlsa the primitive refuses
|
|
840
|
+
// rather than silently degrading.
|
|
841
|
+
var threw = await asyncThrowsWithCode(function () {
|
|
842
|
+
return withFakeResolveTlsa(undefined, function () {
|
|
843
|
+
return b.network.smtp.dane.tlsa("example.com", 25, { dnssecValidated: true });
|
|
844
|
+
});
|
|
845
|
+
}, /dane-unavailable/);
|
|
846
|
+
check("dane.tlsa: resolveTlsa missing → dane-unavailable", threw);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// ---- DANE verifyChain (dane.verifyChain) — input guards + DANE-TA chain ---
|
|
850
|
+
|
|
851
|
+
function testVerifyChainInputGuards() {
|
|
852
|
+
check("verifyChain: empty chain → dane-bad-chain",
|
|
853
|
+
throwsWithCode(function () { b.network.smtp.dane.verifyChain([], []); }, /dane-bad-chain/));
|
|
854
|
+
check("verifyChain: non-array chain → dane-bad-chain",
|
|
855
|
+
throwsWithCode(function () { b.network.smtp.dane.verifyChain("nope", []); }, /dane-bad-chain/));
|
|
856
|
+
check("verifyChain: non-Buffer chain entry → dane-bad-chain",
|
|
857
|
+
throwsWithCode(function () { b.network.smtp.dane.verifyChain(["not-a-buffer"], []); }, /dane-bad-chain/));
|
|
858
|
+
check("verifyChain: non-array tlsaRecords → dane-bad-tlsa",
|
|
859
|
+
throwsWithCode(function () { b.network.smtp.dane.verifyChain([Buffer.from("x")], "nope"); }, /dane-bad-tlsa/));
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function testVerifyChainDaneEeSha256() {
|
|
863
|
+
// DANE-EE / Cert / SHA-256 — the mtype=1 hash branch against the leaf.
|
|
864
|
+
var leaf = Buffer.from("leaf-bytes-for-sha256", "utf8");
|
|
865
|
+
var sha256Hex = nodeCrypto.createHash("sha256").update(leaf).digest("hex");
|
|
866
|
+
var rv = b.network.smtp.dane.verifyChain([leaf], [{ usage: 3, selector: 0, mtype: 1, dataHex: sha256Hex }]);
|
|
867
|
+
check("verifyChain: DANE-EE / Cert / SHA-256 matches via hash",
|
|
868
|
+
rv.ok === true && rv.matches[0].usage === "DANE-EE" && rv.matches[0].mtype === "SHA-256");
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function testVerifyChainDaneEeFullNoMatchAndBadDataHex() {
|
|
872
|
+
var leaf = Buffer.from("leaf", "utf8");
|
|
873
|
+
// mtype=0 (Full) with the wrong bytes → no match, ok=false.
|
|
874
|
+
var wrong = b.network.smtp.dane.verifyChain([leaf], [{ usage: 3, selector: 0, mtype: 0, dataHex: "00" }]);
|
|
875
|
+
check("verifyChain: DANE-EE Full wrong bytes → no match", wrong.ok === false && wrong.matches.length === 0);
|
|
876
|
+
// Non-string dataHex is coerced to empty → cannot match.
|
|
877
|
+
var noHex = b.network.smtp.dane.verifyChain([leaf], [{ usage: 3, selector: 0, mtype: 0, dataHex: null }]);
|
|
878
|
+
check("verifyChain: non-string dataHex → no match", noHex.ok === false && noHex.matches.length === 0);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function testVerifyChainPkixModesRejectedByDefault() {
|
|
882
|
+
// usage 0/1 WITHOUT opts.allowPkixModes → structured pkix-modes-not-allowed
|
|
883
|
+
// error, ok=false (RFC 7672 §3.1.1 — SMTP DANE honors only DANE-TA/EE).
|
|
884
|
+
var fake = Buffer.from("anything", "utf8");
|
|
885
|
+
var rvEe = b.network.smtp.dane.verifyChain([fake], [{ usage: 1, selector: 0, mtype: 1, dataHex: "abc" }]);
|
|
886
|
+
check("verifyChain: PKIX-EE without opt-in → pkix-modes-not-allowed",
|
|
887
|
+
rvEe.ok === false && rvEe.errors[0] && rvEe.errors[0].reason === "pkix-modes-not-allowed");
|
|
888
|
+
var rvTa = b.network.smtp.dane.verifyChain([fake], [{ usage: 0, selector: 0, mtype: 1, dataHex: "abc" }]);
|
|
889
|
+
check("verifyChain: PKIX-TA without opt-in → pkix-modes-not-allowed",
|
|
890
|
+
rvTa.ok === false && rvTa.errors[0] && rvTa.errors[0].reason === "pkix-modes-not-allowed");
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function testVerifyChainDaneTaSyntheticUnverified() {
|
|
894
|
+
// usage=2 against non-DER buffers: the TLSA hash-matches the non-leaf
|
|
895
|
+
// cert, but the ASN.1 chain-order extraction fails, so the match is
|
|
896
|
+
// accepted-but-flagged chainOrderUnverified (synthetic/test inputs only).
|
|
897
|
+
var leaf = Buffer.from("leaf-synthetic", "utf8");
|
|
898
|
+
var ca = Buffer.from("ca-synthetic", "utf8");
|
|
899
|
+
var caSha = nodeCrypto.createHash("sha256").update(ca).digest("hex");
|
|
900
|
+
var rv = b.network.smtp.dane.verifyChain([leaf, ca], [{ usage: 2, selector: 0, mtype: 1, dataHex: caSha }]);
|
|
901
|
+
check("verifyChain: DANE-TA on non-DER buffers → match flagged chainOrderUnverified",
|
|
902
|
+
rv.ok === true && rv.matches[0].usage === "DANE-TA" &&
|
|
903
|
+
rv.matches[0].certIndex === 1 && rv.matches[0].chainOrderUnverified === true);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function testVerifyChainDaneTaChainOrderVerified() {
|
|
907
|
+
// Real DER: the matched DANE-TA cert's Subject equals the leaf's Issuer,
|
|
908
|
+
// so the RFC 7672 §3.1.1 chain-order check passes and the match is NOT
|
|
909
|
+
// flagged unverified.
|
|
910
|
+
var caName = _derName("Test Root CA");
|
|
911
|
+
var leafName = _derName("leaf.example.com");
|
|
912
|
+
var leaf = _derCert(caName, leafName); // issuer=CA, subject=leaf
|
|
913
|
+
var ta = _derCert(caName, caName); // self-issued root
|
|
914
|
+
var taSha = nodeCrypto.createHash("sha256").update(ta).digest("hex");
|
|
915
|
+
var rv = b.network.smtp.dane.verifyChain([leaf, ta], [{ usage: 2, selector: 0, mtype: 1, dataHex: taSha }]);
|
|
916
|
+
check("verifyChain: DANE-TA ordered chain → verified match",
|
|
917
|
+
rv.ok === true && rv.matches.length === 1 &&
|
|
918
|
+
rv.matches[0].usage === "DANE-TA" && rv.matches[0].certIndex === 1 &&
|
|
919
|
+
rv.matches[0].chainOrderUnverified === undefined);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function testVerifyChainDaneTaChainOrderMismatch() {
|
|
923
|
+
// Real DER where the hash-matching cert's Subject does NOT equal the
|
|
924
|
+
// leaf's Issuer: the match is refused and a chain-order-mismatch error
|
|
925
|
+
// is recorded (a cert that merely hash-matches is not accepted as the
|
|
926
|
+
// trust anchor unless it is actually the parent).
|
|
927
|
+
var caName = _derName("Real Issuer CA");
|
|
928
|
+
var leafName = _derName("leaf.example.com");
|
|
929
|
+
var otherName = _derName("Unrelated Subject");
|
|
930
|
+
var leaf = _derCert(caName, leafName); // leaf.issuer = caName
|
|
931
|
+
var rogue = _derCert(caName, otherName); // subject != caName
|
|
932
|
+
var rogueSha = nodeCrypto.createHash("sha256").update(rogue).digest("hex");
|
|
933
|
+
var rv = b.network.smtp.dane.verifyChain([leaf, rogue], [{ usage: 2, selector: 0, mtype: 1, dataHex: rogueSha }]);
|
|
934
|
+
check("verifyChain: DANE-TA out-of-order chain → refused with chain-order-mismatch",
|
|
935
|
+
rv.ok === false && rv.matches.length === 0 &&
|
|
936
|
+
rv.errors[0] && rv.errors[0].reason === "dane-ta-chain-order-mismatch");
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function testVerifyChainDaneTaSkipsNonMatchingCert() {
|
|
940
|
+
// A three-cert chain where the TLSA record matches only the last cert:
|
|
941
|
+
// the non-matching intermediate is skipped and the loop continues up the
|
|
942
|
+
// chain until the trust anchor matches.
|
|
943
|
+
var leaf = Buffer.from("leaf-3chain", "utf8");
|
|
944
|
+
var inter = Buffer.from("intermediate-3chain", "utf8");
|
|
945
|
+
var ta = Buffer.from("trust-anchor-3chain", "utf8");
|
|
946
|
+
var taSha = nodeCrypto.createHash("sha256").update(ta).digest("hex");
|
|
947
|
+
var rv = b.network.smtp.dane.verifyChain([leaf, inter, ta], [{ usage: 2, selector: 0, mtype: 1, dataHex: taSha }]);
|
|
948
|
+
check("verifyChain: DANE-TA skips non-matching intermediate, matches anchor",
|
|
949
|
+
rv.ok === true && rv.matches[0].usage === "DANE-TA" && rv.matches[0].certIndex === 2);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function testVerifyChainSpkiSelectorRealDer() {
|
|
953
|
+
// selector=1 (SPKI) against a REAL DER leaf: the SubjectPublicKeyInfo is
|
|
954
|
+
// extracted from tbsCertificate and matched. Full (mtype 0) compares the
|
|
955
|
+
// SPKI bytes verbatim; the DER embeds _SPKI_DER, so its hex is the
|
|
956
|
+
// association data.
|
|
957
|
+
var leaf = _derCert(_derName("Issuer"), _derName("leaf.example.com"));
|
|
958
|
+
var full = b.network.smtp.dane.verifyChain([leaf],
|
|
959
|
+
[{ usage: 3, selector: 1, mtype: 0, dataHex: _SPKI_DER.toString("hex") }]);
|
|
960
|
+
check("verifyChain: DANE-EE / SPKI / Full matches extracted SubjectPublicKeyInfo",
|
|
961
|
+
full.ok === true && full.matches[0].usage === "DANE-EE" && full.matches[0].mtype === "Full");
|
|
962
|
+
|
|
963
|
+
// SHA-256 over the same SPKI bytes → hash match.
|
|
964
|
+
var spkiSha = nodeCrypto.createHash("sha256").update(_SPKI_DER).digest("hex");
|
|
965
|
+
var hashed = b.network.smtp.dane.verifyChain([leaf],
|
|
966
|
+
[{ usage: 3, selector: 1, mtype: 1, dataHex: spkiSha }]);
|
|
967
|
+
check("verifyChain: DANE-EE / SPKI / SHA-256 matches SPKI digest",
|
|
968
|
+
hashed.ok === true && hashed.matches[0].mtype === "SHA-256");
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function testVerifyChainSelectorAndMtypeFallthroughs() {
|
|
972
|
+
var leaf = _derCert(_derName("Issuer"), _derName("leaf.example.com"));
|
|
973
|
+
// selector=1 mtype=1 with the WRONG digest → SPKI extracted but no match.
|
|
974
|
+
var wrongSha = b.network.smtp.dane.verifyChain([leaf],
|
|
975
|
+
[{ usage: 3, selector: 1, mtype: 1, dataHex: "00".repeat(32) }]);
|
|
976
|
+
check("verifyChain: SPKI SHA-256 wrong digest → no match",
|
|
977
|
+
wrongSha.ok === false && wrongSha.matches.length === 0);
|
|
978
|
+
// selector=1 mtype=2 (SHA-512) wrong digest → no match (exercises the
|
|
979
|
+
// SHA-512 branch's non-match arm).
|
|
980
|
+
var wrong512 = b.network.smtp.dane.verifyChain([leaf],
|
|
981
|
+
[{ usage: 3, selector: 1, mtype: 2, dataHex: "00".repeat(64) }]);
|
|
982
|
+
check("verifyChain: SPKI SHA-512 wrong digest → no match",
|
|
983
|
+
wrong512.ok === false && wrong512.matches.length === 0);
|
|
984
|
+
// selector=2 is neither Cert(0) nor SPKI(1) → _selectorBytes returns
|
|
985
|
+
// null → the record cannot match.
|
|
986
|
+
var badSelector = b.network.smtp.dane.verifyChain([leaf],
|
|
987
|
+
[{ usage: 3, selector: 2, mtype: 0, dataHex: "aa" }]);
|
|
988
|
+
check("verifyChain: unknown selector → no bytes → no match",
|
|
989
|
+
badSelector.ok === false && badSelector.matches.length === 0);
|
|
990
|
+
// mtype=9 is not a defined matching type → falls through to no match.
|
|
991
|
+
var badMtype = b.network.smtp.dane.verifyChain([leaf],
|
|
992
|
+
[{ usage: 3, selector: 0, mtype: 9, dataHex: "aa" }]);
|
|
993
|
+
check("verifyChain: unknown mtype → no match", badMtype.ok === false && badMtype.matches.length === 0);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function testVerifyChainMalformedDerFailsClosed() {
|
|
997
|
+
// A hostile / malformed peer cert must never crash the DANE verifier and
|
|
998
|
+
// must never yield a spuriously-verified match from unparseable ASN.1.
|
|
999
|
+
// These fixtures drive the structural guard arms in the cert-field
|
|
1000
|
+
// extractors (non-SEQUENCE top, short TBSCertificate, non-SEQUENCE tbs,
|
|
1001
|
+
// wrong-typed SubjectPublicKeyInfo slot) — every one returns null so the
|
|
1002
|
+
// verdict stays fail-closed.
|
|
1003
|
+
var sigAlg = asn1.writeSequence([ asn1.writeOid("1.2.840.113549.1.1.11"), asn1.writeNull() ]);
|
|
1004
|
+
var bit = asn1.writeNode(asn1.TAG.BIT_STRING, Buffer.from([0x00, 0xaa]));
|
|
1005
|
+
var name = _derName("x");
|
|
1006
|
+
var validity = asn1.writeSequence([
|
|
1007
|
+
asn1.writeNode(asn1.TAG.UTC_TIME, Buffer.from("260101000000Z")),
|
|
1008
|
+
asn1.writeNode(asn1.TAG.UTC_TIME, Buffer.from("270101000000Z")),
|
|
1009
|
+
]);
|
|
1010
|
+
|
|
1011
|
+
// (a) valid DER whose top node is an INTEGER, not a SEQUENCE.
|
|
1012
|
+
var intCert = asn1.writeInteger(Buffer.from([0x2a]));
|
|
1013
|
+
// (b) SEQUENCE whose tbsCertificate SEQUENCE holds too few fields.
|
|
1014
|
+
var shortTbs = asn1.writeSequence([
|
|
1015
|
+
asn1.writeSequence([ asn1.writeInteger(Buffer.from([1])), asn1.writeInteger(Buffer.from([2])) ]),
|
|
1016
|
+
sigAlg, bit,
|
|
1017
|
+
]);
|
|
1018
|
+
// (c) SEQUENCE whose first child (the tbs slot) is an INTEGER.
|
|
1019
|
+
var nonSeqTbs = asn1.writeSequence([ asn1.writeInteger(Buffer.from([1])), sigAlg, bit ]);
|
|
1020
|
+
// (d) well-formed field layout but the SubjectPublicKeyInfo slot is an
|
|
1021
|
+
// INTEGER instead of a SEQUENCE.
|
|
1022
|
+
var wrongSpki = asn1.writeSequence([
|
|
1023
|
+
asn1.writeSequence([
|
|
1024
|
+
asn1.writeContextExplicit(0, asn1.writeInteger(Buffer.from([2]))),
|
|
1025
|
+
asn1.writeInteger(Buffer.from([1])), sigAlg, name, validity, name,
|
|
1026
|
+
asn1.writeInteger(Buffer.from([7])), // SPKI slot: INTEGER, not SEQUENCE
|
|
1027
|
+
]),
|
|
1028
|
+
sigAlg, bit,
|
|
1029
|
+
]);
|
|
1030
|
+
|
|
1031
|
+
// (e) SEQUENCE with an empty value → zero children.
|
|
1032
|
+
var emptySeq = asn1.writeSequence([]);
|
|
1033
|
+
// (f) SEQUENCE whose value is a truncated inner tag → readSequence throws.
|
|
1034
|
+
var truncatedSeq = asn1.writeNode(asn1.TAG.SEQUENCE | 0x20, Buffer.from([0x30]));
|
|
1035
|
+
// (g) SEQUENCE whose tbs child is itself a SEQUENCE with a truncated
|
|
1036
|
+
// value → the inner readSequence(tbs.value) throws.
|
|
1037
|
+
var truncatedTbs = asn1.writeSequence([
|
|
1038
|
+
asn1.writeNode(asn1.TAG.SEQUENCE | 0x20, Buffer.from([0x30])), sigAlg, bit,
|
|
1039
|
+
]);
|
|
1040
|
+
|
|
1041
|
+
var mangled = [intCert, shortTbs, nonSeqTbs, wrongSpki, emptySeq, truncatedSeq, truncatedTbs];
|
|
1042
|
+
for (var i = 0; i < mangled.length; i += 1) {
|
|
1043
|
+
var cert = mangled[i];
|
|
1044
|
+
var sha = nodeCrypto.createHash("sha256").update(cert).digest("hex");
|
|
1045
|
+
var noThrowTa = true, taRv = null;
|
|
1046
|
+
try { taRv = b.network.smtp.dane.verifyChain([cert, cert], [{ usage: 2, selector: 0, mtype: 1, dataHex: sha }]); }
|
|
1047
|
+
catch (_e) { noThrowTa = false; }
|
|
1048
|
+
check("verifyChain: malformed cert #" + i + " DANE-TA does not throw", noThrowTa && taRv !== null);
|
|
1049
|
+
|
|
1050
|
+
var noThrowEe = true, eeRv = null;
|
|
1051
|
+
try { eeRv = b.network.smtp.dane.verifyChain([cert], [{ usage: 3, selector: 1, mtype: 0, dataHex: "aa" }]); }
|
|
1052
|
+
catch (_e) { noThrowEe = false; }
|
|
1053
|
+
check("verifyChain: malformed cert #" + i + " SPKI selector fails closed (no match, no throw)",
|
|
1054
|
+
noThrowEe && eeRv !== null && eeRv.ok === false && eeRv.matches.length === 0);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// ---- TLS-RPT submit (tlsRpt.submit) — rua guard ---------------------------
|
|
1059
|
+
|
|
1060
|
+
async function testSubmitRuaGuard() {
|
|
1061
|
+
check("submit: empty rua array → tls-rpt-bad-rua",
|
|
1062
|
+
await asyncThrowsWithCode(function () {
|
|
1063
|
+
return b.network.smtp.tlsRpt.submit({ "organization-name": "example.com" }, { rua: [] });
|
|
1064
|
+
}, /tls-rpt-bad-rua/));
|
|
1065
|
+
check("submit: missing rua (non-array) → tls-rpt-bad-rua",
|
|
1066
|
+
await asyncThrowsWithCode(function () {
|
|
1067
|
+
return b.network.smtp.tlsRpt.submit({ "organization-name": "example.com" }, {});
|
|
1068
|
+
}, /tls-rpt-bad-rua/));
|
|
1069
|
+
// Called with no opts at all → defaults to {} → the same rua guard fires.
|
|
1070
|
+
check("submit: no opts → tls-rpt-bad-rua",
|
|
1071
|
+
await asyncThrowsWithCode(function () {
|
|
1072
|
+
return b.network.smtp.tlsRpt.submit({ "organization-name": "example.com" });
|
|
1073
|
+
}, /tls-rpt-bad-rua/));
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async function testSubmitMailtoSubjectFallbacks() {
|
|
1077
|
+
// A mailto submission for a report missing organization-name / report-id
|
|
1078
|
+
// still produces a subject; the missing fields fall back to empty
|
|
1079
|
+
// strings rather than "undefined".
|
|
1080
|
+
var rv = await b.network.smtp.tlsRpt.submit({}, { rua: ["mailto:tls@example.com"] });
|
|
1081
|
+
var e = rv.results[0];
|
|
1082
|
+
check("submit: mailto with bare report → prepared body, empty-string subject fields",
|
|
1083
|
+
e.kind === "mailto" && e.ok === true && Buffer.isBuffer(e.mailto.body) &&
|
|
1084
|
+
e.mailto.subject.indexOf("undefined") === -1 &&
|
|
1085
|
+
e.mailto.subject.indexOf("Report-ID: <>") !== -1);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// ---- runner --------------------------------------------------------------
|
|
1089
|
+
|
|
1090
|
+
async function run() {
|
|
1091
|
+
testParseStsEmptyAndNonString();
|
|
1092
|
+
testParseStsBadMode();
|
|
1093
|
+
testParseStsBadVersion();
|
|
1094
|
+
testParseStsModeVariantsAndMaxAge();
|
|
1095
|
+
|
|
1096
|
+
testMatchMxDefensiveReturns();
|
|
1097
|
+
testMatchMxCaseAndDotEdges();
|
|
1098
|
+
|
|
1099
|
+
await testMtaStsFetchDomainGuard();
|
|
1100
|
+
await testMtaStsFetchNoTxtRecord();
|
|
1101
|
+
await testMtaStsFetchTxtAbsent();
|
|
1102
|
+
await testMtaStsFetchSuccessAndMaxAgeClamp();
|
|
1103
|
+
await testMtaStsFetchMaxAgeCeilingClamp();
|
|
1104
|
+
await testMtaStsFetchMaxAgeAbsentUsesDefault();
|
|
1105
|
+
await testMtaStsFetchTxtRecordShapes();
|
|
1106
|
+
await testMtaStsFetch404ReturnsNull();
|
|
1107
|
+
await testMtaStsFetchNon2xxThrows();
|
|
1108
|
+
await testMtaStsFetchHttpThrowIsOpportunisticNull();
|
|
1109
|
+
await testMtaStsFetchTxtHardLookupFailure();
|
|
1110
|
+
|
|
1111
|
+
await testDaneTlsaDomainGuard();
|
|
1112
|
+
await testDaneTlsaMapsRecordsWhenDnssecAsserted();
|
|
1113
|
+
await testDaneTlsaDefaultPortWhenOmitted();
|
|
1114
|
+
await testDaneTlsaRefusesWithoutDnssec();
|
|
1115
|
+
await testDaneTlsaEmptyOnNxAndNoData();
|
|
1116
|
+
await testDaneTlsaLookupFailure();
|
|
1117
|
+
await testDaneTlsaNullRecordsYieldEmpty();
|
|
1118
|
+
await testDaneTlsaUnavailableRuntime();
|
|
1119
|
+
|
|
1120
|
+
testDaneRecordShapeGuard();
|
|
1121
|
+
testDaneRecordShapeAllLabels();
|
|
1122
|
+
|
|
1123
|
+
testVerifyChainInputGuards();
|
|
1124
|
+
testVerifyChainPkixOptIn();
|
|
1125
|
+
testVerifyChainPkixModesRejectedByDefault();
|
|
1126
|
+
testVerifyChainUnsupportedUsage();
|
|
1127
|
+
testVerifyChainDaneEeSha256();
|
|
1128
|
+
testVerifyChainDaneEeSha512();
|
|
1129
|
+
testVerifyChainDaneEeFullNoMatchAndBadDataHex();
|
|
1130
|
+
testVerifyChainSpkiSelectorNoBytes();
|
|
1131
|
+
testVerifyChainDaneTaSyntheticUnverified();
|
|
1132
|
+
testVerifyChainDaneTaChainOrderVerified();
|
|
1133
|
+
testVerifyChainDaneTaChainOrderMismatch();
|
|
1134
|
+
testVerifyChainDaneTaSkipsNonMatchingCert();
|
|
1135
|
+
testVerifyChainSpkiSelectorRealDer();
|
|
1136
|
+
testVerifyChainSelectorAndMtypeFallthroughs();
|
|
1137
|
+
testVerifyChainMalformedDerFailsClosed();
|
|
1138
|
+
|
|
1139
|
+
testRecordShapeTypeGuards();
|
|
1140
|
+
testRecordShapePolicyDefaults();
|
|
1141
|
+
|
|
1142
|
+
await testFetchPolicyDomainGuard();
|
|
1143
|
+
await testFetchPolicyRuaRequired();
|
|
1144
|
+
await testFetchPolicyNoMatchingRecord();
|
|
1145
|
+
await testFetchPolicyHardLookupFailure();
|
|
1146
|
+
await testFetchPolicyNoTxtAtAll();
|
|
1147
|
+
await testFetchPolicyTrimsAndSplitsRua();
|
|
1148
|
+
await testFetchPolicyStringRecordAmongNonMatching();
|
|
1149
|
+
|
|
1150
|
+
await testSubmitRejectsNonObjectReport();
|
|
1151
|
+
await testSubmitHttpsNon2xx();
|
|
1152
|
+
await testSubmitHttpsSuccess2xx();
|
|
1153
|
+
await testSubmitHttpsThrows();
|
|
1154
|
+
await testSubmitHttpsThrowsNonError();
|
|
1155
|
+
await testSubmitInvalidMailtoAddr();
|
|
1156
|
+
await testSubmitUnsupportedScheme();
|
|
1157
|
+
await testSubmitValidMailtoBody();
|
|
1158
|
+
await testSubmitRuaGuard();
|
|
1159
|
+
await testSubmitMailtoSubjectFallbacks();
|
|
1160
|
+
|
|
1161
|
+
testParseReportBadInput();
|
|
1162
|
+
testParseReportBadJsonAndShape();
|
|
1163
|
+
testParseReportMissingRequiredField();
|
|
1164
|
+
testParseReportBadDateRange();
|
|
1165
|
+
testParseReportPoliciesNotArray();
|
|
1166
|
+
testParseReportTooManyPolicies();
|
|
1167
|
+
testParseReportTooLarge();
|
|
1168
|
+
testParseReportHappyPathAggregates();
|
|
1169
|
+
testParseReportContactOptional();
|
|
1170
|
+
testParseReportNonFiniteCountsSkipped();
|
|
1171
|
+
testParseReportGzipMagicSniff();
|
|
1172
|
+
testParseReportGzipViaContentType();
|
|
1173
|
+
testParseReportGunzipFailure();
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
module.exports = { run: run };
|
|
1177
|
+
|
|
1178
|
+
if (require.main === module) {
|
|
1179
|
+
run().then(function () { console.log("OK"); })
|
|
1180
|
+
.catch(function (e) { console.error(e.stack || e); process.exit(1); });
|
|
1181
|
+
}
|